[
  {
    "path": ".editorconfig",
    "content": "# EditorConfig helps developers define and maintain consistent\n# coding styles between different editors and IDEs\n# editorconfig.org\n\nroot = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\nindent_size = 2\nindent_style = space\ninsert_final_newline = true\nquote_type = single\ntrim_trailing_whitespace = true\n\n[*.go]\nindent_style = tab\n\n[*.{sh,bash,bats}]\nindent_size = 4\nsimplify = true\n"
  },
  {
    "path": ".gitattributes",
    "content": "# All Linux scripts should have LF line endings\n# But only text files should be changed (not any binaries / images / etc.)\nresources/linux/** text=auto eol=lf\nresources/setup-spin text=auto eol=lf\npkg/rancher-desktop/assets/scripts/** text=auto eol=lf\n"
  },
  {
    "path": ".github/.yamlfmt",
    "content": "formatter:\n  indentless_arrays: true\n  retain_line_breaks: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Report Rancher Desktop issue\nlabels: [\"kind/bug\"]\nbody:\n- type: textarea\n  attributes:\n    label: Actual Behavior\n    description: \"A clear and concise description of what the bug is.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Steps to Reproduce\n    description: \"Please, describe the steps to reproduce the behaviour.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Result\n    description: \"Please, show what error or behaviour you're seeing.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Expected Behavior\n    description: \"A clear and concise description of what you expected to happen.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Additional Information\n    description: >-\n      Add any other context about the problem here.  Please make sure to excerpt\n      or attach logs.  Include screenshots if appropriate, but they are not a\n      replacement for logs.\n- type: input\n  attributes:\n    label: Rancher Desktop Version\n    description: \"What version of Rancher Desktop are you using?\"\n    placeholder: \"e.g. 1.1.1\"\n  validations:\n    required: true\n- type: input\n  attributes:\n    label: Rancher Desktop K8s Version\n    description: \"What version of Kubernetes are you using?\"\n    placeholder: \"e.g. 1.99.9\"\n  validations:\n    required: true\n- type: dropdown\n  attributes:\n    label: \"Which container engine are you using?\"\n    options:\n      - containerd (nerdctl)\n      - moby (docker cli)\n  validations:\n    required: true\n- type: dropdown\n  attributes:\n    label: \"What operating system are you using?\"\n    options:\n      - macOS\n      - Windows\n      - Ubuntu\n      - Other Linux\n      - Other (specify below)\n  validations:\n    required: true\n- type: input\n  attributes:\n    label: Operating System / Build Version\n    description: \"What operating system and build version are you using?\"\n    placeholder: \"e.g. Windows 10 Home 1909, macOS Monterey 12.0.1, Ubuntu 20.04, etc...\"\n  validations:\n    required: true\n- type: dropdown\n  attributes:\n    label: What CPU architecture are you using?\n    options:\n      - x64\n      - ia32\n      - arm64 (Apple Silicon)\n  validations:\n    required: true\n- type: dropdown\n  attributes:\n    label: \"Linux only: what package format did you use to install Rancher Desktop?\"\n    options:\n      - N/A\n      - deb\n      - rpm\n      - AppImage\n      - Flatpak\n  validations:\n    required: false\n- type: textarea\n  attributes:\n    label: Windows User Only\n    description: \"Are you using VPN, Proxy, Special Firewall rules, Security Software or custom Activity directory features? if Yes, please describe.\"\n    placeholder: \"e.g. VPN PulseSecure, Kaspersky Total Security 21.2.x, custom proxy configs, activity directory features, or N/A\"\n  validations:\n    required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n- name: Ask a question (GitHub Discussions)\n  url: https://github.com/rancher-sandbox/rancher-desktop/discussions\n  about: We use GitHub Discussions for questions and GitHub issues for tracking bug reports and feature requests\n- name: Chat with Rancher Desktop users and developers\n  url: https://slack.rancher.io/\n  about: We hang out in the `#rancher-desktop` channel in the Rancher Users slack\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: \"Suggest a feature or idea to Rancher Desktop.\"\nlabels: [\"kind/enhancement\"]\nbody:\n- type: textarea\n  attributes:\n    label: Problem Description\n    description: \"A clear and concise description of what enhancement you'd like.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Proposed Solution\n    description: \"Describe the solution you'd like in a clear and concise manner.\"\n  validations:\n    required: true\n- type: textarea\n  attributes:\n    label: Additional Information\n    description: \"Add any other context/information about the problem here.\"\n  validations:\n    required: false\n"
  },
  {
    "path": ".github/actions/get-token/action.yaml",
    "content": "name: Get Token\ndescription: >-\n  This action attempts to get a token with the requested permissions; if this is\n  not running from the upstream repository, it attempts to get the token from a\n  secret.  Otherwise, it uses the vault actions.\n  This requires permissions set described in\n  https://github.com/rancher-eio/read-vault-secrets\ninputs:\n  token-secret:\n    description: Secret to fall back to\n    required: false\noutputs:\n  token:\n    description: The GitHub token retrieved\n    value: ${{ github.repository == 'rancher-sandbox/rancher-desktop' && steps.gen-token.outputs.token || steps.get-secret.outputs.token }}\nruns:\n  using: composite\n  steps:\n  - id: vault\n    name: Read vault secrets\n    if: github.repository == 'rancher-sandbox/rancher-desktop'\n    uses: rancher-eio/read-vault-secrets@main\n    with:\n      secrets: |\n        secret/data/github/repo/${{ github.repository }}/github/app-credentials appId | APP_ID ;\n        secret/data/github/repo/${{ github.repository }}/github/app-credentials privateKey | PRIVATE_KEY\n  - id: gen-token\n    name: Generate token\n    if: github.repository == 'rancher-sandbox/rancher-desktop'\n    uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1\n    with:\n      app-id: ${{ env.APP_ID }}\n      private-key: ${{ env.PRIVATE_KEY }}\n  - id: get-secret\n    name: Fetch secret.\n    if: github.repository != 'rancher-sandbox/rancher-desktop'\n    run: echo \"token=$SECRET\" >> \"$GITHUB_OUTPUT\"\n    shell: bash\n    env:\n      SECRET: ${{ inputs.token-secret }}\n"
  },
  {
    "path": ".github/actions/setup-environment/action.yaml",
    "content": "name: Setup Environment\ndescription: >-\n  This is a composite action that is used to set up the runner for running\n  Rancher Desktop.\ninputs:\n  user:\n    default: ''\n    description: >-\n      (Linux only) The user to use to set up `pass`\n\nruns:\n  using: composite\n  steps:\n  - name: \"Windows: Stop unwanted services\"\n    if: runner.os == 'Windows'\n    shell: pwsh\n    run: >-\n      Get-Service -ErrorAction Continue -Name\n      @('W3SVC', 'docker')\n      | Stop-Service\n\n  - name: \"Windows: Update any pre-installed WSL\"\n    if: runner.os == 'Windows'\n    shell: pwsh\n    run: |\n      # Sometimes this results in a HTTP 403 for some reason; in that case, we\n      # need to retry.\n      do {\n        wsl --update\n      } while ( -not $? )\n      # Setting the default version also lets WSL finish updating.\n      wsl --set-default-version 2\n\n  - name: \"Windows: Install yq\"\n    if: runner.os == 'Windows'\n    shell: bash\n    run: |\n      set -o xtrace\n      bindir=\"$HOME/bin\"\n      if [[ ! \"$PATH\" =~ \"$bindir\" ]]; then\n        bindir=/usr/bin\n      fi\n      if ! command -v yq; then\n        mkdir -p \"$bindir\"\n        curl --location --output \"$bindir/yq.exe\" \\\n          https://github.com/mikefarah/yq/releases/download/v4.43.1/yq_windows_amd64.exe\n        chmod a+x \"$bindir/yq.exe\"\n      fi\n\n  - name: \"Linux: Determine whether sudo is required\"\n    if: runner.os == 'Linux'\n    shell: bash\n    id: sudo\n    run: |\n      if [[ $(id --user) -eq 0 ]]; then\n        echo \"sudo=command\" >> \"$GITHUB_OUTPUT\"\n        # Fix for https://github.com/rocky-linux/sig-cloud-instance-images/issues/56\n        chmod u+r /etc/shadow\n      else\n        echo \"sudo=sudo\" >> \"$GITHUB_OUTPUT\"\n      fi\n\n  - name: \"Linux: Enable KVM access\"\n    if: runner.os == 'Linux'\n    shell: bash\n    run: ${{ steps.sudo.outputs.sudo }} chmod a+rwx /dev/kvm\n\n  - name: \"Linux: Set unprivileged port start to 80\"\n    if: runner.os == 'Linux'\n    shell: bash\n    run: >-\n      ${{ steps.sudo.outputs.sudo }}\n      sh -c\n      'echo 80 > /proc/sys/net/ipv4/ip_unprivileged_port_start'\n\n  - name: \"Linux: Install required packages\"\n    if: runner.os == 'Linux'\n    shell: bash\n    run: |\n      source /etc/os-release\n      for id in $ID $ID_LIKE; do\n        case $id in\n          suse|opensuse)\n            ${{ steps.sudo.outputs.sudo }} zypper --non-interactive install \\\n              fuse gawk git GraphicsMagick gtk3-tools jq mozilla-nss \\\n              noto-sans-fonts password-store sudo xvfb-run xauth which\n            if [[ ${GITHUB_JOB:-unknown} =~ appimage ]]; then\n              ${{ steps.sudo.outputs.sudo }} zypper --non-interactive install \\\n                libasound2 openssh-clients\n            fi\n            exit 0;;\n          rocky|rhel|centos|fedora)\n            if [[ \"$id\" != \"fedora\" ]]; then\n              ${{ steps.sudo.outputs.sudo }} dnf install --assumeyes \\\n                \"https://dl.fedoraproject.org/pub/epel/epel-release-latest-${VERSION_ID%%.*}.noarch.rpm\"\n              ${{ steps.sudo.outputs.sudo }} /usr/bin/crb enable # spellcheck-ignore-line\n            fi\n            ${{ steps.sudo.outputs.sudo }} dnf install --assumeyes \\\n              at-spi2-atk cups-libs git GraphicsMagick gtk3 jq \\\n              libva nss pass procps-ng sudo xorg-x11-server-Xvfb \\\n              /usr/bin/script \\\n              --setopt=excludepkgs=systemd-standalone-tmpfiles\n            exit 0;;\n          debian|ubuntu)\n            ${{ steps.sudo.outputs.sudo }} apt-get update\n            ${{ steps.sudo.outputs.sudo }} apt-get install --verbose-versions --yes \\\n              curl jq pass sudo xvfb\n            exit 0;;\n        esac\n      done\n      printf \"Could not find known distribution in [%s %s]\\n\" \"$ID\" \"$ID_LIKE\" >&2\n      exit 1\n\n  - name: \"Linux: Set up passwordless sudo\"\n    if: runner.os == 'Linux'\n    shell: bash\n    run: |\n      case \"$TARGET_USER\" in\n        \"\"|root)\n          exit 0;;\n      esac\n      echo \"$TARGET_USER ALL=(ALL) NOPASSWD: ALL\" > /etc/sudoers.d/$TARGET_USER\n    env:\n      TARGET_USER: ${{ inputs.user || 'root' }}\n\n  - name: \"Linux: Initialize pass\"\n    if: runner.os == 'Linux'\n    shell: >-\n      /usr/bin/sudo --user=${{ inputs.user || 'root' }}\n      --login --set-home --non-interactive bash {0}\n    run: |\n      # Configure the agent to allow default passwords\n      HOMEDIR=\"$(gpgconf --list-dirs homedir)\" # spellcheck-ignore-line\n      mkdir -p \"${HOMEDIR}\"\n      chmod 0700 \"${HOMEDIR}\"\n      echo \"allow-preset-passphrase\" >> \"${HOMEDIR}/gpg-agent.conf\"\n\n      # Create a GPG key\n      gpg --quick-generate-key --yes --batch --passphrase '' \\\n        user@rancher-desktop.test default \\\n        default never\n\n      # Get info about the newly created key\n      DATA=\"$(gpg --batch --with-colons --with-keygrip --list-secret-keys)\"\n      FINGERPRINT=\"$(awk -F: '/^fpr:/ { print $10 ; exit }' <<< \"${DATA}\")\" # spellcheck-ignore-line\n      GRIP=\"$(awk -F: '/^grp:/ { print $10 ; exit }' <<< \"${DATA}\")\"\n\n      # Save the password\n      gpg-connect-agent --verbose \"PRESET_PASSPHRASE ${GRIP} -1 00\" /bye\n\n      # Initialize pass\n      pass init \"${FINGERPRINT}\"\n"
  },
  {
    "path": ".github/actions/spelling/README.md",
    "content": "# check-spelling/check-spelling configuration\n\nFile | Purpose | Format | Info\n---- | ------- | ------ | ----\n[dictionary.txt](dictionary.txt) | Replacement dictionary (creating this file will override the default dictionary) | one word per line | [dictionary](https://github.com/check-spelling/check-spelling/wiki/Configuration#dictionary)\n[allow.txt](allow.txt) | Add words to the dictionary | one word per line (only letters and `'`s allowed) | [allow](https://github.com/check-spelling/check-spelling/wiki/Configuration#allow)\n[reject.txt](reject.txt) | Remove words from the dictionary (after allow) | grep pattern matching whole dictionary words | [reject](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-reject)\n[excludes.txt](excludes.txt) | Files to ignore entirely | perl regular expression | [excludes](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-excludes)\n[only.txt](only.txt) | Only check matching files (applied after excludes) | perl regular expression | [only](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-only)\n[patterns.txt](patterns.txt) | Patterns to ignore from checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)\n[candidate.patterns](candidate.patterns) | Patterns that might be worth adding to [patterns.txt](patterns.txt) | perl regular expression with optional comment block introductions (all matches will be suggested) | [candidates](https://github.com/check-spelling/check-spelling/wiki/Feature:-Suggest-patterns)\n[line_forbidden.patterns](line_forbidden.patterns) | Patterns to flag in checked lines | perl regular expression (order matters, first match wins) | [patterns](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-patterns)\n[expect.txt](expect.txt) | Expected words that aren't in the dictionary | one word per line (sorted, alphabetically) | [expect](https://github.com/check-spelling/check-spelling/wiki/Configuration#expect)\n[advice.md](advice.md) | Supplement for GitHub comment when unrecognized words are found | GitHub Markdown | [advice](https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice)\n\nNote: you can replace any of these files with a directory by the same name (minus the suffix)\nand then include multiple files inside that directory (with that suffix) to merge multiple files together.\n"
  },
  {
    "path": ".github/actions/spelling/advice.md",
    "content": "<!-- See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples%3A-advice --> <!-- markdownlint-disable MD033 MD041 -->\n<details><summary>If the flagged items are :exploding_head: false positives</summary>\n\nIf items relate to a ...\n* binary file (or some other file you wouldn't want to check at all).\n\n  Please add a file path to the `excludes.txt` file matching the containing file.\n\n  File paths are Perl 5 Regular Expressions - you can [test](\nhttps://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your files.\n\n  `^` refers to the file's path from the root of the repository, so `^README\\.md$` would exclude [README.md](\n../tree/HEAD/README.md) (on whichever branch you're using).\n\n* well-formed pattern.\n\n  If you can write a [pattern](\nhttps://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns\n) that would match it,\n  try adding it to the `patterns.txt` file.\n\n  Patterns are Perl 5 Regular Expressions - you can [test](\nhttps://www.regexplanet.com/advanced/perl/) yours before committing to verify it will match your lines.\n\n  Note that patterns can't match multiline strings.\n\n</details>\n\n<!-- adoption information-->\n:steam_locomotive: If you're seeing this message and your PR is from a branch that doesn't have check-spelling,\nplease merge to your PR's base branch to get the version configured for your repository.\n"
  },
  {
    "path": ".github/actions/spelling/allow.txt",
    "content": "emoji\ngithub\nhttps\npasswordless\nssh\nubuntu\nworkarounds\n"
  },
  {
    "path": ".github/actions/spelling/candidate.patterns",
    "content": "# Repeated letters\n\\b([A-Za-z])\\g{-1}{2,}\\b\n\n# marker to ignore all code on line\n^.*/\\* #no-spell-check-line \\*/.*$\n# marker to ignore all code on line\n^.*\\bno-spell-check(?:-line|)(?:\\s.*|)$\n\n# https://cspell.org/configuration/document-settings/\n# cspell inline\n^.*\\b[Cc][Ss][Pp][Ee][Ll]{2}:\\s*[Dd][Ii][Ss][Aa][Bb][Ll][Ee]-[Ll][Ii][Nn][Ee]\\b\n\n# copyright\nCopyright (?:\\([Cc]\\)|)(?:[-\\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+\n\n# patch hunk comments\n^@@ -\\d+(?:,\\d+|) \\+\\d+(?:,\\d+|) @@ .*\n# git index header\nindex (?:[0-9a-z]{7,40},|)[0-9a-z]{7,40}\\.\\.[0-9a-z]{7,40}\n\n# file permissions\n['\"`\\s](?!-+\\s)[-bcdLlpsw](?:[-r][-w][-Ssx]){2}[-r][-w][-SsTtx]\\+?['\"`\\s]\n\n# css fonts\n\\bfont(?:-family(?:[-\\w+]*)|):[^;}]+\n\n# css url wrappings\n\\burl\\([^)]+\\)\n\n# cid urls\n(['\"])cid:.*?\\g{-1}\n\n# data url in parens\n\\(data:(?:[^) ][^)]*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})[^)]*\\)\n# data url in quotes\n([`'\"])data:(?:[^ `'\"].*?|)(?:[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,}).*\\g{-1}\n# data url\n\\bdata:[-a-zA-Z=;:/0-9+_]*,\\S*\n\n# https/http/file urls\n(?:\\b(?:https?|ftp|file)://)[-A-Za-z0-9+&@#/*%?=~_|!:,.;]+[-A-Za-z0-9+&@#/*%=~_|]\n\n# mailto urls\nmailto:[-a-zA-Z=;:/?%&0-9+@._]{3,}\n\n# magnet urls\nmagnet:[?=:\\w]+\n\n# magnet urls\n\"magnet:[^\"]+\"\n\n# obs:\n\"obs:[^\"]*\"\n\n# The `\\b` here means a break, it's the fancy way to handle urls, but it makes things harder to read\n# In this examples content, I'm using a number of different ways to match things to show various approaches\n# asciinema\n\\basciinema\\.org/a/[0-9a-zA-Z]+\n\n# asciinema v2\n^\\[\\d+\\.\\d+, \"[io]\", \".*\"\\]$\n\n# apple\n\\bdeveloper\\.apple\\.com/[-\\w?=/]+\n# Apple music\n\\bembed\\.music\\.apple\\.com/fr/playlist/usr-share/[-\\w.]+\n\n# appveyor api\n\\bci\\.appveyor\\.com/api/projects/status/[0-9a-z]+\n# appveyor project\n\\bci\\.appveyor\\.com/project/(?:[^/\\s\"]*/){2}builds?/\\d+/job/[0-9a-z]+\n\n# Amazon\n\n# Amazon\n\\bamazon\\.com/[-\\w]+/(?:dp/[0-9A-Z]+|)\n# AWS ARN\narn:aws:[-/:\\w]+\n# AWS S3\n\\b\\w*\\.s3[^.]*\\.amazonaws\\.com/[-\\w/&#%_?:=]*\n# AWS execute-api\n\\b[0-9a-z]{10}\\.execute-api\\.[-0-9a-z]+\\.amazonaws\\.com\\b\n# AWS ELB\n\\b\\w+\\.[-0-9a-z]+\\.elb\\.amazonaws\\.com\\b\n# AWS SNS\n\\bsns\\.[-0-9a-z]+.amazonaws\\.com/[-\\w/&#%_?:=]*\n# AWS VPC\nvpc-\\w+\n\n# While you could try to match `http://` and `https://` by using `s?` in `https?://`, sometimes there\n# YouTube url\n\\b(?:(?:www\\.|)youtube\\.com|youtu.be)/(?:channel/|embed/|user/|playlist\\?list=|watch\\?v=|v/|)[-a-zA-Z0-9?&=_%]*\n# YouTube music\n\\bmusic\\.youtube\\.com/youtubei/v1/browse(?:[?&]\\w+=[-a-zA-Z0-9?&=_]*)\n# YouTube tag\n<\\s*youtube\\s+id=['\"][-a-zA-Z0-9?_]*['\"]\n# YouTube image\n\\bimg\\.youtube\\.com/vi/[-a-zA-Z0-9?&=_]*\n# Google Accounts\n\\baccounts.google.com/[-_/?=.:;+%&0-9a-zA-Z]*\n# Google Analytics\n\\bgoogle-analytics\\.com/collect.[-0-9a-zA-Z?%=&_.~]*\n# Google APIs\n\\bgoogleapis\\.(?:com|dev)/[a-z]+/(?:v\\d+/|)[a-z]+/[-@:./?=\\w+|&]+\n# Google Artifact Registry\n\\.pkg\\.dev(?:/[-\\w]+)+(?::[-\\w]+|)\n# Google Storage\n\\b[-a-zA-Z0-9.]*\\bstorage\\d*\\.googleapis\\.com(?:/\\S*|)\n# Google Calendar\n\\bcalendar\\.google\\.com/calendar(?:/u/\\d+|)/embed\\?src=[@./?=\\w&%]+\n\\w+\\@group\\.calendar\\.google\\.com\\b\n# Google DataStudio\n\\bdatastudio\\.google\\.com/(?:(?:c/|)u/\\d+/|)(?:embed/|)(?:open|reporting|datasources|s)/[-0-9a-zA-Z]+(?:/page/[-0-9a-zA-Z]+|)\n# The leading `/` here is as opposed to the `\\b` above\n# ... a short way to match `https://` or `http://` since most urls have one of those prefixes\n# Google Docs\n/docs\\.google\\.com/[a-z]+/(?:ccc\\?key=\\w+|(?:u/\\d+|d/(?:e/|)[0-9a-zA-Z_-]+/)?(?:edit\\?[-\\w=#.]*|/\\?[\\w=&]*|))\n# Google Drive\n\\bdrive\\.google\\.com/(?:file/d/|open)[-0-9a-zA-Z_?=]*\n# Google Groups\n\\bgroups\\.google\\.com(?:/[a-z]+/(?:#!|)[^/\\s\"]+)*\n# Google Maps\n\\bmaps\\.google\\.com/maps\\?[\\w&;=]*\n# Google themes\nthemes\\.googleusercontent\\.com/static/fonts/[^/\\s\"]+/v\\d+/[^.]+.\n# Google CDN\n\\bclients2\\.google(?:usercontent|)\\.com[-0-9a-zA-Z/.]*\n# Goo.gl\n/goo\\.gl/[a-zA-Z0-9]+\n# Google Chrome Store\n\\bchrome\\.google\\.com/webstore/detail/[-\\w]*(?:/\\w*|)\n# Google Books\n\\bgoogle\\.(?:\\w{2,4})/books(?:/\\w+)*\\?[-\\w\\d=&#.]*\n# Google Fonts\n\\bfonts\\.(?:googleapis|gstatic)\\.com/[-/?=:;+&0-9a-zA-Z]*\n# Google Forms\n\\bforms\\.gle/\\w+\n# Google Scholar\n\\bscholar\\.google\\.com/citations\\?user=[A-Za-z0-9_]+\n# Google Colab Research Drive\n\\bcolab\\.research\\.google\\.com/drive/[-0-9a-zA-Z_?=]*\n# Google Cloud regions\n(?:us|(?:north|south)america|europe|asia|australia|me|africa)-(?:north|south|east|west|central){1,2}\\d+\n\n# GitHub SHAs (api)\n\\bapi.github\\.com/repos(?:/[^/\\s\"]+){3}/[0-9a-f]+\\b\n# GitHub SHAs (markdown)\n(?:\\[`?[0-9a-f]+`?\\]\\(https:/|)/(?:www\\.|)github\\.com(?:/[^/\\s\"]+){2,}(?:/[^/\\s\")]+)(?:[0-9a-f]+(?:[-0-9a-zA-Z/#.]*|)\\b|)\n# GitHub SHAs\n\\bgithub\\.com(?:/[^/\\s\"]+){2}[@#][0-9a-f]+\\b\n# GitHub SHA refs\n\\[([0-9a-f]+)\\]\\(https://(?:www\\.|)github.com/[-\\w]+/[-\\w]+/commit/\\g{-1}[0-9a-f]*\n# GitHub wiki\n\\bgithub\\.com/(?:[^/]+/){2}wiki/(?:(?:[^/]+/|)_history|[^/]+(?:/_compare|)/[0-9a-f.]{40,})\\b\n# githubusercontent\n/[-a-z0-9]+\\.githubusercontent\\.com/[-a-zA-Z0-9?&=_\\/.]*\n# githubassets\n\\bgithubassets.com/[0-9a-f]+(?:[-/\\w.]+)\n# gist github\n\\bgist\\.github\\.com/[^/\\s\"]+/[0-9a-f]+\n# git.io\n\\bgit\\.io/[0-9a-zA-Z]+\n# GitHub JSON\n\"node_id\": \"[-a-zA-Z=;:/0-9+_]*\"\n# Contributor\n\\[[^\\]]+\\]\\(https://github\\.com/[^/\\s\"]+/?\\)\n# GHSA\nGHSA(?:-[0-9a-z]{4}){3}\n\n# GitHub actions\n\\buses:\\s+(['\"]?)[-\\w.]+/[-\\w./]+@[-\\w.]+\\g{-1}\n\n# GitLab commit\n\\bgitlab\\.[^/\\s\"]*/\\S+/\\S+/commit/[0-9a-f]{7,16}#[0-9a-f]{40}\\b\n# GitLab merge requests\n\\bgitlab\\.[^/\\s\"]*/\\S+/\\S+/-/merge_requests/\\d+/diffs#[0-9a-f]{40}\\b\n# GitLab uploads\n\\bgitlab\\.[^/\\s\"]*/uploads/[-a-zA-Z=;:/0-9+]*\n# GitLab commits\n\\bgitlab\\.[^/\\s\"]*/(?:[^/\\s\"]+/){2}commits?/[0-9a-f]+\\b\n\n# #includes\n^\\s*#include\\s*(?:<.*?>|\".*?\")\n\n# #pragma lib\n^\\s*#pragma comment\\(lib, \".*?\"\\)\n\n# binance\naccounts\\.binance\\.com/[a-z/]*oauth/authorize\\?[-0-9a-zA-Z&%]*\n\n# bitbucket diff\n\\bapi\\.bitbucket\\.org/\\d+\\.\\d+/repositories/(?:[^/\\s\"]+/){2}diff(?:stat|)(?:/[^/\\s\"]+){2}:[0-9a-f]+\n# bitbucket repositories commits\n\\bapi\\.bitbucket\\.org/\\d+\\.\\d+/repositories/(?:[^/\\s\"]+/){2}commits?/[0-9a-f]+\n# bitbucket commits\n\\bbitbucket\\.org/(?:[^/\\s\"]+/){2}commits?/[0-9a-f]+\n\n# bit.ly\n\\bbit\\.ly/\\w+\n\n# bitrise\n\\bapp\\.bitrise\\.io/app/[0-9a-f]*/[\\w.?=&]*\n\n# bootstrapcdn.com\n\\bbootstrapcdn\\.com/[-./\\w]+\n\n# cdn.cloudflare.com\n\\bcdnjs\\.cloudflare\\.com/[./\\w]+\n\n# circleci\n\\bcircleci\\.com/gh(?:/[^/\\s\"]+){1,5}.[a-z]+\\?[-0-9a-zA-Z=&]+\n\n# gitter\n\\bgitter\\.im(?:/[^/\\s\"]+){2}\\?at=[0-9a-f]+\n\n# gravatar\n\\bgravatar\\.com/avatar/[0-9a-f]+\n\n# ibm\n[a-z.]*ibm\\.com/[-_#=:%!?~.\\\\/\\d\\w]*\n\n# imgur\n\\bimgur\\.com/[^.]+\n\n# Internet Archive\n\\barchive\\.org/web/\\d+/(?:[-\\w.?,'/\\\\+&%$#_:]*)\n\n# discord\n/discord(?:app\\.com|\\.gg)/(?:invite/)?[a-zA-Z0-9]{7,}\n\n# Disqus\n\\bdisqus\\.com/[-\\w/%.()!?&=_]*\n\n# medium link\n\\blink\\.medium\\.com/[a-zA-Z0-9]+\n# medium\n\\bmedium\\.com/@?[^/\\s\"]+/[-\\w]+\n\n# microsoft\n\\b(?:https?://|)(?:(?:(?:blogs|download\\.visualstudio|docs|msdn2?|research)\\.|)microsoft|blogs\\.msdn)\\.co(?:m|\\.\\w\\w)/[-_a-zA-Z0-9()=./%]*\n# powerbi\n\\bapp\\.powerbi\\.com/reportEmbed/[^\"' ]*\n# vs devops\n\\bvisualstudio.com(?::443|)/[-\\w/?=%&.]*\n# microsoft store\n\\bmicrosoft\\.com/store/apps/\\w+\n\n# mvnrepository.com\n\\bmvnrepository\\.com/[-0-9a-z./]+\n\n# now.sh\n/[0-9a-z-.]+\\.now\\.sh\\b\n\n# oracle\n\\bdocs\\.oracle\\.com/[-0-9a-zA-Z./_?#&=]*\n\n# chromatic.com\n/\\S+.chromatic.com\\S*[\")]\n\n# codacy\n\\bapi\\.codacy\\.com/project/badge/Grade/[0-9a-f]+\n\n# compai\n\\bcompai\\.pub/v1/png/[0-9a-f]+\n\n# mailgun api\n\\.api\\.mailgun\\.net/v3/domains/[0-9a-z]+\\.mailgun.org/messages/[0-9a-zA-Z=@]*\n# mailgun\n\\b[0-9a-z]+.mailgun.org\n\n# /message-id/\n/message-id/[-\\w@./%]+\n\n# Reddit\n\\breddit\\.com/r/[/\\w_]*\n\n# requestb.in\n\\brequestb\\.in/[0-9a-z]+\n\n# sched\n\\b[a-z0-9]+\\.sched\\.com\\b\n\n# Slack url\nslack://[a-zA-Z0-9?&=]+\n# Slack\n\\bslack\\.com/[-0-9a-zA-Z/_~?&=.]*\n# Slack edge\n\\bslack-edge\\.com/[-a-zA-Z0-9?&=%./]+\n# Slack images\n\\bslack-imgs\\.com/[-a-zA-Z0-9?&=%.]+\n\n# shields.io\n\\bshields\\.io/[-\\w/%?=&.:+;,]*\n\n# stackexchange -- https://stackexchange.com/feeds/sites\n\\b(?:askubuntu|serverfault|stack(?:exchange|overflow)|superuser).com/(?:questions/\\w+/[-\\w]+|a/)\n\n# Sentry\n[0-9a-f]{32}\\@o\\d+\\.ingest\\.sentry\\.io\\b\n\n# Twitter markdown\n\\[@[^[/\\]:]*?\\]\\(https://twitter.com/[^/\\s\"')]*(?:/status/\\d+(?:\\?[-_0-9a-zA-Z&=]*|)|)\\)\n# Twitter hashtag\n\\btwitter\\.com/hashtag/[\\w?_=&]*\n# Twitter status\n\\btwitter\\.com/[^/\\s\"')]*(?:/status/\\d+(?:\\?[-_0-9a-zA-Z&=]*|)|)\n# Twitter profile images\n\\btwimg\\.com/profile_images/[_\\w./]*\n# Twitter media\n\\btwimg\\.com/media/[-_\\w./?=]*\n# Twitter link shortened\n\\bt\\.co/\\w+\n\n# facebook\n\\bfburl\\.com/[0-9a-z_]+\n# facebook CDN\n\\bfbcdn\\.net/[\\w/.,]*\n# facebook watch\n\\bfb\\.watch/[0-9A-Za-z]+\n\n# dropbox\n\\bdropbox\\.com/sh?/[^/\\s\"]+/[-0-9A-Za-z_.%?=&;]+\n\n# ipfs protocol\nipfs://[0-9a-zA-Z]{3,}\n# ipfs url\n/ipfs/[0-9a-zA-Z]{3,}\n\n# w3\n\\bw3\\.org/[-0-9a-zA-Z/#.]+\n\n# loom\n\\bloom\\.com/embed/[0-9a-f]+\n\n# regex101\n\\bregex101\\.com/r/[^/\\s\"]+/\\d+\n\n# figma\n\\bfigma\\.com/file(?:/[0-9a-zA-Z]+/)+\n\n# freecodecamp.org\n\\bfreecodecamp\\.org/[-\\w/.]+\n\n# image.tmdb.org\n\\bimage\\.tmdb\\.org/[/\\w.]+\n\n# mermaid\n\\bmermaid\\.ink/img/[-\\w]+|\\bmermaid-js\\.github\\.io/mermaid-live-editor/#/edit/[-\\w]+\n\n# Wikipedia\n\\ben\\.wikipedia\\.org/wiki/[-\\w%.#]+\n\n# gitweb\n[^\"\\s]+/gitweb/\\S+;h=[0-9a-f]+\n\n# HyperKitty lists\n/archives/list/[^@/]+@[^/\\s\"]*/message/[^/\\s\"]*/\n\n# lists\n/thread\\.html/[^\"\\s]+\n\n# list-management\n\\blist-manage\\.com/subscribe(?:[?&](?:u|id)=[0-9a-f]+)+\n\n# kubectl.kubernetes.io/last-applied-configuration\n\"kubectl.kubernetes.io/last-applied-configuration\": \".*\"\n\n# pgp\n\\bgnupg\\.net/pks/lookup[?&=0-9a-zA-Z]*\n\n# Spotify\n\\bopen\\.spotify\\.com/embed/playlist/\\w+\n\n# Mastodon\n\\bmastodon\\.[-a-z.]*/(?:media/|@)[?&=0-9a-zA-Z_]*\n\n# scastie\n\\bscastie\\.scala-lang\\.org/[^/]+/\\w+\n\n# images.unsplash.com\n\\bimages\\.unsplash\\.com/(?:(?:flagged|reserve)/|)[-\\w./%?=%&.;]+\n\n# pastebin\n\\bpastebin\\.com/[\\w/]+\n\n# heroku\n\\b\\w+\\.heroku\\.com/source/archive/\\w+\n\n# quip\n\\b\\w+\\.quip\\.com/\\w+(?:(?:#|/issues/)\\w+)?\n\n# badgen.net\n\\bbadgen\\.net/badge/[^\")\\]'\\s]+\n\n# statuspage.io\n\\w+\\.statuspage\\.io\\b\n\n# media.giphy.com\n\\bmedia\\.giphy\\.com/media/[^/]+/[\\w.?&=]+\n\n# tinyurl\n\\btinyurl\\.com/\\w+\n\n# codepen\n\\bcodepen\\.io/[\\w/]+\n\n# registry.npmjs.org\n\\bregistry\\.npmjs\\.org/(?:@[^/\"']+/|)[^/\"']+/-/[-\\w@.]+\n\n# getopts\n\\bgetopts\\s+(?:\"[^\"]+\"|'[^']+')\n\n# ANSI color codes\n(?:\\\\(?:u00|x)1[Bb]|\\\\03[1-7]|\\x1b|\\\\u\\{1[Bb]\\})\\[\\d+(?:;\\d+)*m\n\n# URL escaped characters\n%[0-9A-F][A-F](?=[A-Za-z])\n# lower URL escaped characters\n#%[0-9a-f][a-f](?=[a-z]{2,})\n# IPv6\n\\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\\b\n# c99 hex digits (not the full format, just one I've seen)\n0x[0-9a-fA-F](?:\\.[0-9a-fA-F]*|)[pP]\n# Punycode\n\\bxn--[-0-9a-z]+\n# sha\nsha\\d+:[0-9a-f]*?[a-f]{3,}[0-9a-f]*\n# sha-... -- uses a fancy capture\n(\\\\?['\"]|&quot;)[0-9a-f]{40,}\\g{-1}\n# hex runs\n\\b(?=(?:[a-fA-F]{0,2}\\d)*[a-fA-F]{3})[0-9a-fA-F]{16,}\\b\n# hex in url queries\n=[0-9a-fA-F]*?(?:[A-F]{3,}|[a-f]{3,})[0-9a-fA-F]*?&\n# ssh\n(?:ssh-\\S+|-nistp256) [-a-zA-Z=;:/0-9+]{12,}\n\n# PGP\n\\b(?:[0-9A-F]{4} ){9}[0-9A-F]{4}\\b\n# GPG keys\n\\b(?:[0-9A-F]{4} ){5}(?: [0-9A-F]{4}){5}\\b\n# Well known gpg keys\n.well-known/openpgpkey/[\\w./]+\n\n# pki\n-----BEGIN.*-----END\n\n# pki (base64)\nLS0tLS1CRUdJT.*\n\n# C# includes\n^\\s*using [^;]+;\n\n# uuid:\n\\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\\b\n# hex digits including css/html color classes:\n(?:[\\\\0][xX]|\\\\u\\{?|[uU]\\+|#x?|%23|&H)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\\d+)\\b\n\n# integrity\nintegrity=(['\"])(?:\\s*sha\\d+-[-a-zA-Z=;:/0-9+]{40,})+\\g{-1}\n\n# https://www.gnu.org/software/groff/manual/groff.html\n# man troff content\n\\\\f[BCIPR]\n# '/\"\n\\\\\\([ad]q\n\n# .desktop mime types\n^MimeTypes?=.*$\n# .desktop localized entries\n^[A-Z][a-z]+\\[[a-z]+\\]=.*$\n# Localized .desktop content\nName\\[[^\\]]+\\]=.*\n\n# IServiceProvider / isAThing\n#(?:(?:\\b|_|(?<=[a-z]))I|(?:\\b|_)(?:nsI|isA))(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\\d]|\\b))\n\n# python\n\\b(?i)py(?!gments|gmy|lon|ramid|ro|th)(?=[a-z]{2,})\n\n# crypt\n(['\"])\\$2[ayb]\\$.{56}\\g{-1}\n\n# apache/old crypt\n(['\"]|)\\$+(?:apr|)1\\$+.{8}\\$+.{22}\\g{-1}\n\n# sha1 hash\n\\{SHA\\}[-a-zA-Z=;:/0-9+]{3,}\n\n# machine learning (?)\n\\b(?i)ml(?=[a-z]{2,})\n\n# scrypt / argon\n\\$(?:scrypt|argon\\d+[di]*)\\$\\S+\n\n# go.sum\n\\bh1:\\S+\n\n# golang print-f-style functions\n(?i)(?<=append|comma|debug|equal|err|error|exit|fatal|format|info|log|name|panic|print|skip|scan|string|trace|true|warn|warning|wrap|write)(?:f|ln)[ (]\n\n# golang regular expression\n(?<!\")\\br\".+?\"\n\n# imports\n^import\\s+(?:(?:static|type)\\s+|)(?:[\\w.]|\\{\\s*\\w*?(?:,\\s*(?:\\w*|\\*))+\\s*\\})+(?:\\s+from (['\"]).*?\\g{-1}|)\n\n# scala modules\n(\"[^\"]+\"\\s*%%?\\s*){2,3}\"[^\"]+\"\n\n# Dataframes / NumPy\n\\b(?:df|np)\\.\\w{3,}\n\n# container images\nimage: [-\\w./:@]+\n\n# Docker images\n^\\s*(?i)FROM\\s+\\S+:\\S+(?:\\s+AS\\s+\\S+|)\n\n# `docker images` REPOSITORY TAG IMAGE ID CREATED SIZE\n\\s*\\S+/\\S+\\s+\\S+\\s+[0-9a-f]{8,}\\s+\\d+\\s+(?:hour|day|week)s ago\\s+[\\d.]+[KMGT]B\n\n# Intel intrinsics\n_mm\\d*_(?!dd)\\w+\n\n# Input to GitHub JSON\ncontent: (['\"])[-a-zA-Z=;:/0-9+]*=\\g{-1}\n\n# This does not cover multiline strings, if your repository has them,\n# you'll want to remove the `(?=.*?\")` suffix.\n# The `(?=.*?\")` suffix should limit the false positives rate\n# printf\n%(?:(?:(?:hh?|ll?|[jzt])?[diuoxn]|l?[cs]|L?[fega]|p)(?=[a-z]{2,})|(?:X|L?[FEGA])(?=[a-zA-Z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%)\\b)(?=.*?['\"])\n\n# Alternative printf\n# %s\n%(?:s(?=[a-z]{2,}))(?!%)(?=[_a-zA-Z]+(?!%[^s])\\b)(?=.*?['\"])\n\n# Python string prefix / binary prefix\n# Note that there's a high false positive rate, remove the `?=` and search for the regex to see if the matches seem like reasonable strings\n(?<!['\"])\\b(?:B|BR|Br|F|FR|Fr|R|RB|RF|Rb|Rf|U|UR|Ur|b|bR|br|f|fR|fr|r|rB|rF|rb|rf|u|uR|ur)['\"](?=[A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\n\n# Regular expression for word breaks\n#\\\\b(?=[a-z]{2})\n\n# Regular expressions for (P|p)assword\n\\([A-Z]\\|[a-z]\\)[a-z]+\n\n# JavaScript regular expressions\n# javascript exec/test regex\n/.{3,}?/[gim]*\\.(?:exec|test)\\(\n# javascript match regex\n\\.match\\(/[^/\\s\"]{3,}/[gim]*\\s*\n# javascript match regex\n\\.match\\(/\\\\[b].{3,}?/[gim]*\\s*\\)(?:;|$)\n# javascript regex\n^\\s*/\\\\[b].{3,}?/[gim]*\\s*(?:\\)(?:;|$)|,$)\n# javascript replace regex\n\\.replace\\(/[^/\\s\"]{3,}/[gim]*\\s*,\n# assign regex\n= /[^*].*?(?:[a-z]{3,}|[A-Z]{3,}|[A-Z][a-z]{2,}).*/[gim]*(?=\\W|$)\n# perl regex test\n[!=]~ (?:/.*/|m\\{.*?\\}|m<.*?>|m([|!/@#,;']).*?\\g{-1})\n\n# perl qr regex\n(?<!\\$)\\bqr(?:\\{.*?\\}|<.*?>|\\(.*?\\)|([|!/@#,;']).*?\\g{-1})\n\n# perl run\nperl(?:\\s+-[a-zA-Z]\\w*)+\n\n# C network byte conversions\n(?:\\d|\\bh)to(?!ken)(?=[a-z])|to(?=[adhiklpun]\\()\n\n# Go regular expressions\nregexp?\\.MustCompile\\((?:`[^`]*`|\".*\"|'.*')\\)\n\n# regex choice\n\\((?:\\?:|)[^)|]+(?<! )\\|(?!(?:jq|xargs)\\b)[^)| ][^)]*\\)\n\n# proto\n^\\s*(\\w+)\\s\\g{-1} =\n\n# sed regular expressions\nsed 's/(?:[^/]*?[a-zA-Z]{3,}[^/]*?/){2}\n\n# node packages\n([\"'])@[^/'\" ]+/[^/'\" ]+\\g{-1}\n\n# go install\ngo install(?:\\s+[a-z]+\\.[-@\\w/.]+)+\n\n# pom.xml\n<(?:group|artifact)Id>.*?<\n\n# jetbrains schema https://youtrack.jetbrains.com/issue/RSRP-489571\nurn:shemas-jetbrains-com\n\n# Debian changelog severity\n[-\\w]+ \\(.*\\) (?:\\w+|baseline|unstable|experimental); urgency=(?:low|medium|high|emergency|critical)\\b\n\n# kubernetes pod status lists\n# https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-phase\n\\w+(?:-\\w+)+\\s+\\d+/\\d+\\s+(?:Running|Pending|Succeeded|Failed|Unknown)\\s+\n\n# kubectl - pods in CrashLoopBackOff\n\\w+-[0-9a-f]+-\\w+\\s+\\d+/\\d+\\s+CrashLoopBackOff\\s+\n\n# kubernetes applications\n\\.apps/[-\\w]+\n\n# kubernetes object suffix\n-[0-9a-f]{10}-\\w{5}\\s\n\n# kubernetes crd patterns\n^\\s*pattern: .*$\n\n# posthog secrets\n([`'\"])phc_[^\"',]+\\g{-1}\n\n# xcode\n\n# xcodeproject scenes\n(?:Controller|destination|(?:first|second)Item|ID|id)=\"\\w{3}-\\w{2}-\\w{3}\"\n\n# xcode api botches\ncustomObjectInstantitationMethod\n\n# msvc api botches\nPrependWithABINamepsace\n\n# configure flags\n.* \\| --\\w{2,}.*?(?=\\w+\\s\\w+)\n\n# font awesome classes\n\\.fa-[-a-z0-9]+\n\n# bearer auth\n(['\"])[Bb]ear[e][r] .{3,}?\\g{-1}\n\n# bearer auth\n\\b[Bb]ear[e][r]:? [-a-zA-Z=;:/0-9+.]{3,}\n\n# basic auth\n(['\"])[Bb]asic [-a-zA-Z=;:/0-9+]{3,}\\g{-1}\n\n# basic auth\n: [Bb]asic [-a-zA-Z=;:/0-9+.]{3,}\n\n# base64 encoded content\n#([`'\"])[-a-zA-Z=;:/0-9+]{3,}=\\g{-1}\n# base64 encoded content in xml/sgml\n>[-a-zA-Z=;:/0-9+]{3,}=</\n# base64 encoded content, possibly wrapped in mime\n#(?:^|[\\s=;:?])[-a-zA-Z=;:/0-9+]{50,}(?:[\\s=;:?]|$)\n# base64 encoded json\n\\beyJ[-a-zA-Z=;:/0-9+]+\n# base64 encoded pkcs\n\\bMII[-a-zA-Z=;:/0-9+]+\n\n# uuencoded\n#[!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_]{40,}\n\n# DNS rr data\n(?:\\d+\\s+){3}(?:[-+/=.\\w]{2,}\\s*){1,2}\n\n# encoded-word\n=\\?[-a-zA-Z0-9\"*%]+\\?[BQ]\\?[^?]{0,75}\\?=\n\n# numerator\n\\bnumer\\b(?=.*denom)\n\n# Time Zones\n\\b(?:Africa|Atlantic|America|Antarctica|Arctic|Asia|Australia|Europe|Indian|Pacific)(?:/[-\\w]+)+\n\n# linux kernel info\n^(?:bugs|flags|Features)\\s+:.*\n\n# systemd mode\nsystemd.*?running in system mode \\([-+].*\\)$\n\n# Lorem\n# Update Lorem based on your content (requires `ge` and `w` from https://github.com/jsoref/spelling; and `review` from https://github.com/check-spelling/check-spelling/wiki/Looking-for-items-locally )\n# grep '^[^#].*lorem' .github/actions/spelling/patterns.txt|perl -pne 's/.*i..\\?://;s/\\).*//' |tr '|' \"\\n\"|sort -f |xargs -n1 ge|perl -pne 's/^[^:]*://'|sort -u|w|sed -e 's/ .*//'|w|review -\n# Warning, while `(?i)` is very neat and fancy, if you have some binary files that aren't proper unicode, you might run into:\n# ... Operation \"substitution (s///)\" returns its argument for non-Unicode code point 0x1C19AE (the code point will vary).\n# ... You could manually change `(?i)X...` to use `[Xx]...`\n# ... or you could add the files to your `excludes` file (a version after 0.0.19 should identify the file path)\n(?:(?:\\w|\\s|[,.])*\\b(?i)(?:amet|consectetur|cursus|dolor|eros|ipsum|lacus|libero|ligula|lorem|magna|neque|nulla|suscipit|tempus)\\b(?:\\w|\\s|[,.])*)\n\n# Non-English\n# Even repositories expecting pure English content can unintentionally have Non-English content... People will occasionally mistakenly enter [homoglyphs](https://en.wikipedia.org/wiki/Homoglyph) which are essentially typos, and using this pattern will mean check-spelling will not complain about them.\n# .\n# If the content to be checked should be written in English and the only Non-English items will be people's names, then you can consider adding this.\n# .\n# Alternatively, if you're using check-spelling v0.0.25+, and you would like to _check_ the Non-English content for spelling errors, you can. For information on how to do so, see:\n# https://docs.check-spelling.dev/Feature:-Configurable-word-characters.html#unicode\n[a-zA-Z]*[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3}[a-zA-ZÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]*|[a-zA-Z]{3,}[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź]|[ÀÁÂÃÄÅÆČÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæčçèéêëìíîïðñòóôõöøùúûüýÿĀāŁłŃńŅņŒœŚśŠšŜŝŸŽžź][a-zA-Z]{3,}\n\n# highlighted letters\n\\[[A-Z]\\][a-z]+\n\n# French\n# This corpus only had capital letters, but you probably want lowercase ones as well.\n\\b[LN]'+[a-z]{2,}\\b\n\n# latex (check-spelling >= 0.0.22)\n\\\\\\w{2,}\\{\n\n# American Mathematical Society (AMS) / Doxygen\nTeX/AMS\n\n# File extensions\n\\*\\.[+\\w]+,\n\n# eslint\n\"varsIgnorePattern\": \".+\"\n\n# nolint\nnolint:\\s*[\\w,]+\n\n# Windows short paths\n[/\\\\][^/\\\\]{5,6}~\\d{1,2}(?=[/\\\\])\n\n# Windows Resources with accelerators\n\\b[A-Z]&[a-z]+\\b(?!;)\n\n# signed off by\n(?i)Signed-off-by: .*\n\n# cygwin paths\n/cygdrive/[a-zA-Z]/(?:Program Files(?: \\(.*?\\)| ?)(?:/[-+.~\\\\/()\\w ]+)*|[-+.~\\\\/()\\w])+\n\n# in check-spelling@v0.0.22+, printf markers aren't automatically consumed\n# printf markers\n#(?<!\\\\)\\\\[nrt](?=[a-z]{2,})\n# alternate printf markers if you run into latex and friends\n#(?<!\\\\)\\\\[nrt](?=[a-z]{2,})(?=.*['\"`])\n\n# Markdown anchor links\n\\(#\\S*?[a-zA-Z]\\S*?\\)\n\n# apache\na2(?:en|dis)\n\n# weak e-tag\nW/\"[^\"]+\"\n\n# authors/credits\n^\\*(?: [A-Z](?:\\w+|\\.)){2,} (?=\\[|$)\n\n# the negative lookahead here is to allow catching 'templatesz' as a misspelling\n# but to otherwise recognize a Windows path with \\templates\\foo.template or similar:\n\\\\(?:necessary|r(?:elease|eport|esolve[dr]?|esult)|t(?:arget|emplates?))(?![a-z])\n# ignore long runs of a single character:\n\\b([A-Za-z])\\g{-1}{3,}\\b\n\n# version suffix <word>v#\n(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\\d+(?:\\b|(?=[a-zA-Z_]))\n\n# Compiler flags (Unix, Java/Scala)\n# Use if you have things like `-Pdocker` and want to treat them as `docker`\n#(?:^|[\\t ,>\"'`=(#])-(?:(?:J-|)[DPWXY]|[Llf])(?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,})\n\n# Compiler flags (Windows / PowerShell)\n# This is a subset of the more general compiler flags pattern.\n# It avoids matching `-Path` to prevent it from being treated as `ath`\n#(?:^|[\\t ,\"'`=(#])-(?:[DPL](?=[A-Z]{2,})|[WXYlf](?=[A-Z]{2,}|[A-Z][a-z]|[a-z]{2,}))\n\n# Compiler flags (linker)\n,-B\n\n# Library prefix\n# e.g., `lib`+`archive`, `lib`+`raw`, `lib`+`unwind`\n# (ignores some words that happen to start with `lib`)\n(?:\\b|_)[Ll]ib(?!era[lt])(?:re(?=office)|era|)(?!ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])\n\n# iSCSI iqn (approximate regex)\n\\biqn\\.[0-9]{4}-[0-9]{2}(?:[\\.-][a-z][a-z0-9]*)*\\b\n\n# WWNN/WWPN (NAA identifiers)\n\\b(?:0x)?10[0-9a-f]{14}\\b|\\b(?:0x|3)?[25][0-9a-f]{15}\\b|\\b(?:0x|3)?6[0-9a-f]{31}\\b\n\n# curl arguments\n\\b(?:\\\\n|)curl(?:\\.exe|)(?:\\s+-[a-zA-Z]{1,2}\\b)*(?:\\s+-[a-zA-Z]{3,})(?:\\s+-[a-zA-Z]+)*\n# set arguments\n\\b(?:bash|sh|set)(?:\\s+[-+][abefimouxE]{1,2})*\\s+[-+][abefimouxE]{3,}(?:\\s+[-+][abefimouxE]+)*\n# tar arguments\n\\b(?:\\\\n|)g?tar(?:\\.exe|)(?:\\s-C \\S+|(?:\\s+--[-a-zA-Z]+|\\s+-[a-zA-Z]+|\\s[ABGJMOPRSUWZacdfh-pr-xz]+\\b)(?:=[^ ]*|))+\n# tput arguments -- https://man7.org/linux/man-pages/man5/terminfo.5.html -- technically they can be more than 5 chars long...\n\\btput\\s+(?:(?:-[SV]|-T\\s*\\w+)\\s+)*\\w{3,5}\\b\n# macOS temp folders\n/var/folders/\\w\\w/[+\\w]+/(?:T|-Caches-)/\n# github runner temp folders\n/home/runner/work/_temp/[-_/a-z0-9]+\n"
  },
  {
    "path": ".github/actions/spelling/excludes.txt",
    "content": "# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-excludes\n(?:^|/)(?i)COPYRIGHT\n(?:^|/)(?i)LICEN[CS]E\n(?:^|/)(?i)third[-_]?party/\n(?:^|/)3rdparty/\n(?:^|/)generated/\n(?:^|/)go\\.(?:work\\.)?sum$\n(?:^|/)package(?:-lock|)\\.json$\n(?:^|/)Pipfile$\n(?:^|/)pyproject.toml\n(?:^|/)vendor/\n(?:^|/|\\b)requirements(?:-dev|-doc|-test|)\\.txt$\n-lock\\.yaml$\n\\.a$\n\\.ai$\n\\.all-contributorsrc$\n\\.avi$\n\\.bmp$\n\\.bz2$\n\\.cert?$|\\.crt$\n\\.class$\n\\.coveragerc$\n\\.crl$\n\\.csr$\n\\.dll$\n\\.docx?$\n\\.drawio$\n\\.DS_Store$\n\\.eot$\n\\.eps$\n\\.exe$\n\\.gif$\n\\.git-blame-ignore-revs$\n\\.gitattributes$\n\\.gitkeep$\n\\.graffle$\n\\.gz$\n\\.icns$\n\\.ico$\n\\.ipynb$\n\\.jar$\n\\.jks$\n\\.jpe?g$\n\\.key$\n\\.kiwi$\n\\.lib$\n\\.lock$\n\\.map$\n\\.min\\..\n\\.mo$\n\\.mod$\n\\.mp[34]$\n\\.o$\n\\.ocf$\n\\.otf$\n\\.p12$\n\\.parquet$\n\\.pdf$\n\\.pem$\n\\.pfx$\n\\.png$\n\\.psd$\n\\.pyc$\n\\.pylintrc$\n\\.qm$\n\\.s$\n\\.sig$\n\\.so$\n\\.svgz?$\n\\.sys$\n\\.tar$\n\\.tgz$\n\\.tiff?$\n\\.ttf$\n\\.wav$\n\\.webm$\n\\.webp$\n\\.woff2?$\n\\.xcf$\n\\.xlsx?$\n\\.xpm$\n\\.xz$\n\\.zip$\n^\\.github/actions/spelling/\n^\\Q.github/workflows/spelling.yml\\E$\n^\\Qpkg/rancher-desktop/assets/styles/fonts/_dots.scss\\E$\n^\\Qpkg/rancher-desktop/assets/styles/fonts/_zerowidthspace.scss\\E$\n^\\Qpkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/build.txt\\E$\n^\\Qpkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/pull03.txt\\E$\n^\\Qpkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/push.txt\\E$\n^\\QSECURITY.md\\E$\n^pkg/rancher-desktop/assets/scripts/logrotate-k3s$\n^pkg/rancher-desktop/assets/scripts/logrotate-lima-guestagent$\n^pkg/rancher-desktop/sudo-prompt/\n^pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/trivy-image-postgres\nignore$\n/translations/(?!en)\n^\\Qpkg/rancher-desktop/router.js\\E$\n^\\Qsrc/go/nerdctl-stub/nerdctl_commands_generated.go\\E$\n^\\Q.golangci.yaml\\E$\n^\\Qsrc/go/networking/.golangci.yml\\E$\n# Generated file\n^\\Qpkg/rancher-desktop/assets/extension-data.yaml\\E$\n# Mostly image names\n^\\Qscripts/assets/extension-data.yaml\\E$\n# Test data\n^screenshots/test-data/\n"
  },
  {
    "path": ".github/actions/spelling/expect.txt",
    "content": "abbrv\nactionmenu\nACTIONSTART\nactivedirectory\naddexclusion\naddext\naddgroup\naddlabel\naddrepo\nadfs\nadrg\nairgap\naks\nalertmanager\nalibaba\naliyun\naliyunecs\naliyunkubernetescontainerservice\nallcols\nallusers\nALLPLATFORMS\naltgraph\nandsection\napierrors\napify\napiservice\napitracker\nAPPDIR\nappimage\nappimagekit\nAPPLEID\napplescript\nAPPLICATIONFOLDER\nARPNOMODIFY\nARPPRODUCTICON\nARPURLINFOABOUT\narrowdown\narrowleft\narrowright\narrowup\nasound\nassumeyes\natk\nauthconfig\nauthdata\nAuthenticode\nauthprovider\nauxww\nAwop\nbackgrounding\nbackuptip\nbaiducloudcontainerengine\nbannrbmp\nbanzaicloud\nbasedisk\nbassano\nbatslib\nbci\nbellingham\nbinfmt\nblahblah\nblockmap\nBlt\nbootfs\nbosco\nbpf\nbpffs\nbrowserhome\nbrucebean\nbsdtar\nbuildctl\nbuildkit\nbuildkitd\nbuildmode\nbuildroot\nbulbasaur\nbulkable\nbulkaction\ncacerts\ncamelpunch\nCAPI\ncapslock\ncaroot\ncatalogtemplate\ncbr\nCCE\nceci\ncecinestpasuncategory\nceph\ncertutil\ncgroupfs\ncheckpath\nchirico\ncidata\ncidfile\nCim\nclientcmd\nclientset\nclonefile\ncloudca\nCNCF\ncni\ncnutils\ncommitish\ncomposefile\nconfd\nconfigjson\nconfigmap\nconflist\nconstrainttemplate\ncontainerapi\ncontainerd\nCONTAINERENGINE\ncontainernetworking\ncooldown\ncopypac\ncoredns\ncpanm\ncpanminus\ncrds\ncredfwd\nCREDHELPER\ncri\ncrond\ncshrc\nctrctl\nctxs\ndaemonset\ndapp\ndatadog\ndcmonitor\ndcnone\ndcrd\ndebbuild\nDebugw\ndecapsulates\ndedot\ndeepmap\ndefattr\ndeislabs\ndestinationrule\nDETECTEXCEPTIONS\ndevelopercertificate\nDEVMODE\ndfile\ndidinit\ndiffdisk\ndigitalocean\ndirents\ndistatus\ndistros\nDlg\ndlgbmp\ndlicense\nDNAT\ndnsmasq\ndoclink\ndonotuse\ndport\ndri\nDuex\ndustin\nDwm\ndwmapi\nDWMWA\nEACCESS\neagerzeroedthick\neastus\nebegin\necm\nedmonton\neinfo\nelectronjs\nelko\nendgroup\nengineimage\nepinio\nEPONY\nerrdefs\nERRFILE\nerrgroup\nErrorw\nerrwrap\nescapehtml\nestargz\nESXi\netcdbackup\nETest\neuid\neula\nexcludepkgs\nexcludesection\nExecutability\nEXITDIALOGOPTIONALCHECKBOX\nEXITDIALOGOPTIONALCHECKBOXTEXT\nexoscale\nexternalname\nexternalservice\nextglob\nfactoryreset\nfakercfile\nfanotify\nfav\nfdx\nfeaturename\nFEEEFEEE\nfemto\nffi\nFflags\nficlone\nfilekey\nfilestat\nfineprint\nfleetworkspace\nFOLDERID\nfontstack\nfqname\nfreeipa\nfrontends\nfscache\ngabcdef\ngazornaanplatt\ngcs\nGENERALIZEDTIME\ngetwindowid\ngha\ngitmodules\ngitrepo\ngke\nglobalrole\nglobalrolebinding\nGluster\ngname\ngoland\ngomod\ngooglegke\ngoogleoauth\ngopacket\nGOTOOLCHAIN\nGOWORK\ngovet\ngpu\ngtk\nguestagent\nGutterless\ngvisor\nhardlinks\nhashicorp\nHDD\nhealthz\nHec\nheketi\nhelmcharts\nhfs\nhkcu\nhklm\nHMR\nhocs\nhorizontalpodautoscaler\nHOSTPORT\nhoverable\nhowett\nhpa\nHRESULT\nhtpasswd\nhttpconfig\nHuawei\nhuaweicce\nhvsock\nhwnd\nhyperv\nicns\nIdempotently\nidentitytoken\niex\nifaces\nifname\nifnotstart\nifstarted\niidfile\nimageinfo\nIMAGENAME\nIMAGEPATH\nindentless\ninstallable\nINSTALLMESSAGE\nINSTALLPROPERTY\nIPlugin\niptable\nIsf\nislabel\nisthebestmeshuggahalbum\nistio\nistiod\nisv\nitp\niwr\njetstack\nJOBOBJECT\njoycelin\njsmith\nJSONOr\njsontable\nJSONTo\njulianb\nkarl\nKaspersky\nkde\nKDM\nkeycloak\nkeycloakoidc\nkeyform\nkeygrip\nkiali\nkib\nKinfo\nkiwano\nKNOWNFOLDERID\nkontainer\nkubeconfig\nKubectx\nkubepods\nkuberlr\nKubewarden\nkurrent\nkwctl\nldconfig\nLGHT\nlimactl\nlimaiptables\nlimaloc\nlinds\nlinenum\nlinkname\nlinode\nlinuxkit\nLinuxy\nloadbalancer\nloblaw\nlocalnet\nlogdna\nloglines\nlogz\nlte\nmabels\nmacarm\nmachinedeployment\nmachineset\nmacx\nmagog\nmcapps\nmediaselect\nmessageformat\nmetainfo\nmetricsection\nmikey\nmilli\nMinidriver\nminimizable\nmissmatched\nmitm\nmoar\nmoby\nmockhelper\nmoproxy\nmountinfo\nMRM\nmsiexec\nMSIHANDLE\nMSIINSTALLPERUSER\nMSIX\nmsixbundle\nmsize\nmsvs\nmultierror\nmultiport\nmungers\nmyport\nnamearray\nnapanee\nnavlink\nneq\nnerdctl\nnetlink\nnetns\nnetsh\nnetworkpolicy\nneu\nnewauth\nnewfs\nnewpid\nnginx\nngx\nninep\nnoarch\nnocopy\nnologin\nnologo\nnomount\nNOPASSWD\nnoprofile\nnoproxy\nnorc\nnorestart\nnormaliser\nNOSETENV\nnothrow\nnotifempty\nnotset\nnpipe\nnsenter\nnsis\nNSISUNINSTALLCOMMAND\nnullglob\nnuxt\nnxt\noapi\nobjectset\nocticons\nopenapitools\nopenldapconfig\nopenrc\nopenresty\nopenssh\nopenstack\nopensuse\nopentelekomcloudcontainerengine\noperstate\nopsgenie\noracleoke\norsection\nosacompile\nosascript\nosc\nosxkeychain\notccce\noverlayfs\npagedown\npageload\npagerduty\npageup\nparsesection\npascalize\npathspec\npcs\npdp\npersistentvolume\npersistentvolumeclaim\nPFlags\npgid\npidfd\nPII\npikachu\nPinganyun\npinganyunecs\nplists\nplutil\npodmetrics\npodmonitor\npodsecuritypolicytemplate\nPOLLIN\nPOptions\nportbinding\nportforward\nportmap\nportmapping\nportproxy\nportstorage\npostgres\nPOSTROUTING\nprakhar\nprebuilds\npreflights\nPrivs\nPROCARGS\nprocnet\nprocnettcp\nprogresskey\nprojectroletemplatebinding\nPrometheis\nprometheusrule\nPROMPTROLLBACKCOST\nprotip\nPROXYLIST\nPSelected\nPSHOME\npsps\nptn\npublicdomain\npushable\npvc\nPWSTR\nqcow\nrackspace\nramdisks\nrancherdesktop\nrancherkubernetesengine\nrawdata\nrcedit\nrcfiles\nrcompare\nrdctl\nrddepman\nrdinstall\nRDRUNAFTERINSTALL\nrdshell\nrdsudo\nrdtest\nrdvsock\nrdx\nreadyz\nregexpsection\nregfiles\nregistrationtoken\nregistrytoken\nregtest\nremotedns\nreplicaset\nreplicationcontroller\nrepofile\nrequried\nresourcequota\nresourceset\nrestclient\nrestoretip\nreusecab\nrioinfo\nrke\nRLENGTH\nrmi\nrockylinux\nRoffline\nrolebinding\nroletemplate\nroletemplatebinding\nrootfs\nrosetta\nRSTART\nrtm\nRunas\nrunbook\nrunc\nrundir\nrunlevel\nruntimeclass\nruntimeclasses\nrvf\nscaleio\nscanbenchmark\nscanprofile\nscanreport\nscinfo\nscreencapture\nscriptdir\nscriptname\nscriptpath\nscrollback\nscrolllock\nsdl\nsecretservice\nserveraddress\nservernum\nserviceaccount\nserviceapi\nservicemonitor\nservicewatcher\nsetproxy\nsfc\nsharedscripts\nsharkanodo\nshasta\nshazbat\nshortkey\nshortlived\nshowduplicates\nshowmuted\nshuf\nsigch\nsigntool\nsingleparsesection\nsio\nskopeo\nsku\nSLAs\nSLIRP\nslo\nSLSA\nsmanager\nsmokris\nsnakize\nsoftmmu\nsomeothername\nsomepaththatshouldnevereverexist\nsonggao\nspinkube\nsplatform\nsplunk\nssd\nsshfs\nsslip\nSsr\nSTARTUPINFO\nstatefulset\nstoinks\nstorageclass\nstorybookjs\nstringifying\nsubkey\nsubshells\nsubvar\nsumologic\nSVCNAME\nsvm\nsyscalls\nsysfs\ntarballs\nTARGETDIR\ntcpip\ntcshrc\nTEAMID\nteamocil\nTelekom\ntempl\nTencent\ntencenttke\ntermch\ntestbuild\ntestfilethatdoesntexist\ntestid\ntestreleasedate\ntesturl\nthanosruler\ntimekey\ntimespan\ntini\nTKE\ntmpconfig\ntmpfiles\ntmutil\ntoggleable\ntogglefullscreen\ntonistiigi\ntoolsets\ntopmenu\nTQF\ntraefik\ntrivy\nTStr\nTVar\ntvf\nucmonitor\nudhcpc\nUEFI\nunexpose\nunexposing\nunparsable\nupcloud\nUpgradable\nuseb\nuserdb\nuserpreference\nUTCTIME\nvcenter\nvcpus\nvde\nVDropdown\nVERSIONSTRING\nveth\nVhd\nvhdx\nvinzenz\nvirtio\nvirtiofs\nvirtsock\nvirtualnetwork\nvirtualservice\nvishvananda\nVMGUID\nVMID\nvmnet\nvmwarevsphere\nvmx\nvnc\nvnode\nvpnkit\nvsock\nvtunnel\nvul\nvznat\nwhatsnew\nwincred\nwinio\nwinthrop\nwix\nwixobj\nWIXUI\nwontfix\nwordpress\nwslconfig\nwslenv\nWSLg\nwslify\nwslinfo\nWSLINSTALLED\nWSLIs\nWSLKERNELOUTDATED\nwslproxy\nwslutils\nWWID\nwwn\nwws\nxauth\nXAUTHORITY\nxdev\nxec\nxfs\nxmlout\nxorg\nXVar\nxwininfo\nxyzzy\nxzf\nyamlfmt\nyarker\nyoumightnotneedjquery\nYubi\nzeroedthick\nzerowidthspace\nzipperhead\nZSelected\nzst\nzstack\nzxf\nzypak\nzypp\nzypper\n"
  },
  {
    "path": ".github/actions/spelling/line_forbidden.patterns",
    "content": "# reject `m_data` as VxWorks defined it and that breaks things if it's used elsewhere\n# see [fprime](https://github.com/nasa/fprime/commit/d589f0a25c59ea9a800d851ea84c2f5df02fb529)\n# and [Qt](https://github.com/qtproject/qt-solutions/blame/fb7bc42bfcc578ff3fa3b9ca21a41e96eb37c1c7/qtscriptclassic/src/qscriptbuffer_p.h#L46)\n#\\bm_data\\b\n\n# Were you debugging using a framework with `fit()`?\n# If you have a framework that uses `it()` for testing and `fit()` for debugging a specific test,\n# you might not want to check in code where you skip all the other tests.\n#\\bfit\\(\n\n# English does not use a hyphen between adverbs and nouns\n# https://twitter.com/nyttypos/status/1894815686192685239\n(?:^|\\s)[A-Z]?[a-z]+ly-(?=[a-z]{3,})(?:[.,?!]?\\s|$)\n\n# Smart quotes should match\n\\s’[^.?!‘’]+’[^.?!‘’]+‘[^.?!‘’]+’|\\s‘[^.?!‘’]+’[^.?!‘’]+’[^.?!‘’]+’|\\s”[^.?!“”]+”[^.?!“”]+“[^.?!“”]+”|\\s“[^.?!“”]+”[^.?!“”]+”[^.?!“”]+”\n\n# Don't write double negatives\n\\w+n't not(?=\\s)\n\n# Generally spaces follow instead of preceding `,`s\n# It's possible this is some strange CSV dialect, but, even so, you could probably move the space.\n\\s[a-z]{3,} ,[a-z]{3,}\\s\n\n# Generally words are written with `'s`, not `\"s`\n[^=|+']\\s\\w+\"s\\s\n\n# Don't miswrite **irreversible binomials**\n# https://en.wikipedia.org/wiki/Irreversible_binomial\n(?i)\\b(?:cheese and macroni|honey and milk|sweet and short|die or do|roll and rock|the bees and the birds|match and mix|tear and wear|clear and loud|death and life|span and spick|vigor and vim|abet and aid|says and deposes|means and ways|dryer and washer|relaxation and rest|famous and rich|loan and savings|come high\\s?water or hell|tuck and nip|turf and surf|between a hard place and a rock|dime and five|mouse and cat|tired and sick|pregnant and barefoot|feathered and tarred|feathers and tar|subtraction and addition|liabilities and assets|forth and back|strikes and balls|end to beginning|white and black|small and big|bust or boom|groom and bride|sister and brother|pass and butt|sell and buy|release and catch|effect and cause|state and church|robbers and cops|go and come|going and coming|Indians and cowboys|nights and days|wide and deep|flow and ebb|ice and fire|last and first|ceiling to floor|drink and food|aft and fore|domestic and foreign|backward and forward|foe or friend|back to front|vegetables and fruits|take and give|evil and good|foot and hand|heels over head|Hell and Heaven|there and here|seek and hide|dale and hill|her and him|low and high|valleys and hills|hers and his|thither and hither|yon and hither|cold and hot|wife and husband|out and in|gentlemen and ladies|sea and land|death or life|short and long|found and lost|hate and love|war and love|wife and man|matter over mind|pop and mom|nice or naughty|far and near|tuck and nip|south to north|then and now|later and now|shut and open|under and over|ride and park|starboard and port|cons and pros|pull and push|file and rank|fall and rise|loan and savings|water and soap|finish to start|go and stop|dip and strike|sour and sweet|thin and thick|ring and tip|fro and to|bottom to top|country and town|down and up|downs and ups|downtown and uptown|peace and war|dryer and washer|wane and wax|no and yes|yang and yin|a curse and a blessing|don'ts and dos |farewell and hail|wait and hurry up|difference\\s(?:\\w+\\s+)+day and night|in health and in sickness|from stern to stem|the dead and the quick|a place and a time|generations and ages|comfort and aid|alack and alas|pieces and bits|soul and body|early and bright|mortar and brick|jowl by cheek|tidy and clean|verse and chapter|saucer and cup|cents and dollars|loathing and fear|chips and fish|foremost and first|farewell and hail|fist over hand|shoulders and head|soul and heart|spices and herbs|home and house|thirst and hunger|fork and knife|bounds and leaps|behold and lo|tidy and neat|dime and nickel|cranny and nook|void and null|bolts and nuts|suffering and pain|quiet and peace|ink and pen|choose and pick|simple and plain|proper and prim|rave and rant|shoals and rocks|awe and shock|wonders and signs|bones and skull|crossbones and skull|narrow and strait|narrow and straight|strain and stress|roundabouts and swings|chiggers and ticks|complain and whine|rain and wind|amen and yea|(?:raised|bred) and born|by crook or by hook|(?<=it was a )stormy and dark(?= night)|(?<=this ) age and day|cross the t's and dot the i's|high minded and haughty|best and highest(?= use)|like daughter, like mother|done and over with|(?<=on ) needles and pins|half a dozen of the other, six of one|(?<=up ) personal and close|baggage and bag|beads and baubles|balance and beams|breakfast and bed|braces and belt|bar and bench|bad and big|bosh bash bish|blue and black|beautiful and bold|Baptists and bootleggers|briefs or boxers|butter and bread|boar and bull|carry and cash|cheese and chalk|clans and cliques|control and command|cream and cookies|dumb and deaf|dash and dine|dirty and down|drabs and dribs|drive and drink|disorderly and drunk|furious and fast|famine or feast|forget and fire|fury and fire|fauna and flora|forget and forgive|function and form|foe or friend|frolics and fun|feathers and fur|goblins and ghosts|giggles and grins|home and hearth|haw and hem|holler and hoot|handgrenades and horseshoes|Gentile and Jew|jiving and juking|country and king|caboodle and kit|kin and kith|longitude and latitude|limb and life|learn and live|load and lock|match and mix|mild and meek|number and name|parcel and part|pencil and pen|post to pillar|pans and pots|perish or publish|riches to rags|raving and ranting|write and read|rumble to ready|wrong and right|roll and rock|ready and rough|regulations and rules|secure and safe|sound and safe|shell and shot|shave and shower|symptoms and signs|slide and slip|span and spick|shine and spit|Stripes and Stars|stones and sticks|spice and sugar|that or this|tat for tit|tail and top|turn and toss|treat or trick|tribulations and trials|tested and tried|true and tried|trailer and truck|wear and wash|waiting and watching|wail and weep|wild and wet|hollering and whooping|woolly and wild|wonderful and wise|warlocks and witches|ruin and wrack|the bees and the birds|(?<=between the) deep blue sea and the devil|Dragons & Dungeons|fuck off or fit in|flop-flip|fancy-free and footloose|to hold and to have|least but not last|Lease-Lend|leave ['‘]em and love ['‘]em|leave it or love it|paper and pen(?:cil|)|patter-pitter|relaxation and rest|(?<=without )reason or rhyme|tacky-ticky|take and break|zoom and boom|cox and box|talk and chalk|darts and charts|dip and chips|drive and dive|square and fair|dime and five|jetsam and flotsam|dry and high|fire and hire|split and hit|thither and hither|trot to hot|puff and huff|bustle and hustle|gap and lap|greatest and latest|proud and loud|greet and meet|right makes might|shame and name|dear and near|sods and odds|upwards and onwards|about and out|proud and out|dump and pump|tough and rough|gun and run|clout and shout|bake and shake|surely but slowly|joke and smoke|dash and stash|bitch and stitch|drop and stop|turf and surf|tide and time|gown and town|bake and wake|tear and wear|feed and weed|dealing and wheeling|dine and wine|nay or yea|trouble double|bender fender|dandy-handy|panky-hanky|scarum-harum|skelter helter|piggledy higgledy|quit it and hit|pocus hocus|toity[- ]hoity|potch-hotch|burly-hurly|bitty-itty|bitsy-itsy|votor moter|the highway or my way|pamby-namby|claim it and name it|ever, never|gritty nitty|porgy orgy|mell-pell|baggy saggy|so good, so far|weeny-teeny|blue true|lose it or use it|nilly willy|(?<=the )nays and (?:the |)yeas|beyond and above|graces and airs|muster and alarm|kicking and alive|well and alive|dangerous and armed|oranges and apples|fill and back|forth and back|eggs and bacon|mash and bangers|switch and bait|tackle and bait|pregnant and barefoot|sale and bargain|breakfast and bed|call and beck|whistles and bells|suspenders and belt|bold and big|tall and big|better and bigger|purge and binge|bridle and bit|bobs and bits|pieces and bits|blue and black|tackle and block|guts and blood|gore and blood|weave and bob|arrow and bow|determined and bound|gagged and bound|scrape and bow|bit and brace|water and bread|circuses and bread|roses and bread|serve and brown|spade and bucket|grind and bump|run and bump|large and by|gown and cap|driver and car|mouse and cat|balances and checks|dumplings and chicken|change and chop|sober and clean|dagger and cloak|tie and coat|doughnuts and coffee|go and come|burn and crash|sugar and cream|punishment and crime|saucer and cup|paste and cut|run and cut|burdock and dandelion|night and day|buried and dead|gone and dead|taxes and death|dash and dine|conquer and divide|out and down|cover and duck|dive and duck|every and each|ears and eyes|figures and facts|wide and far|furious and fast|loose and fast|dandy and fine|thumbs and fingers|brimstone and fire|foremost and first|chips and fish|blood and flesh|bone and flesh|ever and forever|center and front|games and fun|bother and fuss|take and give|aspirations and goals|plenty and good|light and goodness|pound and ground|slash and hack|hearty and hale|fast and hard|eggs and ham|nail and hammer|sickle and hammer|tongs and hammer|minds and hearts|now and here|seek and hide|watch and hide|mighty and high|dry and high|tight and high|miss and hit|run and hit|yon and hither|thither and hither|hosed and home|dry and home|eye and hook|loop and hook|buggy and horse|carriage and horse|heavy and hot|high and hot|bothered and hot|puff and huff|when and if|custard and kippers|tell and kiss|kin and kith|fork and knife|screaming and kicking|streams and lakes|order and law|behold and lo|dam and lock|key and lock|feel and look|clear and loud|boy and man|potatoes and meat|women and men|cookies and milk|honey and milk|tenon and mortise|shakers and movers|address and name|faces and names|easy and nice|cranny and nook|crosses and noughts|bolts and nuts|ends and odds|away and off|done and one|about and out|out and over|terminer and oyer|cream and peaches|Qs and Ps|carrots and peas|axe and pick|moan and piss|vinegar and piss|whine and piss|proper and prim|booty and prize|cons and pros|beans and pork|simple and pure|dirty and quick|pinion and rack|ruin and rack|pillage and rape|famous and rich|fall and rise|shine and rise|board and room|tumble and rough|jump and run|pepper and salt|vinegar and salt|sniff and scratch|rescue and search|destroy and seek|tie and shirt|fat and short|sweet and short|stout and short|tell and show|jive and shuck|tired and sick|burn and slash|arrows and slings|fall and slip|steady and slow|grab and smash|mirrors and smoke|ladders and snakes|dance and song|fury and sound|polish and spit|deliver and stand|strain and stress|Drang und Sturm|debonair and suave|tie and suit|rainbows and sunshine|demand and supply|light and sweetness|sandal and sword|chairs and tables|thin and tall|feathers and tar|crumpets and tea|lightning and thunder|ass and tits|fro and to|nail and tooth|go and touch|field and track|error and trial|tribulations and trials|roll and tuck|turn and twist|about and up|coming and up|vigor and vim|see and wait|fuzzy and warm|weft and warp|ward and watch|wane and wax|means and ways|good and well|whine and whinge|roses and wine|phrases and words|no and yes|a leg and an arm|(?<=old )chain and ball|by golly and by guess|bull-and-cock|dried (dry) and cut|(?<=in this )age and day|pony and dog show|(?<=by )starts and fits|grin and bear it|(?<=move ) earth and heaven|quit it and hit it|kisses and hugs|(?<=for all )purposes and intents|make up and kiss|last testament and will|make do and mend|(?<=every ) then and now|for all and once(?=[,.;!?])|jelly and peanut butter|ice cream and pickles|raining dogs and cats|development and research|blues and rhythm|(?<=between a )hard place and a rock|(?<=all's )done and said|(?<=different )sizes and shapes|bones? and skin|(?<=in )spirit and (?:in |)truth|a miss and a swing|(?<=through )thin and thick|O's and X's|a day and a year|nothing or all|worse or better|small or big|white or black|pleasure or business|night or day|alive or dead|die or do|flight or fight|take or give|bad or good|simple or gentle|she or he|tails or heads|her or his|miss or hit|cure or kill|break or make|less or more|never or now|shine or rain|reason or rhyme|wrong or right|swim or sink|later or sooner|more or two|down or up|death or victory|lose or win|no or yes|the egg or (?:the |)chicken|(?<=neither )fowl nor fish|(?<=come )high water or hell|(?<=neither )there nor here|(?<=neither )hair nor hide|(?<=not one )tittle or jot|(?<=neither )money nor love|shut up or put up|leave it or take it|(?<=neither )ornament nor use|gatherer-hunter|cheese corn|Costello and Abbott|Isaac and Abraham|Patroclus and Achilles|Eve and Adam|Anicetus and Alexiares|Cleopatra and Antony|Ant & Dec|Robin and Batman|Clyde and Bonnie|Abel and Cain|Ball and Cannon|Pollux and Castor|Psyche and Cupid|Clack and Click|Pythias and Damon|Goliath and David|Guattari and Deleuze|Jane and Dick|Marguerite and Faust|Swann and Flanders|Saunders and French|Frack and Frick|Laurie and Fry|Sullivan and Gilbert|Aga and Gilgamesh|Gretel and Hansel|Hellman & Friedman|Esau and Jacob|Jill and Jack|Victor and Jack|Vijaya and Jaya|Jekyll & Hyde|Hardy and Laurel|McCartney and Lennon|Loewe and Lerner|Clark and Lewis|Lilo & Stitch|Large and Little|Meslamta-ea and Lugal-irra|Luigi and Mario|Lewis and Martin|Ashley and Mary-Kate Olsen|Sue and Mel|Wise and Morecambe|Mindy and Mork|Eurydice and Orpheus|Horse-Face and Ox-Head|Penn & Teller|Aristotle and Phyllis|Ferb and Phineas|Pinky & The Brain|Galatea and Pygmalion|Ren & Stimpy|Rhett & Link|Morty and Rick|Hart and Rodgers|Hammerstein and Rodgers|Juliet and Romeo|Remus and Romulus|Guildenstern and Rosencrantz|Max and Sam|Delilah and Samson|Simon & Garfunkel|Sonny & Cher|Thelma & Louise|Thompson and Thomson|Tom & Jerry|Isolde and Tristan|Tim & Eric|Adonis and Venus|Vic & Bob|Crick and Watson|Eve and Adam|pears and apples|glass and bottle|Liszt and Brahms|bone and dog|toad and frog|blister and hand|south and north|pork and rabbit|strife and trouble|eight and two|flute and whistle)\\b\n\n# Don't use `requires that` + `to be`\n# https://twitter.com/nyttypos/status/1894816551435641027\n\\brequires that \\w+\\b[^.]+to be\\b\n\n# A fully parenthetical sentence’s period goes inside the parentheses, not outside.\n# https://twitter.com/nyttypos/status/1898844061873639490\n\\([A-Z][a-z]{2,}(?: [a-z]+){3,}\\)\\.\\s\n\n# Complete sentences shouldn't be in the middle of another sentence as a parenthetical.\n(?<!\\.)(?<!\\betc)\\.\\),\n\n# Complete sentences in parentheticals should not have a space before the period.\n\\s\\.\\)(?!.*\\}\\})\n\n# Write out small numbers (unless they are code)\n# See https://www.scribendi.com/academy/articles/when_to_spell_out_numbers_in_writing.en.html#:~:text=Writing%20Small%20and%20Large%20Numbers,ten%29%20are%20written%20as%20numerals%2e\n#(?<![=>]|depth|frame|page|project|select)\\s[1-9] (?!byte|day|hour|meaning|minute|month|(?:new |)page|people|(?:more |)space|year)[a-z]+ [a-z]+\\s\n\n# Write out numbers at the start of a sentence\n# https://www.scribendi.com/academy/articles/when_to_spell_out_numbers_in_writing.en.html#:~:text=Beginning%20a%20Sentence%20with%20a%20Number,may%20be%2e\n(?:\\b[a-z]{4,}|\\s(?:[a-eg-z][a-z]{2}|f[a-hj-z][a-z]|fi[a-fh][a-z]))[:.?!] [1-9] [a-z]{3,} [a-z]+\\s\\w+\n\n# Don't write two numbers in a row\n# https://www.scribendi.com/academy/articles/when_to_spell_out_numbers_in_writing.en.html#:~:text=Paired%20Numbers%20%28Two%20Numbers%20in,librarian%20to%20begin%20story%20time%2e\n(?:[a-z]{4,}|\\s(?!apr|aug|dec|feb|fri|jan|mar|mon|nov|oct|sat|sep|sun|thu|tue|wed)[a-z]{3})\\s\\d+\\s\\d+(?!--)[-\\s](?:(?!--)[-A-Za-z]){2,}\\s\n\n# This probably indicates Mojibake https://en.wikipedia.org/wiki/Mojibake\n# You probably should try to unbake this content\nÃ(?:Â[¤¶¥]|[£¢])|Ãƒ\n\n# Should be `HH:MM:SS`\n\\bHH:SS:MM\\b\n\n# Should be `86400` (seconds in a standard day)\n\\b84600\\b(?:.*\\bday\\b)\n\n# Should probably be `2006-01-02` (yyyy-mm-dd)\n# Assuming that the time is being passed to https://go.dev/src/time/format.go\n\\b2006-02-01\\b\n\n# Should probably have a trailing `.`\n\\s([a-z]\\.){2,}[a-z]\\s\n\n# Should probably end with `”`\n# Likely bad OCR\n“.+[^'‘\\\\\\[]+’'(?!['\"])\n\n# Should probably end with `”` or with only one of `’`/`'`\n\\s\\w+[^'‘\\\\\\[]+’'(?!['\"])\n\n# Should probably be matching (smart)quotes or backticks (if Markdown)\n# Unless the file format is Tex\n(?<!`)``(?!`).*?''\n\n# Should probably be `YYYYMMDD`\n\\b[Yy]{4}[Dd]{2}[Mm]{2}(?!.*[Yy]{4}[Dd]{2}[Mm]{2}).*$\n\n# Should be `a bit of`\n(<=\\bquite )some(?= hands-on)\n\n# Should be `a few`\n(<=\\bquite )some(?= hands\\s)\n\n# Should be `a priori` or `and prior`\n(?i)(?<!posteriori)\\sand priori\\s\n\n# Should be `a`\n\\san (?=(?:[b-df-gjklpqtvwz]|h(?!our|s[lv]|tml|ttp|ref)|n(?!ginx|grok|pm)|r(?!c)|s(?!s[ho]|vg))[a-z]|x(?!\\d|ml))\n\n# Articles generally shouldn't be used without a noun and a verb\n# - Perhaps you're missing a verb between the noun and the second article.\n# - Or, perhaps you should remove the first verb and treat the intervening word as a verb?\n# - In some cases you should add a `,` between the noun and the second article.\n\\s(?:an?|the)\\s(?!wh|how\\b)[A-Za-z][a-z]+[a-qs-z]\\s(?:an?|the(?! same))\\s\n\n# Should only be one of `a`, `an`, or `the`\n\\b(?:(?:an?|the)\\s+){2,}\\b\n\n# Should be `a large amount` or `large amounts`\n(?<=\\bof )large amount(?= of data\\b)\n\n# Should be a list `something, a second thing, or a third thing` or `something, a thing to do a thing`\n# -- This rule is experimental, if you find it has a high false-positive rate, please let the maintainer know\n(?:^|[?!.] )(?:(?![Ff]or example|Currently|In \\w)[^()?!;,.])+, a(?:\\s+(?!and|to\\b)\\w+)+?\\s+an?\\b\n\n# Should only be `are` or `can`, not both\n\\b(?:(?:are|can)\\s+){2,}\\b\n\n# Should be `at` or possibly `in`\n\\bon(?= the (?:top|bottom) (?:left|right) \\w+)\n\n# Should probably be `ABCDEFGHIJKLMNOPQRSTUVWXYZ`\n(?i)(?!ABCDEFGHIJKLMNOPQRSTUVWXYZ)ABC[A-Z]{21}YZ\n\n# Should be `please` or `also, please`\n# https://english.stackexchange.com/questions/106165/please-do-also-or-please-also\n\\b[Pp]lease(?:, do|) also\\b\n\n# Should be `an`\n#(?<!\\b[Ii] )(?<![-.])(?<!\\d\\s?)\\bam\\b(?!/pm|[:\")])\n\n# Should be `an`\n\\sa(?= (?:a(?!nd\\s|s\\s)|e(?!u)|i(?![ns]\\s)|o(?!nc?e)|u(?!biquitous|int|kr|n[ai]|r[ael]|s[aeiu]|tf\\d*|til|topia|uid|vula|v\\b)|y(?!arn|ear|oga|y)))\n\n# Should be `anymore`\n\\bany more[,.]\n\n# Should be `Ask`\n(?:^|[.?]\\s+)As\\s+[A-Z][a-z]{2,}\\s[^.?]*?(?:how|if|wh\\w+)\\b\n\n# Should be `at one fell swoop`\n# and only when talking about killing, not some other completion\n# Act 4 Scene 3, Macbeth\n# https://www.opensourceshakespeare.org/views/plays/play_view.php?WorkID=macbeth&Act=4&Scene=3&Scope=scene\n\\bin one fell s[lw]?oop\\b\n\n# Should be `'`\n(?i)\\b(?:(?:i|s?he|they|what|who|you)[`\"]ll|(?:are|ca|did|do|does|ha[ds]|have|is|should|were|wo|would)n[`\"]t|(?:s?he|let|that|there|what|where|who)[`\"]s|(?:i|they|we|what|who|you)[`\"]ve)\\b\n\n# Should be `background` / `intro text` / `introduction` / `prologue` unless it's a brand or relates to _subterfuge_\n(?i)\\bpretext\\b\n\n# Should be `Bitbucket`\n\\bBitBucket\\b\n\n# Should be `bearer`\n\\b(?<=the )burden(?= of bad news\\b)\n\n# Should be `beginning`\n\\b(?<=from )begin(?= to end\\b)\n\n# Should be `bona`\n# unless talking about bones\n\\bbone(?= fide\\b)\n\n# Should be `branches`\n# ... unless it's really about the meal that replaces breakfast and lunch.\n\\b[Bb]runches\\b\n\n# Should be `briefcase`\n\\bbrief-case\\b\n\n# Should be `by`\n(?<=caused )from(?= an?\\b)\n\n# Should be `by far` or `far and away`\n\\bby far and away\\b\n\n# Should be `by and large`\n\\bby in large\\b\n\n# Should be `bytes`\n# unless talking about sports where a team gets to skip a game, or\n# saying `goodbyes` (even this is questionable)\n(?<!\\\\)\\bbyes\\b\n\n# Should be `can, not only ..., ... also...`\n\\bcan not only.*can also\\b\n\n# Should be `cannot` (or `can't`)\n# See https://www.grammarly.com/blog/cannot-or-can-not/\n# > Don't use `can not` when you mean `cannot`. The only time you're likely to see `can not` written as separate words is when the word `can` happens to precede some other phrase that happens to start with `not`.\n# > `Can't` is a contraction of `cannot`, and it's best suited for informal writing.\n# > In formal writing and where contractions are frowned upon, use `cannot`.\n# > It is possible to write `can not`, but you generally find it only as part of some other construction, such as `not only . . . but also.`\n# - if you encounter such a case, add a pattern for that case to patterns.txt.\n\\b[Cc]an not\\b(?! only\\b)\n\n# Should be `chart`\n(?i)\\bhelm\\b.*\\bchard\\b\n\n# Should be `code`\n(?<=\\bof )a code(?= that\\b)\n\n# Should be `counter-intuitive`\n\\bcounter intuitive\\b\n\n# Do not use `(click) here` links\n# For more information, see:\n# * https://www.w3.org/QA/Tips/noClickHere\n# * https://webaim.org/techniques/hypertext/link_text\n# * https://granicus.com/blog/why-click-here-links-are-bad/\n# * https://heyoka.medium.com/dont-use-click-here-f32f445d1021\n(?i)(?:>|\\[)(?:(?:click |)here|link|(?:read |)more)(?:</|\\]\\()\n\n# Including \"image of\" or \"picture of\" in alt text is unnecessary.\n\\balt=['\"](?:an? |)(?:image|picture) of\n\n# Alt text should be short\n\\balt=(?:'[^']{126,}'|\"[^\"]{126,}\")\n\n# Should be either of `default` or `fallback`, but not both\n# Unless you have a non-default fallback, but that's just weird.\n\\bdefault fallback\\b\n\n# Should be `dress code`\n\\bdressing code\\b\n\n# Should be `effect`\n(?<=\\btake )affect\\b\n\n# Should be `end`\n(?<=\\b[Ww]e )ends\\b\n\n# Should be `ends`\n\\bend's(?= up\\b)\n\n# Should be `-endian`\n\\b(?i)(?<=big|little) endian\\b\n\n# Should be `equals` to `is equal to`\n\\bequals to\\b\n\n# Should be `equal to`, `equal width`, or reworded\n\\bequal with\\b(?! (?:no|respect))\n\n# Should be `eta` or `calculate eta`\n(?i)\\bestimated? eta\\b\n\n# Should be `ECMA` 262 (JavaScript)\n(?i)\\bTS\\/EMCA\\b|\\bEMCA(?: \\d|\\s*Script)|\\bEMCA\\b(?=.*\\bTS\\b)\n\n# Should be `ECMA` 340 (Near Field Communications)\n(?i)EMCA[- ]340\n\n# Should be `exceed`\n\\bare higher than\\b\n\n# Should be `exceeds`\n\\bis higher than\\b\n\n# Should be `fall back`\n(?<!\\ba )\\bfallback(?= to)\\b\n\n# Should be `for`, `for, to` or `to`\n\\b(?:for to|(?<!\\bup )to for)\\b\n\n# Should be `ghcr.io`\n# https://bmitch.net/blog/2025-08-22-ghrc-appears-malicious/\n\\bghrc\\.io\\b\n\n# Should be `GitHub`\n(?<![&*.]|// |\\b(?:from|import|type) )\\bGithub\\b(?![{()])\n\n# Should be `GitLab`\n(?<![&*.]|// |\\b(?:from|import|type) )\\bGitlab\\b(?![{()])\n\n# Should be `GmbH`\n\\bGmbh\\b\n\n# Should be `heartrending` unless talking about drawing hearts\n\\b(?i)heart[- ]rendering\\b(?![^.?!]*(?:hearts|quirk))\n\n# Should probably be `https://`...\n# Markdown generally doesn't assume that links are to urls\n\\]\\(www\\.\\w\n\n# Should be `https://www.chiark.greenend.org.uk/~sgtatham/putty/`\n# See https://hachyderm.io/@simontatham/114846017785770922\n\\bputty\\.org\\b\n\n# Often should be `if`/`when` (or reworded)\n# unless describing exceptional cases (i.e. emergencies)\n#(?<!just )\\bin case\\b(?! (?:I|s?he|they|of|we|you))\n\n# Should be `in`\n#(?<=\\b(?:go|reach) back )into(?= time\\b)\n\n# Should be `in` or `out`\n(?<=\\bfill )up(?= the forms?\\b)\n\n# Should be `intents and purposes`\n(?<=[Ff]or all )intensive purposes\\b\n\n# Should be `JavaScript`\n\\bJavascript\\b\n\n# Should be `Jira`\n\\bJIRA\\b(?!-\\d+)\n\n# Should be `macOS` or `Mac OS X` or ...\n\\bMacOS\\b\n\n# Should be `Microsoft`\n\\bMicroSoft\\b\n\n# Should be `OAuth`\n(?:^|[^-/*$])[ '\"]oAuth(?: [a-z]|\\d+ |[^ a-zA-Z0-9:;_.()])\n\n# Should be `pytest`\n# see https://docs.pytest.org/en/stable/\n\\bPytest\\b\n\n# Should be `RabbitMQ`\n\\bRabbitmq\\b\n\n# Should be `TensorFlow`\n\\bTensorflow\\b\n\n# Should be `TypeScript`\n\\bTypescript\\b\n\n# Should be `also need to`\n\\bneed to also\\b\n\n# Should be `another`\n\\ban[- ]other(?!-)\\b\n\n# Should be `case-(in)sensitive`\n\\bcase (?:in|)sensitive\\b\n\n# Should be `Cloud`\nCLoud\n\n# Should be `coinciding`\n\\bco-inciding\\b\n\n# Should be `convert`\n# Unless talking about a thing named `Covert`\n\\b[Cc]overt(?= (?!channel)\\w+ to\\b)\n\n# Should be `deprecation warning(s)`\n\\b[Dd]epreciation [Ww]arnings?\\b\n\n# Should be `disk space.`\n(?<=to save )disk\\.(?![a-zA-Z])\n\n# Should be `gets`\n\\scommand get the\\s\n\n# Should be `greater than`\n\\bgreater then\\b\n\n# Should be `has`\n\\b[Ii]t only have\\b\n\n# Should be `here-in`, `the`, `them`, `this`, `these` or reworded in some other way\n\\bthe here(?:\\.|,| (?!and|defined))\n\n# Should be `going to bed` or `going to a bad`\n\\bgoing to bad(?!-)\\b\n\n# Should be `greater than`\n\\bhigher than\\b\n\n# Should be `Homebrew`\n\\bHomeBrew\\b\n\n# Should be `ID` (unless it's a flag/property/RCS variable)\n(?<![-\\.$])\\bId\\b(?![(=:'\"]|  )\n\n# Should be `in front of`\n\\bin from of\\b\n\n# Should be `into`\n# when not phrasal and when `in order to` would be wrong:\n# https://thewritepractice.com/into-vs-in-to/\n#(?<!opt)\\sin to\\s(?!if\\b)\n\n# Should be `use`\n\\sin used by\\b\n\n# Should be `in-depth` if used as an adjective (but `in depth` when used as an adverb)\n\\bin depth\\s(?!before\\b|rather\\b)\\w{6,}\n\n# Should be `in-flight` or `on the fly` (unless actually talking about airline flights)\n\\bon[- ]flight\\b(?!=\\s+(?:(?:\\w{2}|)\\d+|availability|booking|computer|data|delay|departure|management|performance|radar|reservation|scheduling|software|status|ticket|time|type|.*(?:hotel|taxi)))\n\n# Should be `in case`\n\\bencase(?= of)\\b\n\n# Should be `is obsolete`\n\\bis obsolescent\\b\n\n# Should be `it's` or `its`\n(?<![.'])\\bits['’]\n\n# Should be `its`\n(?<=\\b(?:in|of) )it's\\b\n\n# Should be `its`\n\\bit's(?= (?:child|only purpose|own(?:er|)|parent|sibling)\\b)\n\n# Should be `its`\n(?<!since )\\bit's(?= data\\b)\n\n# Should be `... its`\n\\w{2}(?<!\\b(?:case|fail|know|mean|perhap|purpose|sometime|think|unles|ye))s it's\\b(?! (?:an?|going|not|own|the|\\w+ed)\\b)\n\n# Should be `for its` (possessive) or `because it is`\n\\bfor it(?:'s| is)\\b\n\n# Should be `lends`\n\\bleads(?= credence)\n\n# Should be `few times` or `little time`\n(?<=\\bvery )few time\\b\n\n# Should be `log in`\n\\blogin to the\n\n# Should be `long-standing`\n\\blong standing\\b\n\n# Should be `lose`\n(?<=\\bwill )loose\\b\n\n# Should be `OAuth`\n\\bOauth\\b\n\n# Should be `paste`\n(?<=and )past(?= it)\n\n# `apt-key` is deprecated\n# ... instead you should be writing a pair of files:\n# ... * the gpg key added to a distinct key ring file based on your project/distro/key...\n# ... * the sources.list in a district file -- not simply appended to `/etc/apt/sources.list` -- (there is a newer format [DEB822](https://manpages.debian.org/bookworm/dpkg-dev/deb822.5.en.html)) that references the gpg key.\n# Consider:\n# ````sh\n# curl http://download.something.example.com/$DISTRO/Release.key | \\\n#     gpg --dearmor --yes --output /usr/share/keyrings/something-distro.gpg\n# echo \"deb [signed-by=/usr/share/keyrings/something-distro.gpg] http://download.something.example.com/repositories/home:/$DISTRO ./\" \\\n#     >> /etc/apt/sources.list.d/something-distro.list\n# ````\n\\bapt-key add\\b\n\n# Should be `nearby`\n\\bnear by\\b\n\n# Should be `necessary`\n(?<=\\blonger )needed(?= to\\b)\n\n# Should probably be a person named `Nick` or the abbreviation `NIC`\n\\bNic\\b\n\n# Should be `not supposed`\n\\bsupposed not\\b\n\n# Should be `Once this` or `On this` or even `One that`. Rarely `One, this`\n[?!.] One this\\b\n\n# Should probably be `much more`\n\\bmore much\\b\n\n# Should be `perform its`\n\\bperform it's\\b\n\n# Should be `PowerPoint`\n\\bPowerpoint\\b\n\n# Should be `opt-in`\n(?<!\\scan|for)(?<!\\smust)(?<!\\sif)\\sopt in\\s\n\n# Should be `out-of-date` if acting as an adjective before a noun\n\\bout of date(?= \\w{3,}\\b)\n\n# Should be `less than`\n\\bless then\\b|\\blesser than\\b\n\n# Should be `load balancer`\n\\b[Ll]oud balancer\n\n# Should be `I may be`, or `I, maybe`\n\\bI maybe\\b\n\n# Should be `moot`\n\\bmute point\\b\n\n# Should be `one of`\n(?<!-)\\bon of\\b\n\n# Should be `on the other hand`\n\\b(?i)on another hand\\b\n\n# Reword to `on at runtime` or `enabled at launch`\n# The former if you mean it can be changed dynamically.\n# The latter if you mean that it can be changed without recompiling but not after the program starts.\n\\bswitched on runtime\\b\n\n# Should be `Of course,`\n[?.!]\\s+Of course\\s(?=[-\\w\\s]+[.?;!,])\n\n# Most people only have two hands. Reword.\n\\b(?i)on the third hand\\b\n\n# Should be `Open Graph`\n# unless talking about a specific Open Graph implementation:\n# - Java\n# - Node\n# - Py\n# - Ruby\n\\bOpenGraph\\b\n\n# Should be `OpenShift`\n\\bOpenshift\\b\n\n# Should be `otherwise`\n\\bother[- ]wise\\b\n\n# Should be `; otherwise` or `. Otherwise`\n# https://study.com/learn/lesson/otherwise-in-a-sentence.html\n, [Oo]therwise\\b\n\n# Should probably be `Otherwise,`\n(?<=\\. )Otherwise\\s\n\n# Should be `or (more|less)`\n\\bore (?:more|less)\\b\n\n# Should be `or`\n\\b(?i)true of .*false\\b\n\n# Should be `pale`\n\\b(?<=beyond the )pail\\b\n\n# Should be reworded.\n# `passthrough` is an adjective\n# `pass-through` could be a noun\n# `pass through` would be a verb phrase\n\\b(?i)passthrough(?= an?\\b)\n\n# Should be `rather than`\n\\brather then\\b\n\n# Should be `Red Hat`\n\\bRed[Hh]at\\b\n\n# Should be `regardless, ...` or `regardless of (whether)`\n\\b[Rr]egardless if you\\b\n\n# Should probably be `(s)` if trying to say \"word(s)\"\n# False positive if `/s` is used to mean `per second`\n\\s[a-z]{2,}[a-rt-z]/s\\s(?=[a-oq-z]\\w{2}|p[a-df-z]\\w|pe[a-qs-z]|per[a-z])\n\n# Should be `self-signed`\n\\bself signed\\b\n\n# Should be `SendGrid`\n\\bSendgrid\\b\n\n# Should be `set up` (`setup` is a noun / `set up` is a verb)\n\\b[Ss]etup(?= (?:an?|the|to)\\b)\n\n# Should be `state`\n\\bsate(?=\\b|[A-Z])|(?<=[a-z])Sate(?=\\b|[A-Z])|(?<=[A-Z]{2})Sate(?=\\b|[A-Z])\n\n# Should be `succeed` (or `successfully`)\n(?i)(?<=\\bshould )success\\b\n\n# Should be `times`\n(?<=\\ba few )time\\b\n\n# Should be `that`\n\\b[Tt]hat's(?= would)\n\n# Should be `then` (or occasionally `that`)\n# Original GitHub query: \"/ than the \\w+ must/ NOT /(er|more|less|worse|different\\w*)(?: \\w+|)(,[^.?!]+,|) than the/\"\n# RegEx engines don't tend to appreciate variable length negative bookbehind\n(?<!er|ly)(?<!more|less)(?<!worse)(?<!different)\\sthan(?= the \\w+ must)\n\n# Should be `this`\n\\b[Tt]oday(?= morning\\b)\n\n# Should be `through`\n# unless the next concept is something that is being thrown\n(?i)\\bbl[eo]w thr[eo]w(?! (?:him|her|me|them?|us)(?! in))\\b\n\n# Should be `to use`\n\\bto now use\\b\n\n# Should be `let's` or `let us`\n\\b[Ll]ets (?=throw\\.)\n\n# Should be `no longer needed`\n\\bno more needed\\b(?! than\\b)\n\n# Should be `<see|look> below for the`\n(?i)\\bfind below the\\b\n\n# Should be `then any` unless there's a comparison before the `,`\n, than any\\b\n\n# Should be `did not exist`\n\\bwere not existent\\b\n\n# Should be `not exist` or `nonexistent`\n(?<!(?i)IF|WHERE )\\bnot exists\\b\n\n# Should be `nonexistent`\n\\bnon existing\\b\n\n# Should be `nonexistent`\n\\b[Nn]o[nt][- ]existent\\b\n\n# Should be `only does`\n\\bdoes only(?! (?:seem|match))\\b\n\n# Should be `only matches`\n\\bdoes only match\\b\n\n# Should be `only seems`\n\\bdoes only seem\\b\n\n# Should be `our`\n\\bspending out time\\b\n\n# Should be `@brief` / `@details` / `@param` / `@return` / `@retval`\n(?:^\\s*|(?:\\*|//|/*)\\s+`)[\\\\@](?:breif|(?:detail|detials)|(?:params(?!\\.)|prama?)|ret(?:uns?)|retvl)\\b\n\n# Should be `more than` or `more, then`\n\\bmore then\\b\n\n# Should be `Pipeline`/`pipeline`\n(?:(?<=\\b|[A-Z])p|P)ipeLine(?:\\b|(?=[A-Z]))\n\n# Should be `please do not`/`you do not need`\n[Pp]lease do not need\n\n# Should be `preexisting`\n[Pp]re[- ]existing\n\n# Should be `preempt`\n[Pp]re[- ]empt\\b\n\n# Should be `preemptively`\n[Pp]re[- ]emptively\n\n# Should be `prepopulate`\n[Pp]re[- ]populate\n\n# Should be `prerequisite`\n[Pp]re[- ]requisite\n\n# Should be `principal`\n[Pp]rinciple(?= [Ee]ngineer)\n\n# Should be `QuickTime`\n\\bQuicktime\\b\n\n# Should be `recently changed` or `recent changes`\n[Rr]ecent changed\n\n# Should be `reentrancy`\n[Rr]e[- ]entrancy\n\n# Should be `reentrant`\n[Rr]e[- ]entrant\n\n# Should be `room for`\n\\brooms for (?!lease|rent|sale)\n\n# Reword\n\\b(?i)same as for\\b\n\n# Should be `socioeconomic`\n# https://dictionary.cambridge.org/us/dictionary/english/socioeconomic\nsocio-economic\n\n# Should be `strong suit`\n\\b(?:my|his|her|their) strong suite\\b\n\n# Should probably be `temperatures` unless actually talking about thermal drafts (things birds may fly on)\n\\bthermals\\b\n\n# Should be `there are` or `they are` (or `they're`)\n(?i)\\btheir are\\b\n\n# Should be `they have` or `have the` or ...\n\\bthe (?:have(?! nots)|has)\\b\n\n# Should be `too`\n(?<=\\sit's )to(?= (?:big|large|small|tiny|important))\n\n# Should be `too`\n(?<=\\bway )to(?= many\\s)\n\n# Should be `true`\n(?i)(?<![\\[\\]()])\\brue(?:= or false)\n\n# Should be `understand`\n\\bunder stand\\b\n\n# Should be `upside down`\n\\bup side down\\b\n\n# Should be `URI` or `uri` unless it refers to a person named `Uri` (or a flag)\n(?<![-\\.])\\bUri\\b(?![(])\n\n# Should be `it uses is`\n/\\bis uses is\\b/\n\n# Should be `uses it as`\n(?:^|\\. |and )uses is as (?!an?\\b|follows|livestock|[^.]+\\s+as\\b)\n\n# Should be `was`\n\\bhas been(?= removed in v?\\d)\n\n# Should be `where`\n\\bwere they are\\b\n\n# Should be `whether or not ...`\n\\bwhether(?:\\s\\w+)+ or not\\.\n\n# Should be `why`\n, way(?= is [^.]*\\?)\n\n# Should be `wouldn't`\nwould'nt\n\n# should be `vCenter`\n\\bV[Cc]enter\\b\n\n# Should be `VM`\n\\bVm\\b\n\n# Should be `void`\n(?<=\\bnull and)avoid\\b\n\n# Should be `way too`, `ways to`, or have some extra punctuation\n\\sways too\\s(?=\\w)\n\n# Should be `walkthrough(s)`\n\\bwalk-throughs?\\b\n\n# Should be `want`\n(?<=\\bdon't )ant\\b\n\n# Should be `we'll`\n\\bwe 'll\\b\n\n# Should be `Webex`\n\\bWebEx\\b\n\n# Should be `week`\n# unless you're really talking about people or pointers\n\\bevery weak[.,?!]\n\n# Should be `well`\n\\b[Yy]ou(?:'re| are) doing good\\b\n\n# Should be `well`\n# unless there is a word after good and good is modifying it...\n\\b(?<=[Dd]oing pretty )good(?! stuff\\b)\n\n# Should be `WhatsApp`\nWhatsapp\n\n# Should probably be `when`/`where` or reworded\n\\bat the point\\b\n\n# Should be `whereas`\n\\bwhere as\\b\n\n# Should be `which`\n# unless you're talking about Halloween/D&D/wizards/broomsticks\n\\bwitch\\b\n\n# Should be _verb in infinitive form_\n(?<=\\bwill )(?!(?:(?:b|sp|\\b)ring|cities|focus|its|shed|sometimes|th[iu]s|[a-z]+s (?:be|of))\\b)[a-z]+(?:[a-df-z]ed|ing|[a-rt-xz]s)\\b\n\n# Should be `WinGet`\n\\bWinget\\b\n\n# Should be `without` (unless `out` is a modifier of the next word)\n\\bwith out\\b(?!-)\n\n# Should be `wondering`\n\\bwandering(?= why)\n\n# Should be `work around`\n\\b[Ww]orkaround(?= an?\\b)\n\n# Should be `workarounds`\n\\bwork[- ]arounds\\b\n\n# Should be `workaround`\n(?:(?:[Aa]|[Tt]he|ugly)\\swork[- ]around\\b|\\swork[- ]around\\s+for)\n\n# Should be `worst`\n(?i)worse-case\n\n# Should be `you are not` or reworded\n\\byour not\\b\n\n# Should be `your`\n(?i)\\byou're(?= time\\b(?! traveling))\n\n# Should be `(coarse|fine)-grained`\n\\b(?:coarse|fine) grained\\b\n\n# Homoglyph (Cyrillic) should be `A`/`B`/`C`/`E`/`H`/`I`/`I`/`J`/`K`/`M`/`O`/`P`/`S`/`T`/`Y`\n# It's possible that your content is intentionally mixing Cyrillic and Latin scripts, but if it isn't, you definitely want to correct this.\n(?<=[A-Z]{2})[АВСЕНІӀЈКМОРЅТУ]|[АВСЕНІӀЈКМОРЅТУ](?=[A-Z]+(?:\\b|[a-z]+)|[a-z]+(?:[^a-z]|$))\n\n# Homoglyph (Cyrillic) should be `a`/`b`/`c`/`e`/`o`/`p`/`x`/`y`\n# It's possible that your content is intentionally mixing Cyrillic and Latin scripts, but if it isn't, you definitely want to correct this.\n[авсеорху](?=[A-Za-z]{2,})|(?<=[A-Za-z]{2})[авсеорху]|(?<=[A-Za-z])[авсеорху](?=[A-Za-z])\n\n# Should be `neither/nor` -- or reword\n(?<!do )\\bnot\\b([^.?!\"/(](?!neither|,.*?,))+\\bnor\\b\n\n# Should be `neither/nor` (plus rewording the beginning)\n# This is probably a double negative...\n\\bnot\\b[^.?!\"/(]*\\bneither\\b[^.?!\"/(]*\\bnor\\b\n\n# In English, duplicated words are generally mistakes\n# There are a few exceptions (e.g. \"that that\").\n# If the highlighted doubled word pair is in:\n# * code, write a pattern to mask it.\n# * prose, have someone read the English before you dismiss this error.\n\\s([A-Z]{3,}|[A-Z][a-z]{2,}|[a-z]{3,})\\s\\g{-1}\\s\n"
  },
  {
    "path": ".github/actions/spelling/patterns.txt",
    "content": "# See https://github.com/check-spelling/check-spelling/wiki/Configuration-Examples:-patterns\n\n^(?:Build|)Requires: [-.\\w]+\n\n\"\\w+\"(?= instead of \")\n\nNO_VOWELS:\\s+'.+'\n\n\\b(?:e2e|k3s)(?=[a-z])\n\nHTPASSWD='.+'\n\n\\b(?:azure|docker|kontainer)(?=[a-z])\n\n\\b(?:cluster|node)(?=[a-z]{2})\n\n# IServiceProvider / isAThing\n# - Altered to match `A`+`Foo`\n(?:(?:\\b|_|(?<=[a-z]))A)(?=(?:[A-Z][a-z]{2,})+(?:[A-Z\\d]|\\b))\n\n\\bamazon(?=[a-z])\n\n# Automatically suggested patterns\n\n# hit-count: 1264 file-count: 5\n# ANSI color codes\n(?:\\\\(?:u00|x)1[Bb]|\\\\03[1-7]|\\x1b|\\\\u\\{1[Bb]\\})\\[\\d+(?:;\\d+)*m\n\n# hit-count: 1086 file-count: 175\n# golang print-f-style functions\n(?i)(?<=append|comma|debug|equal|err|error|exit|fatal|format|info|log|name|panic|print|skip|scan|string|trace|true|warn|warning|wrap|write)(?:f|ln)[ (]\n\n# hit-count: 740 file-count: 4\n# sha\nsha\\d+:[0-9a-f]*?[a-f]{3,}[0-9a-f]*\n\n# hit-count: 323 file-count: 135\n# imports\n^import\\s+(?:(?:static|type)\\s+|)(?:[\\w.]|\\{\\s*\\w*?(?:,\\s*(?:\\w*|\\*))+\\s*\\})+(?:\\s+from (['\"]).*?\\g{-1}|)\n\n# hit-count: 247 file-count: 111\n# https/http/file urls\n(?:\\b(?:https?|ftp|file|oci)://)[-A-Za-z0-9+&@#/*%?=~_|!:,.;]+[-A-Za-z0-9+&@#/*%=~_|]\n\n# hit-count: 111 file-count: 20\n# version suffix <word>v#\n(?:(?<=[A-Z]{2})V|(?<=[a-z]{2}|[A-Z]{2})v)\\d+(?:\\b|(?=[a-zA-Z_]))\n\n# hit-count: 77 file-count: 9\n# Library prefix\n# e.g., `lib`+`archive`, `lib`+`raw`, `lib`+`unwind`\n# (ignores some words that happen to start with `lib`)\n(?:\\b|_)[Ll]ib(?!era[lt])(?:re(?=office)|era|)(?!ero|erty|rar(?:i(?:an|es)|y))(?=[a-z])\n\n# hit-count: 50 file-count: 14\n# GitHub actions\n\\buses:\\s+(['\"]?)[-\\w.]+/[-\\w./]+@[-\\w.]+\\g{-1}\n\n# hit-count: 44 file-count: 27\n# node packages\n([\"'])@[^/'\" ]+/[^/'\" ]+\\g{-1}\n\n# hit-count: 40 file-count: 22\n# C network byte conversions\n(?:\\d|\\bh)to(?!ken)(?=[a-z])|to(?=[adhiklpun]\\()\n\n# hit-count: 29 file-count: 10\n# in check-spelling@v0.0.22+, printf markers aren't automatically consumed\n# printf markers\n(?<!\\\\)\\\\n(?=[a-z]{2,})\n\n# hit-count: 28 file-count: 19\n# nolint\nnolint:\\s*[\\w,]+\n\n# hit-count: 14 file-count: 5\n# uuid:\n\\b[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\\b\n\n# hit-count: 12 file-count: 3\n# sha-... -- uses a fancy capture\n(\\\\?['\"]|&quot;)[0-9a-f]{40,}\\g{-1}\n\n# hit-count: 10 file-count: 7\n# hex digits including css/html color classes:\n(?:[\\\\0][xX]|\\\\u\\{?|[uU]\\+|#x?|%23|&H)[0-9_a-fA-FgGrR]*?[a-fA-FgGrR]{2,}[0-9_a-fA-FgGrR]*(?:[uUlL]{0,3}|[iu]\\d+)\\b\n\n# hit-count: 10 file-count: 1\n# css url wrappings\n\\burl\\([^)]+\\)\n\n# hit-count: 9 file-count: 9\n# regex choice\n\\((?:\\?:|)[^)|]+(?<! )\\|(?!(?:jq|xargs)\\b)[^)| ][^)]*\\)\n\n# hit-count: 8 file-count: 2\n# css fonts\n\\bfont(?:-family(?:[-\\w+]*)|):[^;}]+\n\n# hit-count: 7 file-count: 6\n# assign regex\n= /[^*].*?(?:[a-z]{3,}|[A-Z]{3,}|[A-Z][a-z]{2,}).*/[gim]*(?=\\W|$)\n\n# hit-count: 5 file-count: 3\n# go install\ngo install(?:\\s+[a-z]+\\.[-@\\w/.]+)+\n\n# hit-count: 4 file-count: 4\n# tar arguments\n\\b(?:\\\\n|)g?tar(?:\\.exe|)(?:\\s-C \\S+|(?:\\s+--[-a-zA-Z]+|\\s+-[a-zA-Z]+|\\s[ABGJMOPRSUWZacdfh-pr-xz]+\\b)(?:=[^ ]*|))+\n\n# hit-count: 4 file-count: 3\n# hex runs\n\\b(?=(?:[a-fA-F]{0,2}\\d)*[a-fA-F]{3})[0-9a-fA-F]{16,}\\b\n\n# hit-count: 3 file-count: 3\n# Go regular expressions\nregexp?\\.MustCompile\\((?:`[^`]*`|\".*\"|'.*')\\)\n\n# hit-count: 3 file-count: 2\n# Docker images\n^\\s*(?i)FROM\\s+\\S+:\\S+(?:\\s+AS\\s+\\S+|)\n\n# hit-count: 3 file-count: 2\n# Intel intrinsics\n_mm\\d*_(?!dd)\\w+\n\n# hit-count: 2 file-count: 2\n# JavaScript regular expressions\n# javascript exec/test regex\n/.{3,}?/[gim]*\\.(?:exec|test)\\(\n\n# hit-count: 2 file-count: 2\n# container images\nimage: [-\\w./:@]+\n\n# hit-count: 2 file-count: 2\n# Regular expression for word breaks\n\\\\b(?=[a-z]{2}.*/[^>])\n\n# hit-count: 2 file-count: 1\n# iSCSI iqn (approximate regex)\n\\biqn\\.[0-9]{4}-[0-9]{2}(?:[\\.-][a-z][a-z0-9]*)*\\b:[\\w.]+\n\n# hit-count: 1 file-count: 1\n# copyright\nCopyright (?:\\([Cc]\\)|)(?:[-\\d, ]|and)+(?: [A-Z][a-z]+ [A-Z][a-z]+,?)+\n\n# hit-count: 1 file-count: 1\n# IPv6\n\\b(?:[0-9a-fA-F]{0,4}:){3,7}[0-9a-fA-F]{0,4}\\b\n\n# hit-count: 1 file-count: 1\n# javascript match regex\n\\.match\\(/[^/\\s\"]{3,}/[gim]*\\s*\n\n# hit-count: 1 file-count: 1\n# kubernetes crd patterns\n^\\s*pattern: .*$\n\n# hit-count: 1 file-count: 1\n# Windows Resources with accelerators\n\\b[A-Z]&[a-z]+\\b(?!;)\n\n# hit-count: 1 file-count: 1\n# gist github\n\\bgist\\.github\\.com/[^/\\s\"]+/[0-9a-f]+\n\n# hit-count: 1 file-count: 1\n# marker to ignore all code on line\n^.*\\bno-spell-check(?:-line|)(?:\\s.*|)$\n\n# Questionably acceptable forms of `in to`\n# Personally, I prefer `log into`, but people object\n# https://www.tprteaching.com/log-into-log-in-to-login/\n\\b(?:(?:[Ll]og(?:g(?=[a-z])|)|[Ss]ign)(?:ed|ing)?) in to\\b\n\n# to opt in\n\\bto opt in\\b\n\n# typos in translation keys\n^\\s*(?:calllback|objectReferance|requriedInt):$\n\n# pass(ed|ing) in\n\\bpass(?:ed|ing) in\\b\n\n# acceptable duplicates\n# ls directory listings\n[-bcdlpsw](?:[-r][-w][-SsTtx]){3}[\\.+*]?\\s+\\d+\\s+\\S+\\s+\\S+\\s+[.\\d]+(?:[KMGT]|)\\s+\n# mount\n\\bmount\\s+-t\\s+(\\w+)\\s+\\g{-1}\\b\n# C types and repeated CSS values\n\\s(auto|await|buffalo|BUG|center|div|inherit|long|LONG|nobody|none|normal|solid|thin|TODO|transparent|very)(?:\\s\\g{-1})+\\s\n# C enum and struct\n\\b(?:enum|struct)\\s+(\\w+)\\s+\\g{-1}\\b\n# go templates\n\\s(\\w+)\\s+\\g{-1}\\s+\\`(?:graphql|inject|json|yaml):\n# doxygen / javadoc / .net\n(?:[\\\\@](?:brief|defgroup|groupname|link|t?param|return|retval)|(?:public|private|\\[Parameter(?:\\(.+\\)|)\\])(?:\\s+(?:static|override|readonly|required|virtual))*)(?:\\s+\\{\\w+\\}|)\\s+(\\w+)\\s+\\g{-1}\\s\n\n# macOS file path\n(?:Contents\\W+|(?!iOS)/)MacOS\\b\n\n# Python package registry has incorrect spelling for macOS / Mac OS X\n\"Operating System :: MacOS :: MacOS X\"\n\n# \"company\" in Germany\n\\bGmbH\\b\n\n# IntelliJ\n\\bIntelliJ\\b\n\n# Commit message -- Signed-off-by and friends\n^\\s*(?:(?:Based-on-patch|Co-authored|Helped|Mentored|Reported|Reviewed|Signed-off)-by|Thanks-to): (?:[^<]*<[^>]*>|[^<]*)\\s*$\n\n# Autogenerated revert commit message\n^This reverts commit [0-9a-f]{40}\\.$\n\n# ignore long runs of a single character:\n\\b([A-Za-z])\\g{-1}{3,}\\b\n\n# Don't check names in dependabot.yml reviewers section\n^\\s+reviewers:\\s*\\[\\s*\"[^\",]+\"\\s*\\]\n\n# Directives to skip the current full line (intended for extension names and\n# their related account names):\n^.*spellcheck-ignore-line.*$\n\n# Don't check package names\n^\\s*zypper\\b.*\\binstall\\b.*\n\n# Allow golangci in GitHub workflows:\n\\buses:\\s*golangci/golangci-lint-action\\b\n\n# on macOS, MacOS is used for the internal folder name within an app...\n([\"/])MacOS\\g{-1}\n\n# GitHub owner names should not be checked (dependency scripts)\n\\bgithubOwner\\s*=\\s*'.*?'\n\n# Win32 constants\n\\bGWL_EXSTYLE\\b\n\\bHWND_NOTOPMOST\\b\n\\bSWP_NOMOVE\\b\n\\bSWP_NOSIZE\\b\n\n# allow repetitive words in iptable rules\nDNAT\\s+.*\\s+anywhere anywhere\n\n# Image names\n\\bghcr\\.io/[A-Za-z0-9_/.-]+(?::[A-Za-z0-9_.-]+)?\\b\n"
  },
  {
    "path": ".github/actions/spelling/reject.txt",
    "content": "attache\naroynt.*\nbellows?\nbenefitting\noccurences?\n.*dnt\ndependan.*\ndevelopement\ndevelopp?e\nDevers?\ndevex.*\ndevide\nDevinn?[ae]\ndevisals?\ndevisors?\ndiables?\nhasta?\nhastat.*\nimmediatly\ninisle\ninital\nlinge\noer\nSorce\n[Ss]pae.*\nTeh\nuntill\nuntilling\nvenders?\nwether.*\n"
  },
  {
    "path": ".github/actions/yarn-install/action.yaml",
    "content": "name: Yarn Install\ndescription: >-\n  This is a composite action that does everything needed to do `yarn install`.\n\nruns:\n  using: composite\n  steps:\n  # In case we're running on a self-hosted runner without `yarn` installed,\n  # set up NodeJS, enable `yarn`, and then handle the caching.\n  - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n    with:\n      node-version-file: package.json\n  - run: corepack enable yarn\n    shell: bash\n  - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n    with:\n      node-version-file: package.json\n      cache: yarn\n\n  - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n    with:\n      go-version-file: go.work\n      cache-dependency-path: src/go/**/go.sum\n\n  - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0\n    with:\n      python-version: '3.x'\n      cache: pip\n  - run: pip install setuptools\n    shell: bash\n\n  - name: Install Windows dependencies\n    if: runner.os == 'Windows'\n    shell: powershell\n    run: .\\scripts\\windows-setup.ps1 -SkipVisualStudio -SkipTools\n\n  - name: Flag build for M1\n    if: runner.os == 'macOS' && runner.arch == 'ARM64'\n    run: echo \"M1=1\" >> \"${GITHUB_ENV}\"\n    shell: bash\n\n  - run: yarn install --immutable\n    shell: bash\n\n  - name: Fix electron sandbox\n    if: runner.os == 'Linux'\n    shell: bash\n    run: |\n      sudo chown root node_modules/electron/dist/chrome-sandbox\n      sudo chmod 04755 node_modules/electron/dist/chrome-sandbox\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n\n# Maintain dependencies for GitHub Actions\n- package-ecosystem: \"github-actions\"\n  directories:\n  - \"/\"\n  - \"/.github/actions/*/\"\n  schedule:\n    interval: \"daily\"\n  cooldown:\n    default-days: 7\n  open-pull-requests-limit: 12\n  labels: [\"component/dependencies\"]\n\n# Maintain dependencies for npm\n- package-ecosystem: \"npm\"\n  directory: \"/\"\n  schedule:\n    interval: \"daily\"\n  cooldown:\n    default-days: 7\n  open-pull-requests-limit: 12\n  labels: [\"component/dependencies\"]\n  ignore:\n  - # Needs to be updated along with NodeJS version.\n    dependency-name: \"@types/node\"\n    update-types: [version-update:semver-major]\n  - # We don't utilize @rancher/shell in a meaningful way. It is safe to\n    # ignore until we arrive a solution that uses it.\n    dependency-name: \"@rancher/shell\"\n    versions: [\">0.1\"]\n\n# Maintain dependencies for Golang\n- package-ecosystem: \"gomod\"\n  directories:\n  - \"/src/go/*\"\n  - \"/src/go/**/*\"\n  - \"/scripts\"\n  schedule:\n    interval: \"daily\"\n# cooldown:\n#   default-days: 7\n  open-pull-requests-limit: 12\n  labels: [\"component/dependencies\"]\n  ignore:\n  - # Swagger dependencies must match whatever the CLI generate.\n    dependency-name: \"github.com/go-openapi/swag\"\n  groups:\n    golang-x:\n      patterns: [\"golang.org/x/*\"]\n    k8s:\n      patterns: [\"k8s.io/*\"]\n"
  },
  {
    "path": ".github/workflows/bats/get-tests.py",
    "content": "#!/usr/bin/env python3\n\n# This script determines the tests to be run.\n# Inputs (as environment variables, all are space-separated):\n# TESTS The set of tests to run (e.g. \"*\", \"containers k8s\")\n# PLATFORMS The set of platforms (e.g. \"linux mac\")\n# ENGINES The set of engines (e.g. \"containerd moby\")\n# KUBERNETES_VERSION The default Kubernetes version to use\n# KUBERNETES_ALT_VERSION Alternative Kubernetes version for coverage\n# The working directory must be the \"bats/tests/\" folder\n\nimport dataclasses\nimport glob\nimport json\nfrom operator import attrgetter\nimport os\nimport sys\nfrom typing import Iterator, List, Literal, get_args\n\nPlatforms = Literal[\"linux\", \"mac\", \"win\"]\nHosts = Literal[\"ubuntu-latest\", \"macos-15-intel\", \"windows-latest\"]\nEngines = Literal[\"containerd\", \"moby\"]\n\n@dataclasses.dataclass\nclass Result:\n    \"\"\"\n    A Result describes a test run, which is a matrix entry.\n    \"\"\"\n    # The name of the test; either a directory or a file name (without extension)\n    name: str\n    host: Hosts\n    engine: Engines\n    # The version of k3s to test\n    k3sVersion: str\n    # A different Kubernetes version, for testing upgrades.\n    k3sAltVersion: str\n\n    key = staticmethod(attrgetter(\"name\", \"host\", \"engine\"))\n\ndef resolve_test(test: str, platform: Platforms) -> Iterator[str]:\n    \"\"\"\n    Given a test spec, convert that to a list of tests.\n    \"\"\"\n    # If we can't glob the test, use it as-is.\n    for test in glob.glob(test) or (test,):\n        if platform == \"mac\" and test == \"k8s\":\n            # The macOS runners on CI are extra slow; for this test suite,\n            # run each test individually.\n            for name in glob.glob(\"k8s/*.bats\"):\n                yield name.removesuffix(\".bats\")\n        else:\n            yield test.removesuffix(\".bats\")\n\ndef skip_test(test: Result) -> bool:\n    \"\"\"\n    Check if a given test should be skipped.\n    We skip some tests because the CI machines can't handle them.\n    \"\"\"\n    if test.host == \"macos-15-intel\" and test.name.startswith(\"k8s/\"):\n        # The macOS CI runners are slow; skip some tests that can be tested on\n        # other OSes.\n        skipped_tests = (\"verify-cached-images\",)\n        if any(test.name == f\"k8s/{t}\" for t in skipped_tests):\n            return True\n    return False\n\nresults: List[Result] = list()\nerrors: bool = False\n\nfor test in (os.environ.get(\"TESTS\", None) or \"*\").split():\n    platforms: List[Platforms] = os.environ.get(\"PLATFORMS\", \"\").split() or get_args(Platforms)\n    engines: List[Engines] = os.environ.get(\"ENGINES\", \"\").split() or get_args(Engines)\n    for platform in platforms:\n      host: Hosts = {\n         \"linux\": \"ubuntu-latest\",\n         \"mac\": \"macos-15-intel\",\n         \"win\": \"windows-latest\",\n      }[platform]\n      for name in resolve_test(test, platform):\n          for engine in engines:\n            if os.access(name, os.R_OK):\n              pass\n            elif os.access(f\"{name}.bats\", os.R_OK):\n              name = f\"{name}.bats\"\n            else:\n              errors = True\n              print(f\"Failed to find test {name}\", file=sys.stderr)\n              continue\n\n            # To get some coverage of different Kubernetes versions, pick the\n            # version depending on the container engine; one gets the old version\n            # we previously tested, the other gets the maximum version\n            # of k3s that is supported by the Rancher helm chart.  These values\n            # come from the environment.\n            k3sVersion = os.environ.get(\"KUBERNETES_VERSION\", \"\")\n            k3sAltVersion = os.environ.get(\"KUBERNETES_ALT_VERSION\", \"\")\n            if k3sVersion == \"\" or k3sAltVersion == \"\":\n               raise \"Either KUBERNETES_VERSION or KUBERNETES_ALT_VERSION is unset\"\n            if engine == \"containerd\":\n              (k3sAltVersion, k3sVersion) = (k3sVersion, k3sAltVersion)\n\n            result = Result(name=name, host=host, engine=engine,\n                            k3sVersion=k3sVersion, k3sAltVersion=k3sAltVersion)\n            if not skip_test(result):\n                results.append(result)\n\ndicts = [dataclasses.asdict(x) for x in sorted(results, key=Result.key)]\n\noutput = os.environ.get(\"GITHUB_OUTPUT\", None)\nif output is not None:\n  with open(output, \"a\") as file:\n    print(f\"tests={json.dumps(dicts)}\", file=file)\n\njson.dump(dicts, sys.stdout, indent=2)\n\nif errors:\n    raise FileNotFoundError(\"Some tests were not found\")\n"
  },
  {
    "path": ".github/workflows/bats/sanitize-artifact-name.sh",
    "content": "#!/bin/bash\nset -o errexit -o nounset -o pipefail\n\n# GitHub restricts artifact filenames:\n\n# Invalid characters include: Double quote \", Colon :, Less than <,\n# Greater than >, Vertical bar |, Asterisk *, Question mark ?, Carriage\n# return \\r, Line feed \\n\n#\n# The following characters are not allowed in files that are uploaded\n# due to limitations with certain file systems such as NTFS. To maintain\n# file system agnostic behavior, these characters are intentionally not\n# allowed to prevent potential problems with downloads on different file\n# systems.\n\n# By default, this script takes a string on standard input and outputs the\n# sanitized string on standard output.  If any positional parameters are given,\n# it instead treats them as file names to (recursively) rename.\n\nsanitize() {\n    local new=$1\n    new=${new//\\\"/%22}\n    new=${new//:/%3A}\n    new=${new//</%3C}\n    new=${new//>/%3E}\n    new=${new//|/%7C}\n    new=${new//\\*/%2A}\n    new=${new//\\?/%3F}\n    new=${new//$'\\r'/}\n    new=${new//$'\\n'/}\n    echo \"$new\"\n}\n\nif [[ ${#@} -lt 1 ]]; then\n    # No arguments; sanitize standard input.\n    sanitize \"$(cat)\"\n    exit\nfi\n\n# Find all files and put the names into the FILES array.\n# We don't rename inside the loop to make sure the find command has\n# finished before we modify any directories it is iterating over.\nFILES=()\nfor PARAM in \"$@\"; do\n    while read -d $'\\0' -r FILE; do\n        FILES+=(\"$FILE\")\n    done < <(find \"$PARAM\" -type f -print0)\ndone\n\nfor FILE in \"${FILES[@]}\"; do\n    NEW=\"$(sanitize \"$FILE\")\"\n    if [[ $FILE != \"$NEW\" ]]; then\n        echo \"$NEW\"\n        mv \"$FILE\" \"$NEW\"\n    fi\ndone\n"
  },
  {
    "path": ".github/workflows/bats/summarize.mjs",
    "content": "// This file creates the summary table at the end of the run.\n//\n// Inputs:\n//   */version.txt   -- The version of Rancher Desktop tested\n//   */name.txt      -- The test suite that was ran\n//   */os.txt        -- The OS the test was run on\n//   */engine.txt    -- The container engine used\n//   */log-name.txt  -- The name of the logs artifact\n//   */report.tap    -- The results\n// Environment:\n//   GITHUB_API_URL, GITHUB_RUN_ID, GITHUB_REPOSITORY, GITHUB_SERVER_URL\n//     See https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables\n//   GITHUB_TOKEN\n//     GitHub authorization token.\n\n// @ts-check\nimport fs from 'fs';\nimport path from 'path';\n\n/**\n * Define interface for emitting one line of output.\n * @typedef {(line: string) => unknown} OutputMethod\n */\n\nclass Run {\n  /** Contents of version.txt. */\n  versionData = '';\n  /** Contents of name.txt. */\n  name = '';\n  /** Contents of os.txt. */\n  os = '';\n  /** Contents of engine.txt. */\n  engine = '';\n  /** Contents of log-name.txt. */\n  logName = '';\n  /** Total number of tests. */\n  total = 0;\n  /** Number of tests passed (not skipped). */\n  passed = 0;\n  /** Number of tests skipped. */\n  skipped = 0;\n  /** Number of tests failed. */\n  failed = 0;\n  /** Job ID; this may not be set. */\n  id = 0;\n  /** ID for the logs artifact; might be missing. */\n  logId = 0;\n  /** Number of tests passed or skipped. */\n  get ok() { return this.passed + this.skipped };\n  /** Whether this run succeeded. */\n  get succeeded() { return this.ok == this.total };\n  /** Version string for this run. */\n  get version() {\n    let v = this.versionData;\n    for (const prefix of ['Rancher Desktop-', 'rancher-desktop-', 'Rancher.Desktop.Setup.']) {\n      if (v.startsWith(prefix)) {\n        v = v.substring(prefix.length);\n      }\n    }\n    const suffixes = ['.msi'];\n    for (const platform of ['linux', 'arm64-mac', 'mac', 'win']) {\n      suffixes.push(`-${ platform }.zip`);\n    }\n    for (const suffix of suffixes) {\n      if (v.endsWith(suffix)) {\n        v = v.substring(0, v.length - suffix.length);\n      }\n    }\n    return v;\n  }\n  /** The column for this run. */\n  get column() { return `${ this.os } ${ this.engine }` }\n}\n\n/**\n * Read the runs in the current directory.\n * @returns {Promise<Run[]>}\n */\nasync function readRuns() {\n  /** @type Run[] */\n  const runs = [];\n\n  for (const entry of await fs.promises.readdir('.', { withFileTypes: true })) {\n    if (!entry.isDirectory()) {\n      continue;\n    }\n    try {\n      /**\n       * Return the contents of a file relative to the entry directory.\n       * @param {string} relPath The name of the file to read.\n       * @returns {Promise<string>} Trimmed contents of the file.\n       */\n      async function readFile(relPath) {\n        const fullPath = path.join(entry.name, relPath);\n        return (await fs.promises.readFile(fullPath, { encoding: 'utf-8' })).trimEnd();\n      }\n      const run = new Run();\n\n      run.versionData = await readFile('version.txt');\n      run.name = await readFile('name.txt');\n      run.os = await readFile('os.txt');\n      run.engine = await readFile('engine.txt');\n      run.logName = await readFile('log-name.txt');\n\n      const report = await fs.promises.open(path.join(entry.name, 'report.tap'));\n\n      for await (const line of report.readLines()) {\n        if (line.startsWith('1..')) {\n          run.total = parseInt(line.substring(3), 10);\n        } else if (line.toLowerCase().includes(' # skip')) {\n          run.skipped++;\n        } else if (line.startsWith('ok ')) {\n          run.passed++;\n        } else if (line.startsWith('no ok ')) {\n          run.failed++;\n        }\n      }\n      runs.push(run);\n    } catch (ex) {\n      // We might be reading `.git`, `.github`, etc; don't abort if we failed to\n      // read anything, but record the error for debugging purposes.\n      console.error(`Failed to read ${ entry.name }:`, ex);\n    }\n  }\n\n  // We don't have job ID and artifact ID from the recorded data (because those\n  // are not available to the jobs as they are run); try to fetch them.\n  await updateRunInfo(runs);\n\n  return runs;\n}\n\n/**\n * Print the version string table.\n * @param {Run[]} runs The runs collected.\n * @param {OutputMethod} output Function to output a line.\n */\nasync function printVersions(runs, output) {\n  /** @type Set<string> */\n  const versions = new Set();\n  for (const run of runs) {\n    versions.add(run.version);\n  }\n  output('Versions\\n---');\n  for (const version of Array.from(versions).sort()) {\n    output('`' + version + '`');\n  }\n  output('');\n}\n\n/**\n * Minimal structure of a /jobs API return.\n * @typedef {Object} GitHubWorkflowJobList\n * @property {GitHubWorkflowRunJob[]} jobs\n */\n/**\n * @typedef {Object} GitHubWorkflowRunJob\n * @property {number} id\n * @property {string} name\n */\n/**\n * Minimal structure of a /artifacts API return.\n * @typedef {Object} GitHubWorkflowArtifactsList\n * @property {GitHubWorkflowArtifact[]} artifacts\n */\n/**\n * @typedef {Object} GitHubWorkflowArtifact\n * @property {number} id\n * @property {string} name\n */\n\n/**\n * Fetch GitHub metadata about the current run.\n * @param {'jobs' | 'artifacts'} infoType The information to get.\n * @returns {Promise<any | undefined>} The data from API, or undefined.\n */\nasync function getRunMetadata(infoType) {\n  const { env } = process;\n  const variables = [\n    'GITHUB_API_URL', 'GITHUB_RUN_ID', 'GITHUB_REPOSITORY',\n  ];\n  for (const variable of variables) {\n    if (!(variable in env)) {\n      console.error(`${ variable } not set, skipping GitHub API calls`);\n      return;\n    }\n  }\n  const url = `${ env.GITHUB_API_URL }/repos/${ env.GITHUB_REPOSITORY }/actions/runs/${ env.GITHUB_RUN_ID }/${ infoType }?per_page=100`;\n  /** @type Record<string, string> */\n  const headers = {};\n\n  if ('GITHUB_TOKEN' in env) {\n    headers.Authorization = `Bearer ${ env.GITHUB_TOKEN }`;\n  }\n  const response = await fetch(url, { headers })\n\n  if (!response.ok) {\n    throw new Error(`Failed to get GitHub ${ infoType } info:` + await response.text());\n  }\n  return await response.json();\n}\n\n/**\n * Update runs in place with metadata from GitHub.\n * @param {Run[]} runs The runs to modify.\n */\nasync function updateRunInfo(runs) {\n  /** @type GitHubWorkflowJobList | undefined */\n  const jobInfo = await getRunMetadata('jobs');\n  if (jobInfo) {\n    // Parse the info to get a list of job matrix values to job ID.\n    // Because there may be more values than the ones we're looking for, we can't\n    // just make it a Map.\n    const jobMap = jobInfo.jobs.map(job => {\n      const name = (/\\((.*)\\)/.exec(job.name) ?? [])[1];\n      const vals = new Set((name?.split(',') ?? []).map(n => n.trim()));\n      return /** @type {const} */([vals, job.id]);\n    });\n\n    for (const run of runs) {\n      const [, id]= jobMap.find(([vals]) => {\n        return vals.has(run.name) && vals.has(run.os) && vals.has(run.engine);\n      }) ?? [];\n      if (id) {\n        run.id = id;\n      }\n    }\n  }\n\n  /** @type GitHubWorkflowArtifactsList | undefined */\n  const artifactInfo = await getRunMetadata('artifacts');\n  if (artifactInfo) {\n    const artifactMap = Object.fromEntries(artifactInfo.artifacts.map(a => [a.name, a.id]));\n    for (const run of runs) {\n      if (run.logName in artifactMap) {\n        run.logId = artifactMap[run.logName];\n      }\n    }\n  }\n}\n\n/**\n * Print the result table\n * @param {Run[]} runs The runs collected\n * @param {OutputMethod} output Function to output a line\n */\nasync function printResults(runs, output) {\n  if (!process.env.EXPECTED_TESTS) {\n    throw new Error('EXPECTED_TESTS was not set');\n  }\n  /** @type {{name: string, host: string, engine: string}[]} */\n  const expectedTests = JSON.parse(process.env.EXPECTED_TESTS);\n  const expectedNames = Array.from(new Set(expectedTests.map(t => t.name))).sort();\n  const expectedHosts = Array.from(new Set(expectedTests.map(t => t.host))).sort();\n  const expectedColumns = expectedHosts.map(host => {\n    const engines = new Set(expectedTests.filter(t => t.host === host).map(t => t.engine));\n    return Array.from(engines).sort().map(engine => [host, engine]);\n  }).flat(1);\n\n  output(['Name', ...expectedColumns.map(parts => parts.join(' '))].join(' | '));\n  output(['', ...expectedColumns].map(() => '---').join(' | '));\n\n  for (const name of expectedNames) {\n    const row = [name];\n    for (const [host, engine] of expectedColumns) {\n      const run = runs.find(r => r.name === name && r.os === host && r.engine === engine);\n      const expected = expectedTests.find(t => t.name === name && t.host === host && t.engine === engine);\n\n      if (run) {\n        const emoji = run.succeeded ? ':white_check_mark:' : ':x:';\n        const count  = run.succeeded ? '' : `${ run.ok }/${ run.total }`;\n        let tooltip = '';\n        tooltip += run.passed ? `${ run.passed } passed ` : '';\n        tooltip += run.failed ? `${ run.failed } failed ` : '';\n        tooltip += run.skipped ? `${ run.skipped } skipped ` : '';\n        tooltip += `out of ${ run.total }`;\n        const { env } = process;\n        let result = '';\n        if (run.logId) {\n          const url = `${ env.GITHUB_SERVER_URL }/${ env.GITHUB_REPOSITORY }/actions/runs/${ env.GITHUB_RUN_ID}/artifacts/${ run.logId }`;\n          result += `<a href=\"${ url }\" title=\"Download logs\">:file_folder:</a> `;\n        }\n        result += `<a title=\"${ tooltip }\"`;\n        if (run.id) {\n          const url = `${ env.GITHUB_SERVER_URL }/${ env.GITHUB_REPOSITORY }/actions/runs/${ env.GITHUB_RUN_ID }/job/${ run.id }`;\n          result += ` href=\"${ url }\"`;\n        }\n        result += `>${ emoji } ${ count }</a>`;\n        row.push(result);\n      } else if (expected) {\n        // The test result is missing for this run.\n        row.push('<a title=\"run results missing\">:x: ??</a>');\n      } else {\n        // This combination is not run.\n        row.push('');\n      }\n    }\n    output(row.join(' | '));\n  }\n}\n\n(async() => {\n  const runs = await readRuns();\n\n  for (const run of runs) {\n    console.log(run);\n  }\n  /** @type {OutputMethod} */\n  let output = console.log;\n  if (process.env.GITHUB_STEP_SUMMARY) {\n    const file = await fs.promises.open(process.env.GITHUB_STEP_SUMMARY, 'a');\n    output = (line) => file.write(line + '\\n');\n  }\n  await printVersions(runs, output);\n  await printResults(runs, output);\n})().catch(ex => {\n  console.error(ex);\n  process.exit(1);\n});\n"
  },
  {
    "path": ".github/workflows/bats.yaml",
    "content": "name: BATS\non:\n  workflow_dispatch:\n    inputs:\n      owner:\n        description: Override owner (e.g. rancher-sandbox)\n        type: string\n      repo:\n        description: Override repository (e.g. rancher-desktop)\n        type: string\n      branch:\n        description: Override branch (e.g. main, or PR#)\n        type: string\n      tests:\n        description: 'Tests (in the tests/ directory, e.g. \"containers\")'\n        default: '*'\n        type: string\n      platforms:\n        description: Platforms to run\n        default: 'linux mac win'\n        type: string\n      engines:\n        description: Container engines to run\n        default: 'containerd moby'\n        type: string\n      kubernetes-version:\n        description: Primary Kubernetes version to test\n        default: '1.22.7' # Must also change in calculate step\n        type: string\n      kubernetes-alt-version:\n        description: Secondary Kubernetes version to test (e.g. for upgrades)\n        default: '1.28.11' # Must also change in calculate step\n        type: string\n      package-id:\n        description: Package run ID override; leave empty to use latest.\n        default: ''\n        type: string\n      experimental:\n        description: Run with experimental settings (WSL)\n        default: false\n        type: boolean\n  schedule:\n  - cron: '0 8 * * 1-5' # 8AM UTC weekdays as a baseline\n\npermissions:\n  contents: read\n\nenv:\n  GH_OWNER:      ${{ github.repository_owner }}\n  GH_REPOSITORY: ${{ github.repository }}\n  GH_REF_NAME:   ${{ github.ref_name }}\n\njobs:\n  get-tests:\n    name: Calculate tests to run\n    runs-on: ubuntu-latest\n    steps:\n    - name: Fetch install script\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        sparse-checkout-cone-mode: false\n        sparse-checkout: |\n          scripts/install-latest-ci.sh\n          .github/workflows/bats/get-tests.py\n\n    - id: repo\n      name: Calculate short repository\n      run: echo \"repo=${GITHUB_REPOSITORY#*/}\" >> \"$GITHUB_OUTPUT\"\n\n    - name: Fetch tests\n      run: |\n        : ${OWNER:=$GH_OWNER}\n        : ${REPO:=${GH_REPOSITORY#$GH_OWNER/}}\n        : ${BRANCH:=$GH_REF_NAME}\n        # If BRANCH is a number, assume it is supposed to be a PR\n        [[ $BRANCH =~ ^[0-9]+$ ]] && export PR=$BRANCH\n        \"scripts/install-latest-ci.sh\"\n      env:\n        GH_TOKEN:     ${{ github.token }}\n        OWNER:        ${{ inputs.owner || github.repository_owner }}\n        REPO:         ${{ inputs.repo || steps.repo.outputs.repo }}\n        BRANCH:       ${{ inputs.branch || github.ref_name }}\n        ID:           ${{ inputs.package-id }}\n        BATS_DIR:     ${{ github.workspace }}/bats\n        INSTALL_MODE: skip\n\n    - name: Calculate tests\n      id: calculate\n      # This script is not inline to make local testing easier\n      run: python3 ${{ github.workspace }}/.github/workflows/bats/get-tests.py\n      env:\n        TESTS:                  ${{ inputs.tests }}\n        PLATFORMS:              ${{ inputs.platforms }}\n        ENGINES:                ${{ inputs.engines }}\n        KUBERNETES_VERSION:     ${{ inputs.kubernetes-version || '1.22.7' }}\n        # rancher/rancher helm chart 2.8.5 supports up to 1.28.*\n        KUBERNETES_ALT_VERSION: ${{ inputs.kubernetes-alt-version || '1.28.11' }}\n      working-directory: bats/tests\n    outputs:\n      repo: ${{ steps.repo.outputs.repo }}\n      tests: ${{ steps.calculate.outputs.tests }}\n\n  bats:\n    needs: get-tests\n    strategy:\n      fail-fast: false\n      matrix:\n        include: ${{ fromJSON(needs.get-tests.outputs.tests )}}\n    runs-on: ${{ matrix.host }}\n    steps:\n    - name: Fetch install script\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        sparse-checkout-cone-mode: false\n        sparse-checkout: |\n          scripts/install-latest-ci.sh\n          .github/actions/setup-environment/action.yaml\n          .github/workflows/bats/sanitize-artifact-name.sh\n\n    - name: Install latest CI build\n      run: |\n        : ${OWNER:=$GH_OWNER}\n        : ${REPO:=${GH_REPOSITORY#$GH_OWNER/}}\n        : ${BRANCH:=$GH_REF_NAME}\n        # If BRANCH is a number, assume it is supposed to be a PR\n        [[ $BRANCH =~ ^[0-9]+$ ]] && export PR=$BRANCH\n        scripts/install-latest-ci.sh\n      shell: bash\n      env:\n        GH_TOKEN:     ${{ github.token }}\n        OWNER:        ${{ inputs.owner || github.repository_owner }}\n        REPO:         ${{ inputs.repo || needs.get-tests.outputs.repo }}\n        BRANCH:       ${{ inputs.branch || github.ref_name }}\n        ID:           ${{ inputs.package-id }}\n        BATS_DIR:     ${{ github.workspace }}/bats\n        INSTALL_MODE: installer\n        ZIP_NAME:     ${{ github.workspace }}/version.txt\n        RD_LOCATION:  system\n\n    - name: Set up environment\n      uses: ./.github/actions/setup-environment\n\n    - name: \"Linux: Install prerequisites\"\n      if: runner.os == 'Linux'\n      run: >-\n        sudo DEBIAN_FRONTEND=noninteractive\n        apt-get install coreutils\n\n    - name: \"macOS: Install prerequisites\"\n      if: runner.os == 'macOS'\n      shell: bash\n      run: brew install --force bash coreutils\n\n    - name: \"Windows: Install WSL2 Distribution\"\n      if: runner.os == 'Windows'\n      shell: pwsh\n      run: |\n        # Install a \"modern\" WSL distribution\n        wsl --install --no-launch --web-download --name openSUSE openSUSE-Leap-15.6\n        # Prevent first-boot from running\n        wsl --distribution openSUSE --user root --exec sed -i -e '/^command/d' /etc/wsl-distribution.conf\n        # Create the initial user\n        wsl --distribution openSUSE --user root --exec /usr/sbin/useradd --create-home --uid 1000 user\n\n    - name: \"Windows: Install prerequisites in WSL\"\n      if: runner.os == 'Windows'\n      shell: pwsh\n      run: >-\n        wsl.exe --distribution openSUSE --user root --exec\n        zypper --non-interactive install\n        curl\n        util-linux\n\n    - name: \"Windows: Enable experimental WSL settings\"\n      if: runner.os == 'Windows' && inputs.experimental\n      shell: pwsh\n      run: |\n        Set-Content -Encoding UTF8NoBOM -Path \"${HOME}/.wslconfig\" -Value @\"\n        ; Note that not all settings here make sense together.\n        [wsl]\n        dnsProxy=false\n        ; networkingMode=mirrored ; https://github.com/rancher-sandbox/rancher-desktop/issues/6665\n        firewall=true\n        dnsTunneling=true\n        autoProxy=true\n        [experimental]\n        autoMemoryReclaim=gradual\n        sparseVhd=true\n        useWindowsDnsCache=true\n        bestEffortDnsParsing=true\n        hostAddressLoopback=true\n        \"@\n\n    - name: Set log directory\n      shell: bash\n      run: |\n        echo \"LOGS_DIR=$(pwd)/logs\" >> \"$GITHUB_ENV\"\n        mkdir logs\n    - name: \"Windows: Override log directory\"\n      if: runner.os == 'Windows'\n      shell: powershell\n      run: >-\n        wsl.exe --distribution openSUSE -- echo 'LOGS_DIR=$(pwd)'\n        | Out-File -Encoding ASCII -Append \"$ENV:GITHUB_ENV\"\n      working-directory: logs\n\n    - name: Normalize test name\n      id: normalize\n      shell: bash\n      run: |\n        t=\"${{ matrix.name }}\"\n        if [[ ! -r \"tests/$t\" ]] && [[ -r \"tests/${t}.bats\" ]]; then\n          t=\"${t}.bats\"\n        fi\n        echo \"test=$t\" >> \"$GITHUB_OUTPUT\"\n      working-directory: bats\n\n    - name: \"macOS: Set startup command\"\n      if: runner.os == 'macOS'\n      run: echo \"BATS_COMMAND=$BATS_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        BATS_COMMAND: exec\n    - name: \"Linux: Set startup command\"\n      if: runner.os == 'Linux'\n      run: echo \"BATS_COMMAND=$BATS_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        BATS_COMMAND: >-\n          exec xvfb-run --auto-servernum\n          --server-args='-screen 0 1280x960x24'\n    - name: \"Windows: Set startup command\"\n      if: runner.os == 'Windows'\n      shell: bash\n      run: echo \"BATS_COMMAND=$BATS_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        BATS_COMMAND: wsl.exe --distribution openSUSE --exec\n\n    - name: Run BATS\n      # We use ${{ env.BATS_COMMAND }} instead of ${BATS_COMMAND} to let the\n      # shell parse the command, instead of doing it via expansion which is then\n      # parsed differently (--server-args isn't kept as one word).  Also, we\n      # need to use the env.* form because PowerShell uses ${ENV:VAR} instead.\n      run: >-\n        ${{ env.BATS_COMMAND }}\n        ./bats-core/bin/bats\n        --gather-test-outputs-in '${{ env.LOGS_DIR }}'\n        --print-output-on-failure\n        --filter-tags '!ci-skip'\n        --formatter cat\n        --report-formatter tap\n        'tests/${{ steps.normalize.outputs.test }}'\n      env:\n        BATS_COMMAND:              ${{ env.BATS_COMMAND }}\n        GITHUB_TOKEN:              ${{ github.token }}\n        LOGS_DIR:                  ${{ env.LOGS_DIR }}\n        RD_CAPTURE_LOGS:           \"true\"\n        RD_CONTAINER_ENGINE:       ${{ matrix.engine }}\n        RD_KUBERNETES_VERSION:     ${{ matrix.k3sVersion }}\n        RD_KUBERNETES_ALT_VERSION: ${{ matrix.k3sAltVersion }}\n        RD_TAKE_SCREENSHOTS:       \"true\"\n        RD_TRACE:                  \"true\"\n        RD_USE_GHCR_IMAGES:        \"true\"\n        RD_USE_RAMDISK:            \"true\"\n        RD_USE_WINDOWS_EXE:        \"${{ runner.os == 'Windows' }}\"\n        WSLENV: \"\\\n          GITHUB_TOKEN:\\\n          RD_CAPTURE_LOGS:\\\n          RD_CONTAINER_ENGINE:\\\n          RD_KUBERNETES_VERSION:\\\n          RD_KUBERNETES_ALT_VERSION:\\\n          RD_TAKE_SCREENSHOTS:\\\n          RD_TRACE:\\\n          RD_USE_GHCR_IMAGES:\\\n          RD_USE_RAMDISK:\\\n          RD_USE_WINDOWS_EXE:\\\n          \"\n      working-directory: bats\n      timeout-minutes: 120\n\n    - name: Calculate log name\n      id: log_name\n      if: ${{ !cancelled() }}\n      run: |\n        name=\"$(.github/workflows/bats/sanitize-artifact-name.sh <<< \"$name\")\"\n        # For the artifact name, backslash and forward slash are also invalid.\n        name=${name//\\\\/%3C}\n        name=${name//\\//%2F}\n        echo \"name=$name\" >>\"$GITHUB_OUTPUT\"\n      shell: bash\n      env:\n        name: ${{ matrix.host }}-${{ matrix.engine }}-${{ matrix.name }}.logs\n\n    - name: Consolidate logs\n      if: ${{ !cancelled() }}\n      run: |\n        # bats/logs may not exist if the workflow is being tested with e.g. tests/helpers/utils.bats\n        if [ -d \"bats/logs\" ]; then\n            cp -R \"bats/logs/\" logs\n        fi\n        cp \"bats/report.tap\" logs\n        .github/workflows/bats/sanitize-artifact-name.sh logs\n        echo \"$NAME\" > logs/name.txt\n        echo \"$OS\" > logs/os.txt\n        echo \"$ENGINE\" > logs/engine.txt\n        echo \"$LOG_NAME\" > logs/log-name.txt\n        mv version.txt logs/\n      shell: bash\n      env:\n        NAME: ${{ matrix.name }}\n        OS: ${{ matrix.host }}\n        ENGINE: ${{ matrix.engine }}\n        LOG_NAME: ${{ steps.log_name.outputs.name }}\n\n    - name: Upload logs\n      if: ${{ !cancelled() }}\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: ${{ steps.log_name.outputs.name }}\n        path: logs/\n        if-no-files-found: error\n\n  summarize:\n    name: Summarize output\n    needs: [ get-tests, bats ]\n    if: ${{ !cancelled() }}\n    runs-on: ubuntu-latest\n    steps:\n    - name: Fetch summarizer script\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        sparse-checkout-cone-mode: false\n        sparse-checkout: |\n          package.json\n          .github/workflows/bats/summarize.mjs\n    - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n      with:\n        node-version-file: package.json\n    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        pattern: \"*.logs\"\n    - run: node .github/workflows/bats/summarize.mjs\n      env:\n        EXPECTED_TESTS: ${{ needs.get-tests.outputs.tests }}\n"
  },
  {
    "path": ".github/workflows/codeql.yaml",
    "content": "name: \"CodeQL Advanced\"\n\non:\n  push:\n    branches: [\"main\", \"release*\"]\n  pull_request:\n    branches: [\"main\", \"release*\"]\n  schedule:\n  - cron: '33 19 * * 5'\n  workflow_dispatch:\n\njobs:\n  analyze:\n    name: Analyze (${{ matrix.language }})\n    # Runner size impacts CodeQL analysis time. To learn more, please see:\n    #   - https://gh.io/recommended-hardware-resources-for-running-codeql\n    #   - https://gh.io/supported-runners-and-hardware-resources\n    #   - https://gh.io/using-larger-runners (GitHub.com only)\n    # Consider using larger runners or machines with greater resources for possible analysis time improvements.\n    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}\n    permissions:\n      # required for all workflows\n      security-events: write\n\n      # required to fetch internal or private CodeQL packs\n      packages: read\n\n      # only required for workflows in private repositories\n      actions: read\n      contents: read\n\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - language: go\n          build-mode: autobuild\n        - language: javascript-typescript\n          build-mode: none\n        # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'\n        # Use `c-cpp` to analyze code written in C, C++ or both\n        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both\n        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both\n        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,\n        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.\n        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how\n        # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages\n    steps:\n    - name: Checkout repository\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n    # Initializes the CodeQL tools for scanning.\n    - name: Initialize CodeQL\n      uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2\n      with:\n        languages: ${{ matrix.language }}\n        build-mode: ${{ matrix.build-mode }}\n        # If you wish to specify custom queries, you can do so here or in a config file.\n        # By default, queries listed here will override any specified in a config file.\n        # Prefix the list here with \"+\" to use these queries and those in the config file.\n\n        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs\n        # queries: security-extended,security-and-quality\n\n    # If the analyze step fails for one of the languages you are analyzing with\n    # \"We were unable to automatically build your code\", modify the matrix above\n    # to set the build mode to \"manual\" for that language. Then modify this step\n    # to build your code.\n    # ℹ️ Command-line programs to run using the OS shell.\n    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun\n    - if: matrix.build-mode == 'manual'\n      shell: bash\n      run: |\n        echo 'If you are using a \"manual\" build mode for one or more of the' \\\n          'languages you are analyzing, replace this with the commands to build' \\\n          'your code, for example:'\n        echo '  make bootstrap'\n        echo '  make release'\n        exit 1\n\n    - name: Perform CodeQL Analysis\n      uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2\n      with:\n        category: \"/language:${{matrix.language}}\"\n"
  },
  {
    "path": ".github/workflows/docker-cli-monitor.yaml",
    "content": "name: Check for new releases of docker/cli\non:\n  schedule:\n    - cron: '55 8 * * *'\n  workflow_dispatch: {}\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  check-docker-cli:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/yarn-install\n\n      - run: yarn dcmonitor\n        env:\n          GITHUB_CREATE_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n          GITHUB_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/workflows/k3s-versions.yaml",
    "content": "name: Update k3s-versions.json\non:\n  schedule:\n  - cron: '43 8 * * *'\n  workflow_dispatch: {}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  check-update-versions:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        # we may need to checkout an existing branch, so need the full history\n        fetch-depth: 0\n    # Setup go to be able to run `go run ./scripts/k3s-version.go`\n    - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0\n      with:\n        go-version-file: go.work\n    - run: ./scripts/k3s-versions.sh\n      env:\n        GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n"
  },
  {
    "path": ".github/workflows/linux-e2e.yaml",
    "content": "name: e2e tests on Linux\n\non:\n  workflow_dispatch:\n  push:\n    branches-ignore:\n    - 'dependabot/**'\n  pull_request: {}\n\njobs:\n  check-paths:\n    uses: ./.github/workflows/paths-ignore.yaml\n  e2e-tests:\n    needs: check-paths\n    if: needs.check-paths.outputs.should-run == 'true'\n    timeout-minutes: 150\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - uses: ./.github/actions/yarn-install\n      - name: Disable admin-access before start up\n        run: |\n          mkdir -p $HOME/.config/rancher-desktop\n          cat <<EOF > $HOME/.config/rancher-desktop/settings.json\n            {\n              \"version\": 5,\n              \"application\": {\n                \"adminAccess\": false\n              }\n            }\n          EOF\n      - name: Enable kvm access\n        run: sudo chmod a+rwx /dev/kvm\n      - name: Run e2e Tests\n        continue-on-error: false\n        run: >-\n          xvfb-run --auto-servernum --server-args='-screen 0 1280x960x24'\n          yarn test:e2e\n        env:\n          RD_DEBUG_ENABLED: '1'\n          CI: true\n        timeout-minutes: 150\n      - name: Upload failure reports\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        if: always()\n        with:\n          name: failure-reports.zip\n          path: ./e2e/reports/*\n      - name: Clean up test environment\n        run: |\n          rm -f $HOME/.config/rancher-desktop.defaults.json\n          rm -f $HOME/.config/rancher-desktop.locked.json\n        if: always()\n"
  },
  {
    "path": ".github/workflows/linux-release.yaml",
    "content": "name: Upload Linux release\non:\n  release:\n    types:\n      - published\n  workflow_dispatch: {}\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  linux-release:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        fetch-depth: 0\n    - name: Populate necessary env vars\n      run: |\n        # get the env vars\n        version_with_v=${GITHUB_REF#\"refs/tags/\"}\n        release_zip_name=\"rancher-desktop-linux-${version_with_v}.zip\"\n        major_minor=$(echo ${version_with_v} | sed -E 's/v([0-9]+\\.[0-9]+)\\.[0-9]+.*/\\1/g')\n        s3_zip_name=\"rancher-desktop-linux-${major_minor}.zip\"\n\n        # make variables available in subsequent steps\n        echo \"version_with_v=$version_with_v\" >> $GITHUB_ENV\n        echo \"release_zip_name=$release_zip_name\" >> $GITHUB_ENV\n        echo \"major_minor=$major_minor\" >> $GITHUB_ENV\n        echo \"s3_zip_name=$s3_zip_name\" >> $GITHUB_ENV\n    - run: mkdir -p dist\n    - name: Fetch the .zip file from release\n      run: >-\n        curl -L -o \"dist/${release_zip_name}\"\n        \"https://github.com/${repository}/releases/download/${version_with_v}/${release_zip_name}\"\n      env:\n        repository: ${{ github.repository }}\n        version_with_v: ${{ env.version_with_v }}\n        release_zip_name: ${{ env.release_zip_name }}\n    - name: Upload zip file to S3\n      run: >-\n        aws s3 cp\n        \"dist/${release_zip_name}\"\n        \"s3://rancher-desktop-assets-for-obs/${s3_zip_name}\"\n      env:\n        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n        AWS_DEFAULT_REGION: us-east-1\n        release_zip_name: ${{ env.release_zip_name }}\n        s3_zip_name: ${{ env.s3_zip_name }}\n    - name: Trigger OBS services for relevant package in stable channel\n      run: >-\n        curl -X POST\n        -H \"Authorization: Token ${OBS_WEBHOOK_TOKEN}\"\n        \"https://build.opensuse.org/trigger/runservice?project=isv:Rancher:stable&package=rancher-desktop-${MAJOR_MINOR}\"\n      env:\n        MAJOR_MINOR: ${{ env.major_minor }}\n        OBS_WEBHOOK_TOKEN: ${{ secrets.OBS_WEBHOOK_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/macM1-e2e.yaml",
    "content": "name: e2e tests on Mac M1\n\non:\n  workflow_dispatch:\n  schedule:\n  - cron: '15 8 * * 1-5'\n\njobs:\n\n  e2e-tests:\n    timeout-minutes: 45\n    runs-on: [self-hosted, macos-latest, arm64]\n    env:\n      M1: 1\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n          ref: main\n      - uses: ./.github/actions/yarn-install\n      - name: Disable admin-access before start up\n        run: |\n          mkdir -p $HOME/Library/Preferences/rancher-desktop\n          touch $HOME/Library/Preferences/rancher-desktop/settings.json\n          cat <<EOF > $HOME/Library/Preferences/rancher-desktop/settings.json\n          {\n            \"version\": 5,\n            \"application\": {\n              \"adminAccess\": false\n              \"updater\":  { \"enabled\": false },\n            },\n            \"virtualMachine\" {\n              \"memoryInGB\": 6,\n            },\n            \"pathManagementStrategy\": \"rcfiles\"\n          }\n          EOF\n      - name: Run Rancher Desktop in dev\n        run: |\n          yarn dev -- --no-modal-dialogs &\n          sleep 200\n          $HOME/.rd/bin/rdctl shutdown\n          wait\n      - name: Run e2e Tests\n        continue-on-error: false\n        run: yarn test:e2e\n      - name: Failed tests\n        if: failure()\n        run: mkdir -p ./e2e/reports\n      - name: Upload Artifacts\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        if: failure()\n        with:\n          name: e2etest-artifacts\n          path: ./e2e/reports/*\n      - name: Cleanup test environment\n        run: |\n          #set -x\n          cd $HOME/Library\n          pushd Logs/rancher-desktop\n          for x in *.log ; do\n           echo -n '' > $x\n          done\n          popd\n          rm -fr \"Application Support/rancher-desktop\"\n          rm -fr Preferences/rancher-desktop\n          rm -fr Caches/rancher-desktop/k3s-versions.json\n          cd $HOME/.rd/bin\n          for x in helm kubectl nerdctl docker ; do\n           if [[ -L $x ]] ; then # && $(readlink $x):]] ; then\n           rm -f $x\n           fi\n          done\n        if: always()\n      - name: End stray processes\n        run: |\n          ps auxww | grep qemu\n          ps auxww | grep rancher | grep -vi -e goland\n        if: always()\n"
  },
  {
    "path": ".github/workflows/package.yaml",
    "content": "name: Package\n\non:\n  pull_request:\n    paths-ignore:\n    - '.github/actions/spelling/**'\n    - 'docs/**'\n    - '**.md'\n  push:\n    branches:\n    - main\n    - release-*\n    tags:\n    - '*'\n  workflow_dispatch:\n    inputs:\n      sign:\n        type: boolean\n        default: true\n        description: Whether to check signing result\n\ndefaults:\n  run:\n    shell: bash\n\njobs:\n  check-paths:\n    uses: ./.github/workflows/paths-ignore.yaml\n    with:\n      paths-ignore-globs: |\n        .github/actions/spelling/**\n        docs/**\n        **.md\n\n  package:\n    needs: check-paths\n    if: needs.check-paths.outputs.should-run == 'true'\n    strategy:\n      matrix:\n        include:\n        - platform: mac\n          arch: x86_64\n          runs-on: macos-15-intel\n        - platform: mac\n          arch: aarch64\n          runs-on: macos-latest\n        - platform: win\n          runs-on: windows-latest\n        - platform: linux\n          runs-on: ubuntu-22.04\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        # Needed to run `git describe` to get full version info\n        fetch-depth: 0\n    - uses: ./.github/actions/yarn-install\n    - run: yarn build\n    - run: yarn package\n    - name: Build bats.tar.gz\n      if: matrix.platform == 'linux'\n      run: make -C bats bats.tar.gz\n    - name: Upload bats.tar.gz\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'linux'\n      with:\n        name: bats.tar.gz\n        path: bats/bats.tar.gz\n        if-no-files-found: error\n    - name: Upload mac disk image\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'mac'\n      with:\n        name: Rancher Desktop.${{ matrix.arch }}.dmg\n        path: dist/Rancher Desktop*.dmg\n        if-no-files-found: error\n    - name: Upload mac zip\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'mac'\n      with:\n        name: Rancher Desktop-mac.${{ matrix.arch }}.zip\n        path: dist/Rancher Desktop*.zip\n        if-no-files-found: error\n    - name: Upload Windows installer\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'win'\n      with:\n        name: Rancher Desktop Setup.msi\n        path: dist/Rancher.Desktop*.msi\n        if-no-files-found: error\n    - name: Upload Windows zip\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'win'\n      with:\n        name: Rancher Desktop-win.zip\n        path: dist/Rancher Desktop-*-win.zip\n        if-no-files-found: error\n    - name: Upload Linux zip\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'linux'\n      with:\n        name: Rancher Desktop-linux.zip\n        path: dist/rancher-desktop-*-linux.zip\n        if-no-files-found: error\n    - name: Trigger OBS build\n      if: matrix.platform == 'linux' && github.ref_type == 'branch' && ( startsWith(github.ref_name, 'main') || startsWith(github.ref_name, 'release-') )\n      run: |\n        if [[ -z $AWS_ACCESS_KEY_ID ]] || [[ -z $OBS_WEBHOOK_TOKEN ]]; then\n          echo \"Secrets unavailable, skipping.\"\n          exit 0\n        fi\n        # in pull requests GITHUB_REF_NAME is in the form \"<pr_number>/merge\";\n        # remove slashes since they aren't valid in filenames\n        no_slash_ref_name=\"${GITHUB_REF_NAME//\\//-}\"\n        zip_name=\"rancher-desktop-linux-${no_slash_ref_name}.zip\"\n\n        # Copy zip file to S3\n        aws s3 cp \\\n          dist/rancher-desktop-*-linux.zip \\\n          \"s3://rancher-desktop-assets-for-obs/$zip_name\"\n\n        # Trigger OBS services for relevant package in dev channel\n        curl -X POST \\\n          -H \"Authorization: Token ${OBS_WEBHOOK_TOKEN}\" \\\n          \"https://build.opensuse.org/trigger/runservice?project=isv:Rancher:dev&package=rancher-desktop-${GITHUB_REF_NAME}\"\n      env:\n        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}\n        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}\n        AWS_DEFAULT_REGION: us-east-1\n        OBS_WEBHOOK_TOKEN: ${{ secrets.OBS_WEBHOOK_TOKEN }}\n\n  sign-win:\n    name: Test Signing (Windows)\n    needs: package\n    runs-on: windows-2022\n    if: >-\n      (github.event_name == 'push' && github.ref == 'refs/heads/main') ||\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release-')) ||\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||\n      (github.event_name == 'workflow_dispatch' && github.event.inputs.sign)\n    permissions:\n      contents: read\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      name: Download artifact\n      with:\n        name: Rancher Desktop-win.zip\n    - name: Generate test signing certificate\n      shell: powershell\n      run: |\n        $cert = New-SelfSignedCertificate `\n          -Type Custom `\n          -Subject \"CN=Rancher-Sandbox, C=CA\" `\n          -KeyUsage DigitalSignature `\n          -CertStoreLocation Cert:\\CurrentUser\\My `\n          -FriendlyName \"Rancher-Sandbox Code Signing\" `\n          -TextExtension @(\"2.5.29.37={text}1.3.6.1.5.5.7.3.3\", \"2.5.29.19={text}\")\n        Write-Output $cert\n        Write-Output \"CSC_FINGERPRINT=$($cert.Thumbprint)\" `\n          | Out-File -Append -Encoding ASCII \"${env:GITHUB_ENV}\"\n      timeout-minutes: 1\n    - name: Sign artifact\n      shell: powershell\n      run: yarn sign (Get-Item \"Rancher Desktop*-win.zip\")\n      timeout-minutes: 10\n    - name: Verify installer signature\n      shell: powershell\n      run: |\n        $usedCert = (Get-AuthenticodeSignature -FilePath 'dist\\Rancher*Desktop*.msi').SignerCertificate\n        Write-Output $usedCert\n        if ($usedCert.Thumbprint -ne $env:CSC_FINGERPRINT) {\n          Throw \"Installer signed with wrong certificate\"\n        }\n      timeout-minutes: 1\n\n  sign-mac:\n    name: Test Signing (macOS)\n    needs: package\n    strategy:\n      matrix:\n        include:\n        - arch: aarch64\n        # skip x86_64, we don't need to duplicate the testing for now.\n    runs-on: macos-latest\n    if: >-\n      (github.event_name == 'push' && github.ref == 'refs/heads/main') ||\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/heads/release-')) ||\n      (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')) ||\n      (github.event_name == 'workflow_dispatch' && github.event.inputs.sign)\n    permissions:\n      contents: read\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      name: Download artifact\n      with:\n        name: Rancher Desktop-mac.${{ matrix.arch }}.zip\n    - name: Generate test signing certificate\n      run: |\n        openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \\\n          -keyform pem -sha256 -days 3650 -nodes -subj \\\n          \"/C=CA/CN=RD Test Signing Key\" \\\n          -addext keyUsage=critical,digitalSignature \\\n          -addext extendedKeyUsage=critical,codeSigning\n        # Create a custom keychain so we can unlock it properly.\n        security create-keychain -p \"\" tmp.keychain\n        security default-keychain -d user -s tmp.keychain\n        security unlock-keychain -p \"\" tmp.keychain\n        security set-keychain-settings -u tmp.keychain # Disable keychain auto-lock\n        security import key.pem -k tmp.keychain -t priv -A\n        security import cert.pem -k tmp.keychain -t cert -A\n        security set-key-partition-list -S apple-tool:,apple:,codesign: -s \\\n          -k \"\" tmp.keychain\n        # Print out the valid certificates for debugging.\n        security find-identity\n        # Determine the key fingerprint.\n        awk_expr='/)/ { print $2 ; exit }'\n        hash=\"$(security find-identity | awk \"$awk_expr\")\"\n        echo \"CSC_FINGERPRINT=${hash}\" >> \"$GITHUB_ENV\"\n      timeout-minutes: 1\n    - name: Flag build for M1\n      if: matrix.arch == 'aarch64'\n      run: echo \"M1=1\" >> \"${GITHUB_ENV}\"\n    - name: Sign artifact\n      run: |\n        for zip in Rancher\\ Desktop-*mac*.zip; do\n          echo \"::group::Signing ${zip}\"\n          yarn sign --skip-notarize --skip-constraints \"${zip}\"\n          echo \"::endgroup::\"\n        done\n      timeout-minutes: 15\n    - name: Verify signature\n      run: |\n        codesign --verify --deep --strict --verbose=2 dist/*.dmg\n        codesign --verify --deep --strict --verbose=2 dist/*.zip\n      timeout-minutes: 5\n"
  },
  {
    "path": ".github/workflows/paths-ignore.yaml",
    "content": "# This is a reusable workflow to determine if the current change requires an E2E\n# run.  This is required because using [paths-ignored] directly means the whole\n# workflow is skipped, but that means that it doesn't count as having run a\n# required workflow.\n\n# Usage:\n#   jobs:\n#     check-paths:\n#        uses: ./.github/workflows/actions/paths-ignore.yaml\n#     do_thing:\n#        if: jobs.check-paths.outputs.should-run == 'true'\n#        # Unfortunately, a string comparison is required.\n\nname: Check for ignored paths\n\non:\n  workflow_call:\n    inputs:\n      paths-ignore-globs:\n        description: >\n          Paths to ignore.  Should glob patterns (as a git pathspec glob), one\n          per line.  See\n          https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-glob\n        type: string\n        default: |\n          .github/actions/spelling/**\n          bats/**\n          docs/**\n          **.md\n    outputs:\n      should-run:\n        description: Whether other steps should run.\n        value: ${{ jobs.check.outputs.should-run }}\n\npermissions:\n  contents: read\n\njobs:\n  check:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Set baseline result\n      run: echo \"SHOULD_RUN=true\" >> \"$GITHUB_ENV\"\n    - name: Determine paths to ignore\n      if: github.event_name == 'pull_request'\n      run: |\n        PATHS_IGNORE=\"PATHS_IGNORE=\"\n        while read -r line; do\n          if [[ -n $line ]]; then\n            PATHS_IGNORE=\"${PATHS_IGNORE} :!/${line}\"\n          fi\n        done <<< \"$INPUT\"\n        echo \"$PATHS_IGNORE\"\n        echo \"$PATHS_IGNORE\" >> \"$GITHUB_ENV\"\n      env:\n        INPUT: ${{ inputs.paths-ignore-globs }}\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      if: github.event_name == 'pull_request'\n      with:\n        fetch-depth: 0\n        persist-credentials: false\n    - name: Check for differences\n      if: github.event_name == 'pull_request'\n      run: |\n        MERGE_BASE=$(git merge-base $BASE $HEAD)\n        diff=\"$(git diff --name-only $MERGE_BASE $HEAD -- $PATHS_IGNORE)\"\n        if [[ -z \"$diff\" ]]; then\n          echo \"No modified files found.\"\n          echo \"SHOULD_RUN=false\" >> \"$GITHUB_ENV\"\n        else\n          printf \"Modified files:\\n%s\\n\" \"$diff\"\n        fi\n      env:\n        BASE: ${{ github.event.pull_request.base.sha }}\n        HEAD: ${{ github.event.pull_request.head.sha }}\n    - name: Set final output\n      id: result\n      run: echo \"should-run=$SHOULD_RUN\" >> \"$GITHUB_OUTPUT\"\n    outputs:\n      should-run: ${{ steps.result.outputs.should-run }}\n"
  },
  {
    "path": ".github/workflows/rddepman.yaml",
    "content": "name: Update external dependencies\non:\n  schedule:\n    - cron: '23 8 * * *'\n  workflow_dispatch: {}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  check-update-versions:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    steps:\n\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/yarn-install\n\n      - run: yarn rddepman\n        env:\n          GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n"
  },
  {
    "path": ".github/workflows/rdx-host-api-tests.yaml",
    "content": "# This workflow builds the Rancher Desktop Extensions Host APIs testing image\n# and publishes it.\n\nname: RDX Host APIs Testing image\non:\n  push:\n    branches: [ main ]\n    paths: [ 'bats/tests/extensions/testdata/**' ]\n  workflow_dispatch: {}\npermissions:\n  packages: write\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0\n    - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0\n    - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0\n      id: meta\n      with:\n        images: |\n          ghcr.io/${{ github.repository }}/rdx-host-api-test\n        tags: type=raw,value=latest,enable={{ is_default_branch }}\n    - uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0\n      with:\n        registry: ghcr.io\n        username: ${{ github.actor }}\n        password: ${{ github.token }}\n    - uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0\n      with:\n        build-args: variant=host-apis\n        context: bats/tests/extensions/testdata\n        platforms: |\n          linux/amd64\n          linux/arm64\n        push: true\n        tags: ${{ steps.meta.outputs.tags }}\n        labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/release-merge-to-main.yaml",
    "content": "name: \"Release: Merge to main\"\n\non:\n  release:\n    types:\n    - created\n    - published\n    - released\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions: {}\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  create-pr:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n    - uses: ./.github/actions/yarn-install\n    - run: node scripts/ts-wrapper.js scripts/release-merge-to-main.ts\n      env:\n        GITHUB_WRITE_TOKEN: ${{ github.token }}\n        GITHUB_PR_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n"
  },
  {
    "path": ".github/workflows/scorecard.yml",
    "content": "name: Scorecard supply-chain security\non:\n  # For Branch-Protection check. Only the default branch is supported. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection\n  branch_protection_rule:\n  # To guarantee Maintained check is occasionally updated. See\n  # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained\n  schedule:\n  - cron: '23 13 * * 4'\n  push:\n    branches:\n    - master\n  workflow_dispatch:\n\n# Declare default permissions as read only.\npermissions: read-all\n\njobs:\n  analysis:\n    name: Scorecard analysis\n    runs-on: ubuntu-latest\n    permissions:\n      # Needed to upload the results to code-scanning dashboard.\n      security-events: write\n      # Needed to publish results and get a badge (see publish_results below).\n      id-token: write\n\n    steps:\n    - name: \"Checkout code\"\n      uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd  # v6.0.2\n      with:\n        persist-credentials: false\n\n    - name: \"Run analysis\"\n      uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a  # v2.4.3\n      with:\n        results_file: results.sarif\n        results_format: sarif\n        # (Optional) \"write\" PAT token. Uncomment the `repo_token` line below if:\n        # - you want to enable the Branch-Protection check on a *public* repository, or\n        # - you are installing Scorecard on a *private* repository\n        # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional.\n        # repo_token: ${{ secrets.SCORECARD_TOKEN }}\n\n        # Public repositories:\n        #   - Publish results to OpenSSF REST API for easy access by consumers\n        #   - Allows the repository to include the Scorecard badge.\n        #   - See https://github.com/ossf/scorecard-action#publishing-results.\n        publish_results: true\n\n    # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF\n    # format to the repository Actions tab.\n    - name: \"Upload artifact\"\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f  # v3.pre.node20\n      with:\n        name: SARIF file\n        path: results.sarif\n        retention-days: 5\n\n    # Upload the results to GitHub's code scanning dashboard (optional).\n    # Commenting out will disable upload of results to your repo's Code Scanning dashboard\n    - name: \"Upload to code-scanning\"\n      uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2\n      with:\n        sarif_file: results.sarif\n"
  },
  {
    "path": ".github/workflows/screenshot.yaml",
    "content": "name: Screenshots\n\non:\n  workflow_dispatch:\n    inputs:\n      mock_version:\n        description: Mock Version\n        type: string\n        required: true\n        default: '1.0.0'\n\njobs:\n  screenshot:\n    name: Take screenshot\n    concurrency:\n      group: \"${{ github.workflow_ref }} (${{ matrix.platform }})\"\n      cancel-in-progress: true\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - platform: mac\n          # Use an x86_64 platform because arm64 runners don't have nested\n          # virtualization available.\n          runs-on: macos-15-intel\n        - platform: win\n          runs-on: windows-latest\n        - platform: linux\n          runs-on: ubuntu-latest\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n\n    - name: \"macOS: Install GetWindowID\"\n      if: runner.os == 'macOS'\n      run: |\n        brew update\n        brew install smokris/getwindowid/getwindowid\n\n    - name: \"Linux: Install Tools\"\n      if: runner.os == 'Linux'\n      run: |\n        sudo apt-get update\n        sudo apt-get install graphicsmagick x11-utils mutter # spellcheck-ignore-line\n\n    - name: \"macOS: Set startup command\"\n      if: runner.os == 'macOS'\n      run: echo \"EXEC_COMMAND=$EXEC_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        EXEC_COMMAND: exec\n    - name: \"Linux: Set startup command\"\n      if: runner.os == 'Linux'\n      run: |\n        # Write a wrapper script to start mutter (so we get window decorations).\n        echo '#!/bin/sh' > /usr/local/bin/exec-command\n        echo 'mutter --replace --sm-disable --x11 &>/dev/null &' >> /usr/local/bin/exec-command\n        echo 'exec \"$@\"' >> /usr/local/bin/exec-command\n        chmod a+x /usr/local/bin/exec-command\n        echo \"EXEC_COMMAND=$EXEC_COMMAND /usr/local/bin/exec-command\" >> \"$GITHUB_ENV\"\n      env:\n        EXEC_COMMAND: >-\n          exec xvfb-run --auto-servernum\n          --server-args='-screen 0 1280x960x24'\n    - name: \"Windows: Set startup command\"\n      if: runner.os == 'Windows'\n      shell: bash\n      run: echo \"EXEC_COMMAND=$EXEC_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        EXEC_COMMAND: # On Windows, we don't need any commands.\n\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - uses: ./.github/actions/setup-environment\n    - name: Override version\n      if: inputs.mock_version\n      run: echo \"RD_MOCK_VERSION=${{ inputs.mock_version }}\" >> \"${GITHUB_ENV}\"\n      shell: bash\n\n    - run: ${{ env.EXEC_COMMAND }} yarn screenshots\n      env:\n        EXEC_COMMAND: ${{ env.EXEC_COMMAND }}\n        RD_ENV_SCREENSHOT_SLEEP: 5000\n        RD_LOGS_DIR: logs\n    - name: Upload screenshots\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: screenshots-${{ matrix.platform }}.zip\n        path: screenshots/output/\n        if-no-files-found: error\n    - name: Upload logs\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: always()\n      with:\n        name: logs-${{ matrix.platform }}.zip\n        path: |\n          logs/\n          e2e/reports/\n          screenshots/output/\n  package:\n    name: Package screenshots for docs\n    needs: screenshot\n    concurrency:\n      group: \"${{ github.workflow_ref }} (package)\"\n      cancel-in-progress: true\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        pattern: screenshots-*.zip\n        merge-multiple: true\n        path: ${{ github.workspace }}/in\n    - name: Rename images\n      run: |\n        while IFS= read -d $'\\0' -r line; do\n          IFS=/ read -r in platform scheme window name <<<\"$line\"\n          if [[ \"$scheme\" != \"light\" ]]; then\n              continue\n          fi\n          case \"$platform\" in\n              darwin) platform=macOS;;\n              linux) platform=Linux;;\n              win32) platform=Windows;;\n          esac\n          if [[ \"$window\" == \"main\" ]]; then\n              window=\"ui-main\"\n          fi\n          if [[ $name =~ ^[0-9]+_ ]]; then\n              name=\"${name#*_}\"\n          fi\n          if [[ name == \"intro\" ]]; then\n            continue\n          fi\n          out=\"out/${window}/${platform}_${name}\"\n          mkdir -p \"$(dirname \"$out\")\"\n          cp \"$line\" \"$out\"\n          echo \"$out\"\n        done < <(find in -name '*.png' -print0)\n    - name: Generate introduction image\n      run: |\n        # The intro image consists of the mac image on the left and the Windows\n        # image on the right, each showing Kubernetes settings.\n        sudo DEBIAN_FRONTEND=noninteractive apt-get install graphicsmagick # spellcheck-ignore-line\n        mkdir -p out/getting-started\n        gm convert in/darwin/light/main/*_intro.png in/win32/light/main/*_intro.png +append out/getting-started/introduction_preferences_tabKubernetes.png\n    - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: screenshots.zip\n        path: out\n        if-no-files-found: error\n        overwrite: true\n"
  },
  {
    "path": ".github/workflows/smoke-test/install-from-repo.sh",
    "content": "#!/usr/bin/env bash\n\n# This script is expected to run as root and install Rancher Desktop from the\n# repository obs://isv:Rancher:dev\n# Expected environment variables:\n#   RD_VERSION\n#      Rancher Desktop version; either major.minor (`1.20`) or the tag (`v1.20.0`).\n\nset -o errexit -o nounset\n\n# shellcheck disable=2329 # The function is invoked dynamically\ninstall_linux_debian() {\n    local keyLocation=/usr/share version\n\n    if [[ -d /etc/apt/keyrings ]]; then\n        keyLocation=/etc/apt\n    fi\n\n    apt-get update\n    apt-get install -y gnupg\n    curl -s https://download.opensuse.org/repositories/isv:/Rancher:/dev/deb/Release.key \\\n        | gpg --dearmor \\\n        > \"${keyLocation}/keyrings/isv-rancher-dev-archive-keyring.gpg\"\n    echo \"deb [signed-by=${keyLocation}/keyrings/isv-rancher-dev-archive-keyring.gpg] https://download.opensuse.org/repositories/isv:/Rancher:/dev/deb/ ./\"\\\n        > /etc/apt/sources.list.d/isv-rancher-dev.list\n    apt-get update\n    version=$(apt-cache show --quiet rancher-desktop \\\n        | awk -F': ' \"/^Version: 0\\.release${RD_VERSION//./\\\\.}\\./ { print \\$2 }\")\n    if [[ -z \"${version}\" ]]; then\n        echo \"Could not find any versions of rancher-desktop\" >&2\n        exit 1\n    fi\n    apt-get install -y \"rancher-desktop=${version}\"\n}\n\n# shellcheck disable=2329 # The function is invoked dynamically\ninstall_linux_opensuse() {\n    zypper --non-interactive addrepo https://download.opensuse.org/repositories/isv:/Rancher:/dev/rpm/isv:Rancher:dev.repo\n    zypper --non-interactive --gpg-auto-import-keys install libxml2-tools\n    local version\n    version=$(zypper --xmlout --non-interactive search --details --match-exact rancher-desktop \\\n        | xmllint --xpath \"string(//solvable[@kind='package']/@edition[contains(., '0.release${RD_VERSION}.')])\" -)\n    zypper --non-interactive install \"rancher-desktop=${version}\"\n}\n\n# shellcheck disable=2329 # The function is invoked dynamically\ninstall_linux_fedora() {\n    dnf config-manager addrepo --from-repofile=https://download.opensuse.org/repositories/isv:/Rancher:/dev/fedora/isv:Rancher:dev.repo\n    local version\n    version=$(dnf --quiet info --showduplicates rancher-desktop.x86_64 \\\n        | awk -F: \"\\$1 ~ /Version/ && \\$2 ~ /0\\.release${RD_VERSION//./\\\\.}/ { print \\$2 }\" \\\n        | tr -d '[:space:]')\n    dnf --assumeyes install \"rancher-desktop-${version}\"\n}\n\nmain() {\n    RD_VERSION=$(grep --only-matching '\\([0-9]\\+\\.[0-9]\\+\\)' <<< \"$RD_VERSION\")\n    source /etc/os-release\n    for id in ${ID:-} ${ID_LIKE:-}; do\n        if [[ \"$(type -t \"install_linux_$id\")\" == function ]]; then\n            eval \"install_linux_$id\"\n            exit 0\n        fi\n    done\n    printf \"Could not find supported distribution in %s\\n\" \"${ID:-} ${ID_LIKE:-}\" >&2\n    exit 1\n}\n\nmain\n"
  },
  {
    "path": ".github/workflows/smoke-test/smoke-test.sh",
    "content": "#!/usr/bin/env bash\n\n# This script is expected to run from CI, and does a final smoke test.\n# There should be an installer in the current directory (or, in the case of\n# Linux, a zip file), along with its accompanying sha512sum file.\n# On Windows, build/signing-config-win.yaml is also required.\n\n# Environment variables as inputs:\n#   RD_SKIP_INSTALL (Linux)\n#     Skip installing Rancher Desktop, and assume it was installed from the repo.\n\n# Required tools:\n# - jq\n# - yq (Windows only)\n\n# Note that, on Windows, this is run via msys bash (installed wit git).\n\nset -o errexit -o nounset\nshopt -s nullglob\n\nexport MSYS2_ARG_CONV_EXCL='*'\nRDCTL= # Path to rdctl\nAPPIMAGE_PID= # PID of AppImage process; not used if not using AppImage.\n\n# All commands in the cleanups array will be run on exit.  They must be plain\n# strings that will be passed to eval\ncleanups=()\n\n# Run the cleanups.\ndo_cleanup() {\n    # In case the array has holes (it shouldn't), make an array of indices.\n    local indices=(\"${!cleanups[@]}\")\n    local i\n    for (( i=${#indices[@]} - 1; i >= 0; i-- )); do\n        # shellcheck disable=2086 # We expect to glob and word split\n        eval ${cleanups[$i]}\n    done\n}\n\ntrap do_cleanup EXIT\n\n# Locate the archive, check its checksum, and echo the file name.\nget_archive() {\n    local checksum archiveName\n    if [[ -n \"${RD_SKIP_INSTALL:-}\" ]]; then\n        echo \"Skipping getting archive.\" >&2\n        echo \"no-archive-used\"\n        return\n    fi\n    for checksum in *.sha512sum; do\n        archiveName=${checksum%.sha512sum}\n        if command -v sha512sum &>/dev/null; then\n            sha512sum --check --quiet --strict \"$checksum\"\n        else\n            shasum --check --quiet --algorithm 512 \"$checksum\"\n        fi\n        grep --quiet \"$archiveName\" \"$checksum\"\n        readlink -f \"$archiveName\"\n        return\n    done\n    echo \"Failed to find archive.\" >&2\n    exit 1\n}\n\n# Return the current platform; one of \"darwin\", \"linux\", \"win32\"\nget_platform() {\n    case \"$(uname -s)\" in\n    Darwin)\n        echo \"darwin\";;\n    Linux)\n        echo \"linux\";;\n    MINGW*)\n        echo \"win32\";;\n    *)\n        printf \"Unsupported platform %s\\n\" \"$(uname -s)\" >&2\n        exit 1;;\n    esac\n}\n\n# Assume the first argument given is a path to the Rancher Desktop .dmg disk\n# image; install it, and set the global variable RDCTL to the path of the rdctl\n# executable.\ninstall_darwin() {\n    local archiveName=$1\n    local mountpoint\n    mountpoint=$(mktemp -d -t rd-dmg-)\n    cleanups+=(\"rm -rf '$mountpoint'\")\n\n    local srcApp=\"${mountpoint}/Rancher Desktop.app\"\n    local destApp=\"/Applications/Rancher Desktop.app\"\n\n    codesign --verify --deep --strict --verbose=2 --check-notarization \"$archiveName\"\n    hdiutil attach \"$archiveName\" -mountpoint \"$mountpoint\"\n    cleanups+=(\"hdiutil detach '$mountpoint'\")\n\n    codesign --verify --deep --strict --verbose=2 --check-notarization \"$srcApp\"\n    mkdir -p \"$destApp\"\n    cleanups+=(\"rm -rf '$destApp'\")\n\n    cp -a \"$srcApp\" \"$(dirname \"$destApp\")\"\n    xattr -d -r -s -v com.apple.quarantine \"$destApp\"\n\n    # Check that the image is compressed\n    local compressionRatio\n    compressionRatio=\"$(hdiutil imageinfo -plist \"$archiveName\" \\\n        | plutil -convert json -o - - \\\n        | jq '.[\"Size Information\"][\"Compressed Ratio\"]')\"\n    if jq --exit-status '. > 0.9' <<<\"$compressionRatio\"; then\n        printf \"Archive %s appears to be uncompressed; compression ratio is %s\\n\" \\\n            \"$archiveName\" \"$compressionRatio\" >&2\n        exit 1\n    fi\n\n    if [[ \"$(uname -m)\" =~ arm ]]; then\n        # For macOS, currently only x86_64 runners support nested virtualization\n        # https://github.com/actions/runner-images/issues/9460\n        # Abort the script (gracefully) instead of trying to run RD.\n        echo \"Skipping actually running on Rancher Desktop because arm64 runners do not have nested virtualization\" >&2\n        exit 0\n    fi\n\n    RDCTL=\"$destApp/Contents/Resources/resources/darwin/bin/rdctl\"\n}\n\n# Assume the first argument given is a path to the Rancher Desktop zip file;\n# install it, and set the global variable RDCTL to the path of the rdctl\n# executable.  If the archive is an AppImage file instead, then this function\n# instead sets APPIMAGE_PID.\ninstall_linux() {\n    if [[ $(id --user) -eq 0 ]]; then\n        echo \"This script should not be run as root\" >&2\n        exit 1\n    fi\n\n    if [[ -z \"${RD_SKIP_INSTALL:-}\" ]]; then\n        local archiveName=$1\n\n        if [[ \"$archiveName\" =~ .*\\.AppImage$ ]]; then\n            sudo chmod a+x \"$archiveName\"\n            \"$archiveName\" \\\n                --no-sandbox --enable-logging=stderr --v=1 \\\n                --no-modal-dialogs --kubernetes.enabled \\\n                --application.updater.enabled=false&\n            APPIMAGE_PID=$!\n            return\n        else\n            sudo mkdir -p /opt/rancher-desktop\n            sudo unzip -d /opt/rancher-desktop \"$archiveName\"\n            sudo chmod 4755 /opt/rancher-desktop/chrome-sandbox\n        fi\n    fi\n\n    RDCTL=\"/opt/rancher-desktop/resources/resources/linux/bin/rdctl\"\n}\n\n# Helper function on Windows to verify the signature of a file (provided as the\n# first argument).\nwin32_verify() {\n    local path\n    path=\"$(cygpath --windows \"$1\")\"\n    # When running GitHub actions, using `powershell.exe` here causes issues\n    # with loading the `Microsoft.PowerShell.Security` module; using `pwsh.exe`\n    # seems to be fine.  This is probably because the default shell is pwsh, and\n    # the environment has paths to the PowerShell 7 version of the module, so it\n    # tries to load that instead of the version appropriate for PowerShell.exe.\n    local pwsh=(pwsh.exe -NoLogo -NoProfile -NonInteractive -Command)\n    local stdout\n    stdout=$(\"${pwsh[@]}\" \"\\$(Get-AuthenticodeSignature '$path').Status\")\n\n    if [[ \"$stdout\" != \"Valid\" ]]; then\n        printf \"%s is not correctly signed:\\n\" \"$path\"\n        \"${pwsh[@]}\" \"Get-AuthenticodeSignature '$path' | Format-List\"\n        exit 1\n    fi\n}\n\n# Assume the first argument given is a path to the Rancher Desktop installer;\n# install it, and set the global variable RDCTL to the path of the rdctl\n# executable.\ninstall_win32() {\n    local archiveName=$1\n\n    win32_verify \"$archiveName\"\n    mkdir -p \"$(cygpath --unix \"${RD_LOGS_DIR}\")\"\n    msiexec.exe '/lv*x' \"${RD_LOGS_DIR}\\\\install.log\" \\\n        /i \"$(cygpath --windows \"$archiveName\")\" /passive ALLUSERS=1\n    # msiexec returns immediately and runs in the background; wait for that\n    # process to exit before continuing.\n    local deadline completed\n    deadline=$(( $(date +%s) + 10 * 60 ))\n    while [[ $(date +%s) -lt $deadline ]]; do\n        if tasklist.exe /FI \"ImageName eq msiexec.exe\" | grep msiexec; then\n            printf \"Waiting for msiexec to finish: %s/%s\\n\" \"$(date)\" \"$(date --date=\"@$deadline\")\"\n            sleep 10\n        else\n            completed=true\n            break\n        fi\n    done\n    if [[ -z \"${completed:-}\" ]]; then\n        echo \"msiexec took too long to finish, aborting\" >&2\n        exit 1\n    fi\n    local installDirectory\n    installDirectory=$(cygpath --unix 'C:\\Program Files\\Rancher Desktop')\n    local rdctl=\"$installDirectory/resources/resources/win32/bin/rdctl.exe\"\n\n    local -a keys\n    mapfile -t keys < <(yq.exe 'keys | .[]' < build/signing-config-win.yaml)\n    local key\n    for key in \"${keys[@]}\"; do\n        local expr='.[env(key)][] | select(. != \"!*\")'\n        local -a values\n        mapfile -t values < <(key=$key yq.exe \"$expr\" < build/signing-config-win.yaml)\n        for value in \"${values[@]}\"; do\n            if [[ \"$value\" == \"wix-custom-action.dll\" ]]; then\n                # This file is not installed\n                continue\n            fi\n            win32_verify \"$installDirectory/$key/$value\"\n        done\n    done\n\n    # Verify that rdctl exists\n    win32_verify \"$rdctl\"\n    RDCTL=$rdctl\n}\n\n# Wait for the backend to be alive.  $RDCTL must be set (from the install_*\n# functions).  If $APPIMAGE_PID is set, assume we're running AppImage instead.\nwait_for_backend() {\n    local deadline state deadline_date platform rd_pid\n    deadline=$(( $(date +%s) + 10 * 60 ))\n    deadline_date=$({ date --date=\"@$deadline\" || date -j -f %s \"$deadline\"; } 2>/dev/null)\n    platform=$(get_platform)\n\n    while [[ $(date +%s) -lt $deadline ]]; do\n        if [[ -n \"${APPIMAGE_PID:-}\" ]] && [[ -z \"${RDCTL:-}\" ]]; then\n            rd_pid=$(pidof --separator $'\\n' rancher-desktop | sort -n | head -n 1 || echo missing)\n            if [[ -e /proc/$rd_pid/exe ]]; then\n                RDCTL=$(dirname \"$(readlink /proc/$rd_pid/exe)\")/resources/resources/linux/bin/rdctl\n                continue\n            fi\n            state=NOT_RUNNING\n        elif [[ $platform == linux ]] && [[ ! -e $HOME/.local/share/rancher-desktop/rd-engine.json ]]; then\n            state=NO_SERVER_CONFIG\n        else\n            state=$(\"$RDCTL\" api /v1/backend_state || echo '{\"vmState\": \"NO_RESPONSE\"}')\n            state=$(jq --raw-output .vmState <<< \"$state\")\n        fi\n        case \"$state\" in\n            ERROR)\n                echo \"Backend reached error state.\" >&2\n                exit 1 ;;\n            STARTED|DISABLED)\n                return ;;\n            *)\n                printf \"Backend state: %s\\n\" \"$state\";;\n        esac\n\n        # if we get here, either we failed to get state or it's starting.\n        printf \"Waiting for backend: (%s) %s/%s\\n\" \"$state\" \"$(date)\" \"$deadline_date\"\n        sleep 10\n    done\n\n    echo \"Timed out waiting for backend to stabilize.\" >&2\n    printf \"Current time: %s\\n\" \"$(date)\" >&2\n    printf \"Deadline: %s\\n\" \"$deadline_date\" >&2\n    exit 1\n}\n\nmain() {\n    local archive platform\n    platform=$(get_platform)\n    archive=$(get_archive)\n\n    eval \"install_${platform}\" \"$archive\"\n    if [[ -z \"${APPIMAGE_PID:-}\" ]]; then\n        \"$RDCTL\" start --no-modal-dialogs \\\n            --kubernetes.enabled --application.updater.enabled=false\n        cleanups+=(\"'$RDCTL' shutdown\")\n    fi\n    wait_for_backend\n    echo \"Smoke test passed.\"\n}\n\nmain\n"
  },
  {
    "path": ".github/workflows/smoke-test.yaml",
    "content": "# This workflow downloads artifacts from a (by default, draft) release and runs\n# a short smoke test where the application is installed and run and immediately\n# shut down.\n# Since we need contents-write permissions to look at draft releases, we\n# actually download the artifacts in a smaller job, then upload them into the\n# run and download it _again_ in the second (per-platform) job where no\n# permissions are required.\nname: Release smoke test\npermissions: {}\non:\n  workflow_dispatch:\n    inputs:\n      tag:\n        description: >\n          Download artifacts from release with this tag, rather than picking the\n          first draft release.\n        type: string\n\njobs:\n  download-artifacts:\n    name: Find release\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write # Needed to list draft releases\n    env:\n      RELEASE_TAG: ${{ inputs.tag }}\n    outputs:\n      release-tag: ${{ steps.set-release-tag.outputs.release-tag }}\n    steps:\n    - name: Find release\n      if: inputs.tag == ''\n      run: >-\n        set -o xtrace;\n        printf \"RELEASE_TAG=%s\\n\" >>\"$GITHUB_ENV\"\n        \"$(gh api repos/${{ github.repository }}/releases\n        --jq 'map(select(.draft))[0].tag_name')\"\n      env:\n        GH_TOKEN: ${{ github.token }}\n    - id: set-release-tag\n      run: >-\n        printf \"release-tag=%s\\n\" \"$RELEASE_TAG\" >> \"$GITHUB_OUTPUT\"\n    - name: Download artifacts\n      run: |\n        if [[ -z \"$RELEASE_TAG\" ]]; then\n          echo \"Failed to find release tag\" >&2\n          exit 1\n        fi\n        gh release download \"$RELEASE_TAG\" \\\n          --repo ${{ github.repository }} \\\n          --pattern '*.dmg' \\\n          --pattern '*.dmg.sha512sum' \\\n          --pattern '*.msi' \\\n          --pattern '*.msi.sha512sum' \\\n          --pattern 'rancher-desktop-linux-*.zip' \\\n          --pattern 'rancher-desktop-linux-*.zip.sha512sum'\n      env:\n        GH_TOKEN: ${{ github.token }}\n\n    - name: Download AppImage\n      run: |\n        branch=$(cut -d. -f1,2 <<< \"${RELEASE_TAG#v}\")\n        read -r artifact_name < <(\n          curl \"${OBS_DOWNLOAD_URL}?jsontable\" \\\n            | jq --raw-output \".data[].name | select(endswith(\\\".AppImage\\\")) | select(contains(\\\".release${branch}.\\\"))\"\n          )\n        curl -L -o rancher-desktop.AppImage \"${OBS_DOWNLOAD_URL}${artifact_name}\"\n        # The AppImage does not have a checksum; make one up.\n        sha512sum rancher-desktop.AppImage > rancher-desktop.AppImage.sha512sum\n        chmod a+x rancher-desktop.AppImage\n      env:\n        OBS_DOWNLOAD_URL: https://download.opensuse.org/download/repositories/isv:/Rancher:/dev/AppImage/\n\n    - name: Upload macOS aarch-64 artifacts\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: application-macos-aarch64.zip\n        if-no-files-found: error\n        path: |\n          *.aarch64.dmg\n          *.aarch64.dmg.sha512sum\n    - name: Upload macOS x86_64 artifacts\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: application-macos-x86_64.zip\n        if-no-files-found: error\n        path: |\n          *.x86_64.dmg\n          *.x86_64.dmg.sha512sum\n    - name: Upload Windows artifacts\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: application-win32.zip\n        if-no-files-found: error\n        path: |\n          *.msi\n          *.msi.sha512sum\n    - name: Upload Linux artifacts\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: application-linux.zip\n        if-no-files-found: error\n        path: |\n          rancher-desktop-linux-*.zip\n          rancher-desktop-linux-*.zip.sha512sum\n    - name: Upload Linux AppImage\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: application-linux.AppImage\n        if-no-files-found: error\n        path: |\n          rancher-desktop.AppImage\n          rancher-desktop.AppImage.sha512sum\n\n  smoke-test:\n    name: Smoke test\n    needs: download-artifacts\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - { platform: macos-aarch64, runs-on: macos-14 }\n        - { platform: macos-x86_64, runs-on: macos-15-intel }\n        - { platform: win32, runs-on: windows-latest }\n        - { platform: linux, runs-on: ubuntu-latest }\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n\n    - name: Set up environment\n      uses: ./.github/actions/setup-environment\n\n    - name: \"Linux: Set startup command\"\n      if: runner.os == 'Linux'\n      run: echo \"EXEC_COMMAND=$EXEC_COMMAND\" >> \"$GITHUB_ENV\"\n      env:\n        EXEC_COMMAND: >-\n          exec xvfb-run --auto-servernum\n          --server-args='-screen 0 1280x960x24'\n\n    - name: Set log directory\n      shell: bash\n      # Use node here to do path manipulation to get correct Windows paths.\n      run: >-\n        node --eval='console.log(\"RD_LOGS_DIR=\" + require(\"path\").join(process.cwd(), \"logs\"));'\n        >> \"$GITHUB_ENV\"\n\n    - name: Download artifacts\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: application-${{ matrix.platform }}.zip\n    - run: ${{ env.EXEC_COMMAND }} .github/workflows/smoke-test/smoke-test.sh\n      shell: bash\n    - name: Upload logs\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: always()\n      with:\n        name: logs-${{ matrix.platform }}.zip\n        path: ${{ github.workspace }}/logs\n        if-no-files-found: warn\n\n  repository-smoke-test:\n    name: Smoke test repository\n    needs: download-artifacts\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - { id: opensuse-tumbleweed, image: \"registry.opensuse.org/opensuse/tumbleweed:latest\"}\n        - { id: opensuse-leap, image: \"registry.opensuse.org/opensuse/leap:latest\" }\n        - { id: debian, image: \"debian:latest\" }\n        - { id: fedora, image: \"fedora:latest\" }\n    runs-on: ubuntu-latest\n    container:\n      image: ${{ matrix.image }}\n      options: --privileged\n    steps:\n    - name: Install basic tools\n      if: contains(matrix.id, 'opensuse')\n      run: >-\n        zypper --non-interactive install\n        git-core tar\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - name: Set up user\n      run: |\n        useradd --create-home --user-group ci-user\n        export LOGS_DIR=$PWD/logs\n        export RD_LOGS_DIR=$LOGS_DIR/rd\n        echo \"LOGS_DIR=$LOGS_DIR\" >> \"$GITHUB_ENV\"\n        echo \"RD_LOGS_DIR=$RD_LOGS_DIR\" >> \"$GITHUB_ENV\"\n        mkdir -p $RD_LOGS_DIR\n        chown --recursive ci-user \"$LOGS_DIR\"\n    - uses: ./.github/actions/setup-environment\n    - name: Install Rancher Desktop from package\n      if: runner.os == 'Linux'\n      run: .github/workflows/smoke-test/install-from-repo.sh\n      env:\n        RD_VERSION: ${{ needs.download-artifacts.outputs.release-tag }}\n    - name: \"openSUSE Workaround for #9145\"\n      if: contains(matrix.id, 'opensuse')\n      run: >-\n        zypper --non-interactive install\n        qemu-img qemu-hw-display-virtio-gpu\n    - name: Run smoke test\n      shell: bash\n      run: |\n        inner_command=(\n          xvfb-run\n            --auto-servernum\n            --server-args='-screen 0 1280x960x24'\n          $PWD/.github/workflows/smoke-test/smoke-test.sh\n        )\n        sudo --user=ci-user --login --set-home --non-interactive \\\n          /usr/bin/env --chdir=$PWD \\\n            RD_DEBUG_ENABLED=1 RD_TEST=smoke RD_SKIP_INSTALL=true RD_LOGS_DIR=$RD_LOGS_DIR \\\n          script \\\n            --log-out $LOGS_DIR/repo-${{ matrix.id }}.log \\\n            --return --command \"${inner_command[*]@Q}\"\n\n    - name: Take screenshot\n      if: failure()\n      continue-on-error: true\n      shell: >-\n        sudo --user=ci-user --login --set-home --non-interactive\n        bash --noprofile --norc -eo pipefail {0}\n      run: |\n        set -o xtrace -o errexit\n        PID=$(pidof smoke-test.sh || echo missing)\n        if [[ ! -r /proc/$PID/environ ]]; then\n          echo \"Rancher Desktop is not running\" >&2\n          exit 0\n        fi\n        export $(gawk 'BEGIN { RS=\"\\0\"; FS=\"=\" } ($1 == \"DISPLAY\" || $1 == \"XAUTHORITY\") { print }' \\\n          < /proc/$PID/environ)\n        env\n        export MAGICK_DEBUG=All # spellcheck-ignore-line\n        gm import -window root -verbose $LOGS_DIR/screenshot-${{ matrix.id }}.png\n\n    - name: Upload logs\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: always()\n      with:\n        name: logs-repo-${{ matrix.id }}.zip\n        path: ${{ github.workspace }}/logs\n        if-no-files-found: warn\n\n  appimage-smoke-test:\n    name: Smoke test AppImage\n    needs: download-artifacts\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n        - { id: opensuse, image: \"registry.opensuse.org/opensuse/tumbleweed:latest\" }\n        - { id: rocky, image: \"rockylinux/rockylinux:9\" }\n    runs-on: ubuntu-latest\n    container:\n      image: ${{ matrix.image }}\n      options: --privileged\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - name: Set up user\n      run: |\n        useradd --create-home --user-group ci-user\n        export LOGS_DIR=$PWD/logs\n        export RD_LOGS_DIR=$LOGS_DIR/rd\n        echo \"LOGS_DIR=$LOGS_DIR\" >> \"$GITHUB_ENV\"\n        echo \"RD_LOGS_DIR=$RD_LOGS_DIR\" >> \"$GITHUB_ENV\"\n        mkdir -p $RD_LOGS_DIR\n        chown --recursive ci-user \"$LOGS_DIR\"\n    - uses: ./.github/actions/setup-environment\n      with:\n        user: ci-user\n\n    - name: Download AppImage\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: application-linux.AppImage\n\n    - name: Run smoke test\n      run: |\n        inner_command=(\n          xvfb-run\n            --auto-servernum\n            --server-args='-screen 0 1280x960x24'\n          $PWD/.github/workflows/smoke-test/smoke-test.sh\n        )\n        sudo --user=ci-user --login --set-home --non-interactive \\\n          /usr/bin/env --chdir=$PWD \\\n            RD_DEBUG_ENABLED=1 RD_TEST=smoke RD_LOGS_DIR=$RD_LOGS_DIR \\\n          script \\\n            --log-out $LOGS_DIR/appimage-${{ matrix.id }}.log \\\n            --return --command \"${inner_command[*]@Q}\" \\\n\n    - name: Take screenshot\n      if: failure()\n      continue-on-error: true\n      shell: >-\n        sudo --user=ci-user --login --set-home --non-interactive\n        bash --noprofile --norc -eo pipefail {0}\n      run: |\n        set -o xtrace -o errexit\n        PID=$(pidof rancher-desktop.AppImage || echo missing)\n        if [[ ! -r /proc/$PID/environ ]]; then\n          echo \"Rancher Desktop is not running\" >&2\n          exit 0\n        fi\n        export $(gawk 'BEGIN { RS=\"\\0\"; FS=\"=\" } ($1 == \"DISPLAY\" || $1 == \"XAUTHORITY\") { print }' \\\n          < /proc/$PID/environ)\n        env\n        export MAGICK_DEBUG=All # spellcheck-ignore-line\n        gm import -window root -verbose $LOGS_DIR/screenshot-${{ matrix.id }}.png\n\n    - name: Upload logs\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: always()\n      with:\n        name: logs-appimage-${{ matrix.id }}.zip\n        path: ${{ github.workspace }}/logs\n        if-no-files-found: warn\n"
  },
  {
    "path": ".github/workflows/spelling.yml",
    "content": "name: Check Spelling\n\n# Comment management is handled through a secondary job, for details see:\n# https://github.com/check-spelling/check-spelling/wiki/Feature%3A-Restricted-Permissions\n#\n# `jobs.comment-push` runs when a push is made to a repository and the `jobs.spelling` job needs to make a comment\n#   (in odd cases, it might actually run just to collapse a comment, but that's fairly rare)\n#   it needs `contents: write` in order to add a comment.\n#\n# `jobs.comment-pr` runs when a pull_request is made to a repository and the `jobs.spelling` job needs to make a comment\n#   or collapse a comment (in the case where it had previously made a comment and now no longer needs to show a comment)\n#   it needs `pull-requests: write` in order to manipulate those comments.\n\n# Updating pull request branches is managed via comment handling.\n# For details, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-expect-list\n#\n# These elements work together to make it happen:\n#\n# `on.issue_comment`\n#   This event listens to comments by users asking to update the metadata.\n#\n# `jobs.update`\n#   This job runs in response to an issue_comment and will push a new commit\n#   to update the spelling metadata.\n#\n# `with.experimental_apply_changes_via_bot`\n#   Tells the action to support and generate messages that enable it\n#   to make a commit to update the spelling metadata.\n#\n# `with.ssh_key`\n#   In order to trigger workflows when the commit is made, you can provide a\n#   secret (typically, a write-enabled github deploy key).\n#\n#   For background, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-Update-with-deploy-key\n\n# SARIF reporting\n#\n# Access to SARIF reports is generally restricted (by GitHub) to members of the repository.\n#\n# Requires enabling `security-events: write`\n# and configuring the action with `use_sarif: 1`\n#\n#   For information on the feature, see: https://github.com/check-spelling/check-spelling/wiki/Feature:-SARIF-output\n\n# Minimal workflow structure:\n#\n# on:\n#   push:\n#     ...\n# jobs:\n#   # you only want the spelling job, all others should be omitted\n#   spelling:\n#     # remove `security-events: write` and `use_sarif: 1`\n#     # remove `experimental_apply_changes_via_bot: 1`\n#     ... otherwise, adjust the `with:` as you wish\n\n# on.pull_request(_target).edited is only needed for with.check_commit_messages: title | description\n\non:\n  push:\n    branches:\n    - \"**\"\n    tags-ignore:\n    - \"**\"\n  pull_request:\n    branches:\n    - \"**\"\n    types:\n    - 'opened'\n    - 'reopened'\n    - 'synchronize'\n\npermissions: {}\n\njobs:\n  spelling:\n    name: Check Spelling\n    permissions:\n      contents: read\n      pull-requests: read\n      actions: read\n      security-events: write # To be able to write SARIF events\n    runs-on: ubuntu-latest\n    if: ${{ contains(github.event_name, 'pull_request') || github.event_name == 'push' }}\n    concurrency:\n      group: spelling-${{ github.event.pull_request.number || github.ref }}\n      # note: If you use only_check_changed_files, you do not want cancel-in-progress\n      cancel-in-progress: true\n    env:\n      UPLOAD_SARIF_LIMITED: '' # Set by `yarn lint:spelling`.\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n\n    # We don't actually need the full `yarn install`; we just do enough to set\n    # up `yarn` to get `yarn lint:spelling` to work.\n    - name: Drop all dependencies\n      run: |\n        yq --inplace '.dependencies = {} | .devDependencies = {}' package.json\n        rm -f yarn.lock\n    - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0\n      with:\n        node-version-file: package.json\n    - run: corepack enable yarn\n    - run: yarn install --no-immutable --mode=skip-build\n\n    - run: sudo apt-get install cpanminus\n\n    - name: Check Spelling\n      run: yarn lint:spelling\n      env:\n        GITHUB_TOKEN: ${{ github.token }} # Needed to generate SARIF reports.\n        RD_LINT_SPELLING: 1\n\n    - name: Upload SARIF report\n      # Use the limited report since if we have more than 25k errors nobody is\n      # going read through it all anyway.\n      if: always() && env.UPLOAD_SARIF_LIMITED != ''\n      continue-on-error: true\n      uses: github/codeql-action/upload-sarif@v4\n      with:\n        category: check-spelling\n        sarif_file: ${{ env.UPLOAD_SARIF_LIMITED }}\n"
  },
  {
    "path": ".github/workflows/test.yaml",
    "content": "name: Test\n\non:\n  push: {}\n  pull_request: {}\n\npermissions: {}\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - run: yarn build\n    - run: yarn lint:nofix\n    - name: Install shfmt\n      run: go install mvdan.cc/sh/v3/cmd/shfmt@latest\n    - run: make -C bats lint\n    - run: yarn test\n  lint:\n    strategy:\n      matrix:\n        # We run the Linux lint in the `test` flow, no need to repeat it.\n        runs-on: [windows-latest, macos-latest]\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n    - if: runner.os == 'Windows'\n      name: Configure git to use Unix line endings\n      run: |\n        git config --global core.autocrlf false\n        git config --global core.eol lf\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - run: yarn license-check\n    - run: ./scripts/go-license-check.sh\n      shell: bash\n    - run: yarn lint:nofix\n"
  },
  {
    "path": ".github/workflows/ucmonitor.yaml",
    "content": "name: Check for unreleased changes\non:\n  schedule:\n    - cron: '48 8 * * *'\n  workflow_dispatch: {}\n\npermissions:\n  issues: write\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  check-unreleased-changes:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 0\n\n      - uses: ./.github/actions/yarn-install\n\n      - run: yarn ucmonitor\n        env:\n          GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n"
  },
  {
    "path": ".github/workflows/upgrade-generate.yaml",
    "content": "name: Generate Upgrade Test Data\non:\n  workflow_dispatch: {}\npermissions:\n  contents: read\njobs:\n  build:\n    strategy:\n      matrix:\n        include:\n        - platform: mac\n          arch: x86_64\n          runs-on: macos-15-intel\n        - platform: mac\n          arch: aarch64\n          runs-on: macos-latest\n        - platform: win\n          runs-on: windows-latest\n    runs-on: ${{ matrix.runs-on }}\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n        # Needed to run `git describe` to get full version info\n        fetch-depth: 0\n    - uses: ./.github/actions/yarn-install\n    - run: yarn build\n    - run: yarn package\n    - name: Upload Windows installer\n      if: runner.os == 'Windows'\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: Rancher Desktop Setup.msi\n        path: dist/Rancher.Desktop*.msi\n        if-no-files-found: error\n    - if: runner.os == 'Windows'\n      run: cat dist/electron-builder.yaml\n    - name: Upload Windows build information\n      if: runner.os == 'Windows'\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      with:\n        name: build-info.yml\n        path: dist/electron-builder.yaml\n        if-no-files-found: error\n    - name: Upload macOS archive\n      uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n      if: matrix.platform == 'mac'\n      with:\n        name: Rancher Desktop-mac.${{ matrix.arch }}.zip\n        path: dist/Rancher Desktop*.zip\n        if-no-files-found: error\n  release:\n    runs-on: ubuntu-latest\n    needs: build\n    permissions:\n      contents: write\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        persist-credentials: false\n    - uses: ./.github/actions/yarn-install\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        ref: gh-pages\n        path: pages\n        persist-credentials: true\n    - name: Download installer (msi)\n      id: msi\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: Rancher Desktop Setup.msi\n        path: RD_SETUP_MSI\n    - name: Download mac x86_64 archive\n      id: mac_x86_64\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: Rancher Desktop-mac.x86_64.zip\n        path: MACX86_ZIP\n    - name: Download mac aarch64 archive\n      id: mac_aarch64\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: Rancher Desktop-mac.aarch64.zip\n        path: MACARM_ZIP\n    - name: Download build information\n      id: info\n      uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1\n      with:\n        name: build-info.yml\n        path: RD_BUILD_INFO\n    - run: node scripts/ts-wrapper.js scripts/populate-update-server.ts\n      env:\n        RD_SETUP_MSI: ${{ steps.msi.outputs.download-path }}\n        RD_MACX86_ZIP: ${{ steps.mac_x86_64.outputs.download-path }}\n        RD_MACARM_ZIP: ${{ steps.mac_aarch64.outputs.download-path }}\n        RD_BUILD_INFO: ${{ steps.info.outputs.download-path }}\n        RD_OUTPUT_DIR: ${{ github.workspace }}/pages\n        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        GITHUB_ACTOR: ${{ github.actor }}\n"
  },
  {
    "path": ".github/workflows/windows-e2e.yaml",
    "content": "name: e2e tests on Windows\n\non:\n  workflow_dispatch:\n  push:\n    branches-ignore:\n    - 'dependabot/**'\n  pull_request: {}\n\ndefaults:\n  run:\n    shell: powershell\njobs:\n  check-paths:\n    uses: ./.github/workflows/paths-ignore.yaml\n  e2e-tests:\n    needs: check-paths\n    if: needs.check-paths.outputs.should-run == 'true'\n    timeout-minutes: 90\n    runs-on: windows-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          persist-credentials: false\n      - uses: ./.github/actions/setup-environment\n      - uses: ./.github/actions/yarn-install\n      - name: Run e2e Tests\n        run: yarn test:e2e\n        env:\n          RD_DEBUG_ENABLED: '1'\n      - name: Upload failure reports\n        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0\n        if: always()\n        with:\n          name: e2etest-artifacts\n          path: ./e2e/reports/*\n"
  },
  {
    "path": ".github/workflows/yarn-dedupe.yaml",
    "content": "name: Deduplicate yarn.lock\n\non:\n  schedule:\n  - cron: '0 9 1 * *'\n  workflow_dispatch: {}\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  check-for-token:\n    outputs:\n      has-token: ${{ steps.calc.outputs.HAS_SECRET }}\n    runs-on: ubuntu-latest\n    steps:\n    - id: calc\n      run: echo \"HAS_SECRET=${HAS_SECRET}\" >> \"${GITHUB_OUTPUT}\"\n      env:\n        HAS_SECRET: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW != '' }}\n\n  yarn-dedupe:\n    needs: check-for-token\n    if: needs.check-for-token.outputs.has-token == 'true'\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n      with:\n        fetch-depth: 0\n    - uses: ./.github/actions/yarn-install\n    - run: ./scripts/yarn-dedupe.sh --push\n      env:\n        GITHUB_TOKEN: ${{ secrets.RUN_WORKFLOW_FROM_WORKFLOW }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*.exe\n/bats/bats.tar.gz\n/bats/bin/\n/bats/logs/\n/coverage/\n/dist/\n/e2e/reports/\n/go.work.sum\n/node_modules/\n/resources/cert-manager*\n/resources/darwin/\n/resources/host/\n/resources/linux/*\n!/resources/linux/rancher-desktop.desktop\n/resources/preload.js*\n/resources/rancher-dashboard/\n/resources/rdx-proxy.tar\n/resources/spin-operator*\n/resources/win32/\n/screenshots/output/\n/src/go/rdctl/pkg/options/generated/*.go\n/e2e/e2e/test-results/.last-run.json\n/.yarn/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"bats/bats-core\"]\n\tpath = bats/bats-core\n\turl = https://github.com/rancher-sandbox/bats-core.git\n\tbranch = master\n[submodule \"bats/bats-assert\"]\n\tpath = bats/bats-assert\n\turl = https://github.com/rancher-sandbox/bats-assert.git\n\tbranch = master\n[submodule \"bats/bats-support\"]\n\tpath = bats/bats-support\n\turl = https://github.com/rancher-sandbox/bats-support.git\n\tbranch = master\n[submodule \"bats/bats-file\"]\n\tpath = bats/bats-file\n\turl = https://github.com/rancher-sandbox/bats-file.git\n\tbranch = master\n"
  },
  {
    "path": ".golangci.yaml",
    "content": "version: \"2\"\nlinters:\n  enable:\n    - bodyclose\n    - copyloopvar\n    - dogsled\n    - dupl\n    - errcheck\n    - goconst\n    - gocritic\n    - goprintffuncname\n    - gosec\n    - govet\n    - ineffassign\n    - misspell\n    - mnd\n    - nakedret\n    - noctx\n    - nolintlint\n    - staticcheck\n    - unconvert\n    - unparam\n    - unused\n    - whitespace\n  settings:\n    dupl:\n      threshold: 100\n    goconst:\n      min-len: 2\n      min-occurrences: 3\n    gocritic:\n      disabled-checks:\n        - dupImport # https://github.com/go-critic/go-critic/issues/845\n        - ifElseChain\n        - unnamedResult\n      enabled-tags:\n        - diagnostic\n        - experimental\n        - opinionated\n        - performance\n        - style\n    gosec:\n      excludes:\n        # Taint analysis rules (G7xx) produce only false positives in this codebase\n        - G702 # command injection via taint analysis - stub binaries forwarding args\n        - G703 # path traversal via taint analysis - os.CreateTemp paths\n        - G704 # SSRF via taint analysis - hardcoded URLs\n        - G705 # XSS via taint analysis - stdout writes\n        - G706 # log injection via taint analysis - internal log calls\n\n        # G115 flags every int↔uintptr cast on file descriptors (os.NewFile,\n        # syscall.Shutdown, IoctlFileClone). File descriptors are always\n        # non-negative and these casts are idiomatic Go.\n        - G115 # integer overflow conversion\n        # G117 matches exported struct fields named \"Password\" etc. The\n        # ConnectionInfo.Password field in rdctl/pkg/config is intentional.\n        - G117 # exported field matches secret pattern\n      config:\n        G306: \"0644\"\n    mnd:\n      # don't include the \"operation\" and \"assign\"\n      checks:\n        - argument\n        - case\n        - condition\n        - return\n      ignored-numbers:\n        - \"0\"\n        - \"1\"\n        - \"2\"\n        - \"3\"\n      ignored-functions:\n        - ^make$\n        - ^net\\.IPv4$\n        - ^os\\.FileMode$\n        - ^os\\.Mkdir(?:All)?$\n        - ^os\\.(?:Open|Write)File$\n        - ^strings\\.SplitN$\n        - ^tabwriter\\.NewWriter$\n        - ^utils\\.GetParentDir$\n    nolintlint:\n      allow-unused: false # report any unused nolint directives\n      require-explanation: true\n      require-specific: true\n  exclusions:\n    generated: lax\n    presets:\n      - comments\n      - common-false-positives\n      - legacy\n      - std-error-handling\n    rules:\n      - linters:\n          - errcheck\n          - gocritic\n          - gosec\n        path: _test\\.go\n      - # Exclude bodyclose when it's passed to client.ProcessRequestForAPI\n        # or client.ProcessRequestForUtility which internally closes the body.\n        linters:\n          - bodyclose\n        path: src/go/rdctl/\n        source: client.ProcessRequestFor(API|Utility)\\(rdClient.DoRequest(WithPayload)?\\(\n      - # Exclude ST1005 when it encounters errors starting with proper noun\n        linters:\n          - staticcheck\n        path: src/go/wsl-helper/cmd/kubeconfig.go\n        text: 'ST1005:'\n        source: errors.New\\(\"Windows\n      - # Exclude ST1005 when it encounters errors starting with proper noun\n        linters:\n          - staticcheck\n        path: src/go/rdctl/pkg/lock/lock.go\n        text: 'ST1005:'\n        source: fmt.Errorf\\(\"Rancher Desktop\n      - # Exclude the FIXME comments from upstream\n        linters:\n          - gocritic\n        path: src/go/wsl-helper/pkg/dockerproxy/platform/vsock_linux\\.go\n        text: todoCommentWithoutDetail\n      - # Ignore errors from syscall\n        linters:\n          - dogsled\n        source: ^\\s*_, _, _ = .*\\.Call\\(\n      - # Ignore foreign constants\n        linters:\n          - staticcheck\n        path: src/go/rdctl/pkg/process/process_darwin.go\n        text: 'ST1003:'\n        source: ^\\s*(CTL_KERN|KERN_PROCARGS)\\s*=\n      - # Ignore foreign constants\n        linters:\n          - staticcheck\n        path: src/go/rdctl/pkg/process/process_windows.go\n        text: 'ST1003:'\n        source: ^\\s*type\\s+[A-Z0-9_]+\\s+struct\n      - # Ignore foreign constants\n        linters:\n          - staticcheck\n        path: src/go/rdctl/pkg/process/process_windows.go\n        text: 'ST1003:'\n        source: ^\\s*[A-Z0-9_]+\\s+=\n      - # Don't de-duplicate different commands.\n        linters:\n          - dupl\n        path: src/go/rdctl/cmd/extension(Install|Uninstall)\\.go$\n      - # Don't use %q for registry files to avoid escaping backslashes\n        linters:\n          - gocritic\n        path: src/go/rdctl/pkg/reg/reg.go\n        text: 'sprintfQuotedString:'\n      - # This seems inconsistent across platforms\n        path: src/go/nerdctl-stub/main_shared.go\n        linters: [ unparam ]\n        text: \\bresult\\b.*\\bis always nil\\b\n        source: func mountArgProcessor\n\nformatters:\n  enable:\n    - gofmt\n    - gci\n  exclusions:\n    generated: lax\n  settings:\n    gci:\n      sections:\n      - standard\n      - default\n      - prefix(github.com/rancher-sandbox/rancher-desktop) # localmodule for go.work\n"
  },
  {
    "path": ".yarnrc.yml",
    "content": "enableScripts: false\nnodeLinker: node-modules\n\nplugins:\n  - path: .yarn/plugins/plugin-rancher-desktop-license-checker.cjs\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Rancher Desktop\n\nRancher Desktop accepts contributions via GitHub pull requests.\nThis document outlines the process to get your pull request accepted.\n\n## Start With An Issue\n\nPrior to creating a pull request it is a good idea to [create an issue].\nThis is especially true if the change request is something large.\nThe bug, feature request, or other type of issue can be discussed prior to\ncreating the pull request. This can reduce rework.\n\n[create an issue]: https://github.com/rancher-sandbox/rancher-desktop/issues/new\n\n## Sign Your Commits\n\nA sign-off is a line at the end of the explanation for a commit.\nAll commits must be signed. Your signature certifies that you wrote the patch\nor otherwise have the right to contribute the material. When you sign off you\nagree to the following rules\n(from [developercertificate.org](https://developercertificate.org/)):\n\n```\nDeveloper Certificate of Origin\nVersion 1.1\n\nCopyright (C) 2004, 2006 The Linux Foundation and its contributors.\n1 Letterman Drive\nSuite D4700\nSan Francisco, CA, 94129\n\nEveryone is permitted to copy and distribute verbatim copies of this\nlicense document, but changing it is not allowed.\n\nDeveloper's Certificate of Origin 1.1\n\nBy making a contribution to this project, I certify that:\n\n(a) The contribution was created in whole or in part by me and I\n    have the right to submit it under the open source license\n    indicated in the file; or\n\n(b) The contribution is based upon previous work that, to the best\n    of my knowledge, is covered under an appropriate open source\n    license and I have the right under that license to submit that\n    work with modifications, whether created in whole or in part\n    by me, under the same open source license (unless I am\n    permitted to submit under a different license), as indicated\n    in the file; or\n\n(c) The contribution was provided directly to me by some other\n    person who certified (a), (b) or (c) and I have not modified\n    it.\n\n(d) I understand and agree that this project and the contribution\n    are public and that a record of the contribution (including all\n    personal information I submit with it, including my sign-off) is\n    maintained indefinitely and may be redistributed consistent with\n    this project or the open source license(s) involved.\n```\n\nThen you add a line to every git commit message:\n\n    Signed-off-by: Joe Smith <joe.smith@example.com>\n\nUse your real name (sorry, no pseudonyms or anonymous contributions).\n\nIf you set your `user.name` and `user.email` git configs, you can sign your\ncommit automatically with `git commit -s`.\n\nNote: If your git config information is set properly then viewing the `git log`\ninformation for your commit will look something like this:\n\n```\nAuthor: John Smith <john.smith@example.com>\nDate:   Thu Feb 2 11:41:15 2018 -0800\n\n    Update README\n\n    Signed-off-by: John Smith <john.smith@example.com>\n```\n\nNotice the `Author` and `Signed-off-by` lines match. If they don't your PR will\nbe rejected by the automated DCO check.\n\n## Pull Requests\n\nPull requests for a code change should reference the issue they are related to.\nThis will enable issues to serve as a central point of reference for a change.\nFor example, if a pull request fixes or completes an issue, the commit or\npull request should include:\n\n```md\nCloses #123\n```\n\nIn this case 123 is the corresponding issue number.\n\n### When End-To-End Tests Fail\n\nEvery pull request triggers a full run of testing in the CI system.\nThe failures reported by the code style checker (aka the \"linter\") and the unit tests are usually\nclear and easy to fix (and can be avoided by running `yarn test` locally before creating a commit).\nBut when an integration, or e2e test, fails, it's sometimes useful to consult the log files\nfor the run.\n\n1. Click on the _Details_ link next to the failing E2E test notification, and\n   navigate to the summary view of the test.\n\n   ![Failure summary screenshot](docs/assets/images/contributing/e2e-summary.png)\n2. From the bottom of the summary view, locate the `failure-reports.zip` link\n   and download it.  (You must be logged in to GitHub to be able to download\n   that file.)\n\n   ![Failure reports screenshot](docs/assets/images/contributing/e2e-failure-reports.png)\n3. Extract that file to find the logs; they are in directories named after each\n   test.  For example, a subset of the log files may include:\n   ```\n   $ ls -l\n   total 62204\n   drwxr-xr-x 29 nobody nobody      928 Oct 12  2020 backend.e2e.spec.ts-logs\n   -rw-r--r--  1 nobody nobody 31616936 Oct 12  2020 backend.e2e.spec.ts-pw-trace.zip\n   $ ls backend.e2e.spec.ts-logs/\n   background.log         k8s.log             networking.log\n   commandLine.log        kube.log            protocol-handler.log\n   dashboardServer.log    lima.ha.stderr.log  server.log\n   deploymentProfile.log  lima.ha.stdout.log  settings.log\n   diagnostics.log        lima.log            shortcuts.log\n   extensions.log         lima.serial.log     steve.log\n   images.log             moby.log            update.log\n   integrations.log       mock.log            window_browser.log\n   k3s.log                nerdctl.log         wsl.log\n   ```\n4. It may be useful to go to https://trace.playwright.dev/ to examine the\n   Playwright traces; they are the files named `*-pw-trace.zip`.  This can be\n   useful for seeing the state of the UI when waiting for elements to appear,\n   disappear, etc.\n\n## Semantic Versioning\n\nRancher Desktop follows [semantic versioning](https://semver.org/).\n\nThis does not cover Kubernetes or other tools provided by Rancher Desktop.\nKubernetes has its own [release versioning](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/release/versioning.md#kubernetes-release-versioning)\nscheme that looks like SemVer but is semantically different.\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# Rancher Desktop\n\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rancher-sandbox/rancher-desktop)\n\nRancher Desktop is an open-source project that brings Kubernetes and\ncontainer management to the desktop. It runs on Windows, macOS and\nLinux. This README pertains to the development of Rancher Desktop.\nFor user-oriented information about Rancher Desktop, please see [rancherdesktop.io][home].\nFor user-oriented documentation, please see [docs.rancherdesktop.io][docs].\n\n[home]: https://rancherdesktop.io\n[docs]: https://docs.rancherdesktop.io\n\n\n## Overview\n\nRancher Desktop is an Electron application that is mainly written in TypeScript.\nIt bundles a variety of other technologies in order to provide one cohesive application.\nIt includes a command line tool, `rdctl`, which is written in Go.\nMost developer activities, such as running a development build, building/packaging\nRancher Desktop, running unit tests, and running end-to-end tests, are done through\n`yarn` scripts. Some exceptions exist, such as running BATS tests.\n\n\n## Setup\n\n### Windows\n\nThere are two options for building from source on Windows: with a\n[Development VM Setup](#development-vm-setup) or\n[Manual Development Environment Setup](#manual-development-environment-setup)\nwith an existing Windows installation.\n\n\n#### Development VM Setup\n\n1. Download a Microsoft Windows 10 [development virtual machine].\n   All of the following steps should be done in that virtual machine.\n2. Open a PowerShell prompt (hit Windows Key + `X` and open\n   `Windows PowerShell`).\n3. Run the [automated setup script]:\n\n   ```powershell\n   Set-ExecutionPolicy RemoteSigned -Scope CurrentUser\n   iwr -useb 'https://github.com/rancher-sandbox/rancher-desktop/raw/main/scripts/windows-setup.ps1' | iex\n   ```\n\n4. Close the privileged PowerShell prompt.\n5. Ensure `msbuild_path` and `msvs_version` are configured correctly in `.npmrc` file. Run the following commands to set these properties:\n\n   ```\n   npm config set msvs_version <visual-studio-version-number>\n   npm config set msbuild_path <path/to/MSBuild.exe>\n   ```\n\n   For example for Visual Studio 2022:\n\n   ```\n   npm config set msvs_version 2022\n   npm config set msbuild_path \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n   ```\n\n   If you get an error message when trying to run `npm config set...`, run `npm config edit` and then add lines like\n\n   ```\n   msvs_version=2022\n   msbuild_path=C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\n   ```\n\n   Do not quote the values to the right side of the equal sign. The quotes aren't needed, and it's possible that some\n   processors will treat them as literal parts of the path, and then fail.\n7. Configure `git` to work with linux- and macos-originated files:\n   ```\n   git config --global --replace-all core.autocrlf false\n   git config --global --replace-all core.eol lf\n   ```\nIf you find the `lint:go` tests are failing mysteriously, it's possible that the line-endings are incorrect.\n\nYou can now clone the repository and run `yarn`.\n\n[development virtual machine]: https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/\n[automated setup script]: ./scripts/windows-setup.ps1\n\n\n#### Manual Development Environment Setup\n\n1. Install [Windows Subsystem for Linux (WSL)] on your machine. Skip this step, if WSL is already installed.\n2. Open a PowerShell prompt (hit Windows Key + `X` and open `Windows PowerShell`).\n3. Install [Scoop] via `iwr -useb get.scoop.sh | iex`.\n4. Install 7zip, git, go, mingw, nvm, and unzip via `scoop install 7zip git go mingw nvm python unzip`.\n   Check node version with `nvm list`. If node v22 is not installed or set as the current version, then install using `nvm install 22` and set as current using `nvm use 22.xx.xx`.\n5. Install the yarn package manager via `npm install --global yarn`\n6. Install Visual Studio 2017 or higher. As of this writing the latest version is available at [https://visualstudio.microsoft.com/downloads/]; if that's changed, a good search engine should find it.\n7. Make sure you have the `Windows SDK` component installed. This [Visual Studio docs] describes steps to install components.\n   The [Desktop development with C++] workload needs to be selected, too.\n8. Configure `git` to work with linux- and macos-originated files:\n   ```\n   git config --global --replace-all core.autocrlf false\n   git config --global --replace-all core.eol lf\n   ```\nIf you find the `lint:go` tests are failing mysteriously, it's possible that the line-endings are incorrect.\n9. Ensure `msbuild_path` and `msvs_version` are configured correctly in `.npmrc` file. Run the following commands to set these properties:\n\n   ```\n   npm config set msvs_version <visual-studio-version-number>\n   npm config set msbuild_path <path/to/MSBuild.exe>\n   ```\n\n   For example for Visual Studio 2022:\n\n   ```\n   npm config set msvs_version 2022\n   npm config set msbuild_path \"C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\"\n   ```\n\n   If you get an error message when trying to run `npm config set...`, run `npm config edit` and then add lines like\n\n   ```\n   msvs_version=2022\n   msbuild_path=C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\Community\\MSBuild\\Current\\Bin\\MSBuild.exe\n   ```\n\n   Do not quote the values to the right side of the equal sign. They aren't needed, and it's possible that some\n   processor will treat them as literal parts of the path, and then fail.\n\nYou can now clone the repository and run `yarn`.\n\n[Scoop]: https://scoop.sh/\n[Visual Studio docs]: https://docs.microsoft.com/en-us/visualstudio/install/modify-visual-studio?view=vs-2022\n[Windows Subsystem for Linux (WSL)]: https://docs.microsoft.com/en-us/windows/wsl/install\n[Desktop development with C++]: https://learn.microsoft.com/en-us/visualstudio/install/modify-visual-studio?view=vs-2022#change-workloads-or-individual-components\n\n### macOS\n\nInstall `nvm` to get Node.js and npm:\n\nSee https://github.com/nvm-sh/nvm#installing-and-updating and run the `curl` or `wget`\ncommand to install nvm.\n\nNote that this script adds code dealing with `nvm` to a profile file\n(like `~/.bash_profile`). To add access to `nvm` to a current shell session,\nyou'll need to `source` that file.\n\nCurrently we build Rancher Desktop with Node 22. To install it, run:\n\n```\nnvm install 22.14\n```\n\nNext, you'll need to install the yarn package manager:\n\n```\nnpm install --global yarn\n```\n\nYou'll also need to run `brew install go` if you haven't installed go.\n\nThen you can install dependencies with:\n```\nyarn\n```\n\n> ### ⚠️ Working on a mac with an M1 chip?\n>\n> You will need to set the `M1` environment variable before installing dependencies and running any npm scripts:\n>\n> ```\n> export M1=1\n> yarn\n> ```\n>\n> You will want to run `git clean -fdx` to clean out any cached assets and re-downloaded with the correct arch before running `yarn` if you previously installed dependencies without setting `M1` first.\n\n### Linux\n\nEnsure you have the following installed:\n\n- [Node.js][Node.js] v22. **Make sure you have any development packages\n  installed.** For example, on openSUSE Leap 15.6 you would need to install\n  `nodejs22` and `nodejs22-devel`.\n\n- [yarn classic][yarn-classic]\n\n- Go 1.22 or later.\n\n- Dependencies described in the [`node-gyp` docs][node-gyp] installation.\n  This is required to install the [`ffi-napi`][ffi-napi] npm package. These docs mention\n  \"a proper C/C++ compiler toolchain\". You can install `gcc` and `g++` for this.\n\nThen you can install dependencies with:\n\n```\nyarn\n```\n\nYou can then run Rancher Desktop as described below. It may fail on the first run -\nif this happens, try doing a factory reset and re-running, which has been known\nto solve this issue.\n\n[Node.js]: https://nodejs.org/\n[ffi-napi]: https://www.npmjs.com/package/ffi-napi\n[node-gyp]: https://github.com/nodejs/node-gyp#on-unix\n[yarn-classic]: https://classic.yarnpkg.com/lang/en/docs/install/#debian-stable\n\n\n## Running\n\nOnce you have your dependencies installed you can run a development version\nof Rancher Desktop with:\n\n```\nyarn dev\n```\n\n\n## Tests\n\nTo run the unit tests:\n\n```\nyarn test\n```\n\nTo run the integration tests:\n\n```\nyarn test:e2e\n```\n\n\n## Building\n\nRancher can be built from source on Windows, macOS or Linux.\nCross-compilation is currently not supported. To run a build do:\n\n```\nyarn build\nyarn package\n```\n\nThe build output goes to `dist/`.\n\n### Debugging builds with the Chrome remote debugger\n\nThe Chrome remote debugger allows you to debug Electron apps using Chrome Developer Tools. You can use it to access log messages that might output to the developer console of the renderer process. This is especially helpful for getting additional debug information in production builds of Rancher Desktop.\n\n#### Starting Rancher Desktop with Remote Debugging Enabled\n\nTo enable remote debugging, start Rancher Desktop with the `--remote-debugging-port` argument.\n\nOn Linux, start Rancher Desktop with the following command:\n\n``` bash\nrancher-desktop --remote-debugging-port=\"8315\" --remote-allow-origins=http://localhost:8315\n```\n\nOn macOS, start Rancher Desktop with the following command:\n\n```\n/Applications/Rancher\\ Desktop.app/Contents/MacOS/Rancher\\ Desktop --remote-debugging-port=\"8315\" --remote-allow-origins=http://localhost:8315\n```\n\nOn Windows, start Rancher Desktop with the following command:\n\n``` powershell\ncd 'C:\\Program Files\\Rancher Desktop\\'\n& '.\\Rancher Desktop.exe' --remote-debugging-port=\"8315\" --remote-allow-origins=http://localhost:8315\n```\n\nAfter Rancher Desktop starts, open Chrome and navigate to `http://localhost:8315/`. Select the available target to start remote debugging Rancher Desktop.\n\n![image](https://github.com/rak-phillip/rancher-desktop/assets/835961/4f5fcb33-d381-4900-a836-685eab3af441)\n\n![image](https://github.com/rak-phillip/rancher-desktop/assets/835961/91b4ee63-7093-4377-b8b3-f2f4a57a16a7)\n\n#### Remote Debugging an Extension\n\nTo remote debug an extension, follow the same process as remote debugging a build. However, you will need to load an extension before navigating to `http://localhost:8315/`. Both Rancher Desktop and the loaded extension should be listed as available targets.\n\n![image](https://github.com/rak-phillip/rancher-desktop/assets/835961/71bb7eec-38e5-4744-a547-ebb36048918a)\n\n![image](https://github.com/rak-phillip/rancher-desktop/assets/835961/f4aad3e1-dabc-473e-9404-05609216cd03)\n\n### Debugging dev env with GoLand\n\nThe following steps have been tested with GoLand on Linux but might work for other\nJetBrains IDEs in a similar way.\n\n1. Install the Node.js plugin (via `File > Settings > Plugins`)\n\n   ![image](https://github.com/s0nea/rancher-desktop/assets/8761082/f9574abb-06d9-4132-a14b-c3d445e87f7d)\n\n2. Go to the \"Run/Debug Configurations\" dialog (via `Run > Edit Configurations...`)\n3. Add a new Node.js configuration with the following settings:\n   - Name: a name for the debug configuration, e.g. `rancher desktop`\n   - Node interpreter: choose your installed node interpreter, e.g. `/usr/bin/node`\n   - Node parameters: `scripts/ts-wrapper.js scripts/dev.ts`\n   - Working directory: choose the working directory of your project, e.g.\n     `~/src/rancher-desktop`\n\n   ![image](https://github.com/s0nea/rancher-desktop/assets/8761082/41686095-04ba-4d9e-bac1-b5587d146381)\n\n4. Save the configuration\n5. You can now set a breakpoint and click \"Debug 'rancher desktop'\" to start debugging\n\n   ![image](https://github.com/s0nea/rancher-desktop/assets/8761082/87ea45f4-0a4d-4a52-9f3b-866c45e3fe2a)\n\n\n## Development Builds\n\n### Windows and macOS\n\nEach commit triggers a GitHub Actions run that results in application bundles\n(`.exe`s and `.dmg`s) being uploaded as artifacts. This can be useful if you\nwant to test the latest build of Rancher Desktop as built by the build system.\nYou can download these artifacts from the Summary page of completed `package`\nactions.\n\n\n### Linux\n\nSimilar to Windows and macOS, Linux builds of Rancher Desktop are made from each\ncommit. However on Linux, only part of the process is done by GitHub Actions.\nThe final part of it is done by [Open Build Service][OBS].\n\nThere are two channels of the Rancher Desktop repositories: `dev` and `stable`.\n`stable` is the channel that most users use. It is the one that users are\ninstructed to add in the official [documentation][docs], and the one that contains\nbuilds that are created from official releases. `dev` is the channel that we are\ninterested in here: it contains builds created from the latest commit made on\nthe `main` branch, and on any branches that match the format `release-*`. To\nlearn how to install the development repositories, see below.\n\nWhen using the `dev` repositories, it is important to understand the format of\nthe versions of Rancher Desktop available from the `dev` repositories.\nThe versions are in the format:\n\n```\n<priority>.<branch>.<commit_time>.<commit>\n```\n\nwhere:\n\n`priority` is a meaningless number that exists to give versions built from the `main`\nbranch priority over versions built from the `release-*` branches when updating.\n\n`branch` is the branch name; dashes are removed due to constraints imposed by\npackage formats.\n\n`commit_time` is the UNIX timestamp of the commit used to make the build.\n\n`commit` is the shortened hash of the commit used to make the build.\n\n[docs]: https://docs.rancherdesktop.io\n[OBS]: https://build.opensuse.org/\n\n\n#### `.deb` Development Repository\n\nYou can add the repo with the following steps:\n\n```\ncurl -s https://download.opensuse.org/repositories/isv:/Rancher:/dev/deb/Release.key | gpg --dearmor | sudo dd status=none of=/usr/share/keyrings/isv-rancher-dev-archive-keyring.gpg\necho 'deb [signed-by=/usr/share/keyrings/isv-rancher-dev-archive-keyring.gpg] https://download.opensuse.org/repositories/isv:/Rancher:/dev/deb/ ./' | sudo dd status=none of=/etc/apt/sources.list.d/isv-rancher-dev.list\nsudo apt update\n```\n\nYou can see available versions with:\n\n```\napt list -a rancher-desktop\n```\n\nOnce you find the version you want to install you can install it with:\n\n```\nsudo apt install rancher-desktop=<version>\n```\n\nThis works even if you already have a version of Rancher Desktop installed.\n\n\n#### `.rpm` Development Repository\n\nYou can add the repo with:\n\n```\nsudo zypper addrepo https://download.opensuse.org/repositories/isv:/Rancher:/dev/rpm/isv:Rancher:dev.repo\nsudo zypper refresh\n```\n\nYou can see available versions with:\n\n```\nzypper search -s rancher-desktop\n```\n\nFinally, install the version you want with:\n\n```\nzypper install --oldpackage rancher-desktop=<version>\n```\n\nThis works even if you already have a version of Rancher Desktop installed.\n\n\n#### Development AppImages\n\nThere are no repositories for AppImages, but you can access the [latest development AppImage builds].\n\n[latest development AppImage builds]: https://download.opensuse.org/repositories/isv:/Rancher:/dev/AppImage/\n\n## API\n\nRancher Desktop supports a limited HTTP-based API. The API is defined in\n`pkg/rancher-desktop/assets/specs/command-api.yaml`, and you can see examples of how it's\ninvoked in the client code at `go/src/rdctl`.\n\n### Stability\n\nThe API is currently at version 1, but is still considered internal and experimental, and\nis subject to change without any advance notice. At some point we expect that necessary\nchanges to the API will go through a warning and deprecation notice.\n\n## Contributing\n\nPlease see [the document about contributing](CONTRIBUTING.md).\n\n\n## Further Reading\n\nPlease see the [docs](docs/development/) directory for further developer documentation.\n"
  },
  {
    "path": "babel.config.cjs",
    "content": "const packageJson = require('./package.json');\n\nconst electronVersion = parseInt(/\\d+/.exec(packageJson.devDependencies.electron), 10);\n\nmodule.exports = {\n  presets: [\n    [\n      '@vue/cli-plugin-babel/preset',\n      { useBuiltIns: false },\n    ],\n    [\n      '@babel/preset-env',\n      {\n        targets: {\n          node:     'current',\n          electron: electronVersion,\n        },\n      },\n    ],\n  ],\n  env: {\n    test: {\n      presets: [\n        ['@babel/env',\n          { targets: { node: 'current' } },\n        ],\n      ],\n    },\n  },\n  plugins: [\n    '@babel/plugin-proposal-class-properties',\n    '@babel/plugin-proposal-nullish-coalescing-operator',\n    '@babel/plugin-proposal-optional-chaining',\n    '@babel/plugin-proposal-private-methods',\n    '@babel/plugin-proposal-private-property-in-object',\n  ],\n};\n"
  },
  {
    "path": "background.ts",
    "content": "import { spawn } from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\n\nimport Electron, { MessageBoxOptions, nativeTheme } from 'electron';\nimport _ from 'lodash';\nimport semver from 'semver';\n\nimport { State } from '@pkg/backend/backend';\nimport BackendHelper from '@pkg/backend/backendHelper';\nimport K8sFactory from '@pkg/backend/factory';\nimport { getImageProcessor } from '@pkg/backend/images/imageFactory';\nimport { ImageProcessor } from '@pkg/backend/images/imageProcessor';\nimport * as K8s from '@pkg/backend/k8s';\nimport { Steve } from '@pkg/backend/steve';\nimport { FatalCommandLineOptionError, LockedFieldError, updateFromCommandLine } from '@pkg/config/commandLineOptions';\nimport { Help } from '@pkg/config/help';\nimport * as settings from '@pkg/config/settings';\nimport * as settingsImpl from '@pkg/config/settingsImpl';\nimport { TransientSettings } from '@pkg/config/transientSettings';\nimport { IntegrationManager, getIntegrationManager } from '@pkg/integrations/integrationManager';\nimport { PathManagementStrategy, PathManager } from '@pkg/integrations/pathManager';\nimport { getPathManagerFor } from '@pkg/integrations/pathManagerImpl';\nimport { BackendState, CommandWorkerInterface, HttpCommandServer } from '@pkg/main/commandServer/httpCommandServer';\nimport SettingsValidator from '@pkg/main/commandServer/settingsValidator';\nimport { ContainerExecHandler } from '@pkg/main/containerExec';\nimport { HttpCredentialHelperServer } from '@pkg/main/credentialServer/httpCredentialHelperServer';\nimport { DashboardServer } from '@pkg/main/dashboardServer';\nimport { DeploymentProfileError, readDeploymentProfiles } from '@pkg/main/deploymentProfiles';\nimport { DiagnosticsManager, DiagnosticsResultCollection } from '@pkg/main/diagnostics/diagnostics';\nimport { ExtensionErrorCode, isExtensionError } from '@pkg/main/extensions';\nimport { ImageEventHandler } from '@pkg/main/imageEvents';\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport mainEvents from '@pkg/main/mainEvents';\nimport buildApplicationMenu from '@pkg/main/mainmenu';\nimport setupNetworking from '@pkg/main/networking';\nimport { Snapshots } from '@pkg/main/snapshots/snapshots';\nimport { Snapshot, SnapshotDialog } from '@pkg/main/snapshots/types';\nimport { Tray } from '@pkg/main/tray';\nimport setupUpdate from '@pkg/main/update';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport getCommandLineArgs from '@pkg/utils/commandLine';\nimport dockerDirManager from '@pkg/utils/dockerDirManager';\nimport { isDevEnv } from '@pkg/utils/environment';\nimport Logging, { clearLoggingDirectory, setLogLevel } from '@pkg/utils/logging';\nimport { fetchMacOsVersion, getMacOsVersion } from '@pkg/utils/osVersion';\nimport paths from '@pkg/utils/paths';\nimport { protocolsRegistered, setupProtocolHandlers } from '@pkg/utils/protocols';\nimport { executable } from '@pkg/utils/resources';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';\nimport { getVersion } from '@pkg/utils/version';\nimport getWSLVersion from '@pkg/utils/wslVersion';\nimport * as window from '@pkg/window';\nimport { closeDashboard, openDashboard } from '@pkg/window/dashboard';\nimport { openPreferences, preferencesSetDirtyFlag } from '@pkg/window/preferences';\n\n// https://www.electronjs.org/docs/latest/breaking-changes#changed-gtk-4-is-default-when-running-gnome\nif (process.platform === 'linux') {\n  Electron.app.commandLine.appendSwitch('gtk-version', '3');\n}\n\nElectron.app.setPath('userData', path.join(paths.appHome, 'electron'));\nElectron.app.setPath('cache', paths.cache);\nElectron.app.setAppLogsPath(paths.logs);\n\nconst console = Logging.background;\n\n// Do an early check for debugging enabled via the environment variable so that\n// we can turn on extra logging to troubleshoot startup issues.\nif (settingsImpl.runInDebugMode(false)) {\n  setLogLevel('debug');\n}\n\nif (!Electron.app.requestSingleInstanceLock()) {\n  process.exit(201);\n}\n\nclearLoggingDirectory();\n\nconst SNAPSHOT_OPERATION = 'Snapshot operation in progress';\n\nconst ipcMainProxy = getIpcMainProxy(console);\nconst k8smanager = newK8sManager();\nconst diagnostics: DiagnosticsManager = new DiagnosticsManager();\n\nlet cfg: settings.Settings;\nlet firstRunDialogComplete = false;\nlet gone = false; // when true indicates app is shutting down\nlet imageEventHandler: ImageEventHandler | null = null;\nlet containerExecHandler: ContainerExecHandler | null = null;\nlet currentContainerEngine = settings.ContainerEngine.NONE;\nlet currentImageProcessor: ImageProcessor | null = null;\nlet enabledK8s: boolean;\nlet pathManager: PathManager;\nconst integrationManager: IntegrationManager = getIntegrationManager();\nlet noModalDialogs = false;\n// Indicates whether the UI should be locked, settings changes should be disallowed\n// and possibly other things should be disallowed. As of the time of writing,\n// set to true when a snapshot is being created or restored.\nlet deploymentProfiles: settings.DeploymentProfileType = { defaults: {}, locked: {} };\n\n/**\n * pendingRestartContext is needed because with the CLI it's possible to change\n * the state of the system without using the UI.  This can push the system out\n * of sync, for example setting kubernetes-enabled=true while it's disabled.\n * Normally the code restarts the system when processing the SET command, but if\n * the backend is currently starting up or shutting down, we have to wait for it\n * to finish.  This module gets a `state-changed` event when that happens,\n * and if this flag is true, a new restart can be triggered.\n */\nlet pendingRestartContext: CommandWorkerInterface.CommandContext | undefined;\n\nlet httpCommandServer: HttpCommandServer | null = null;\nconst httpCredentialHelperServer = new HttpCredentialHelperServer();\n\nif (process.platform === 'linux') {\n  // On Linux, put Electron into a new process group so that we can more\n  // reliably kill processes we spawn from extensions.\n  import('posix-node').then(({ default: { setpgid } }) => {\n    setpgid?.(0, 0);\n  }).catch(ex => {\n    console.error(`Ignoring error setting process group: ${ ex }`);\n  });\n}\n\n// Scheme must be registered before the app is ready\nElectron.protocol.registerSchemesAsPrivileged([\n  { scheme: 'app', privileges: { secure: true, standard: true } },\n]);\n\nprocess.on('unhandledRejection', (reason: any, promise: any) => {\n  if (reason.code === 'ECONNREFUSED' && reason.port === cfg.kubernetes.port) {\n    // Do nothing: a connection to the kubernetes server was broken\n  } else {\n    console.error('UnhandledRejectionWarning:', reason);\n  }\n});\n\nElectron.app.on('second-instance', async() => {\n  await protocolsRegistered;\n  console.warn('A second instance was started');\n  if (firstRunDialogComplete) {\n    window.openMain();\n  }\n});\n\n// takes care of any propagation of settings we want to do\n// when settings change\nmainEvents.on('settings-update', async(newSettings) => {\n  console.log(`mainEvents settings-update: ${ JSON.stringify(newSettings) }`);\n  nativeTheme.themeSource = newSettings.application.theme;\n  const runInDebugMode = settingsImpl.runInDebugMode(newSettings.application.debug);\n\n  if (runInDebugMode) {\n    setLogLevel('debug');\n  } else {\n    setLogLevel('info');\n  }\n  k8smanager.debug = runInDebugMode;\n\n  if (gone) {\n    console.debug('Suppressing settings-update because app is quitting');\n\n    return;\n  }\n\n  await setPathManager(newSettings.application.pathManagementStrategy);\n  await pathManager.enforce();\n\n  if (newSettings.application.hideNotificationIcon) {\n    Tray.getInstance(cfg).hide();\n  } else {\n    if (firstRunDialogComplete) {\n      Tray.getInstance(cfg).show();\n    }\n    mainEvents.emit('k8s-check-state', k8smanager);\n  }\n\n  await runRdctlSetup(newSettings);\n  window.send('preferences/changed');\n});\n\nmainEvents.handle('settings-fetch', () => {\n  return Promise.resolve(cfg);\n});\n\nElectron.protocol.registerSchemesAsPrivileged([{ scheme: 'app' }, {\n  scheme:     'x-rd-extension',\n  privileges: {\n    standard:            true,\n    secure:              true,\n    bypassCSP:           true,\n    allowServiceWorkers: true,\n    supportFetchAPI:     true,\n    corsEnabled:         true,\n  },\n}]);\n\nElectron.app.whenReady().then(async() => {\n  try {\n    const commandLineArgs = getCommandLineArgs();\n\n    // Normally `noModalDialogs` is set when we call `updateFromCommandLine(.., commandLineArgs)`\n    // But if there's an error either in that function, or before, we'll need to know if we should\n    // display the error in a modal-dialog or not. So check the current command-line arguments for that.\n    //\n    // It's very unlikely that a string option is set to this exact string though.\n    // `rdctl start --images.namespace --no-modal-dialogs`\n    // is syntactically correct, but unlikely (because why would someone create a\n    // containerd namespace called \"--no-modal-dialogs\"?\n    noModalDialogs = commandLineArgs.includes('--no-modal-dialogs');\n    setupProtocolHandlers();\n\n    // make sure we have the macOS version cached before calling getMacOsVersion()\n    if (os.platform() === 'darwin') {\n      await fetchMacOsVersion(console);\n    }\n\n    // Needs to happen before any file is written; otherwise, that file\n    // could be owned by root, which will lead to future problems.\n    if (['linux', 'darwin'].includes(os.platform())) {\n      await checkForRootPrivs();\n    }\n    // Check for required OS versions and features\n    await checkPrerequisites();\n\n    DashboardServer.getInstance().init();\n\n    await setupNetworking();\n\n    try {\n      deploymentProfiles = await readDeploymentProfiles();\n    } catch (ex: any) {\n      if (ex instanceof DeploymentProfileError) {\n        await handleFailure(ex);\n      } else {\n        console.log(`Got an unexpected deployment profile error ${ ex }`, ex);\n      }\n\n      throw ex;\n    }\n    try {\n      cfg = settingsImpl.load(deploymentProfiles);\n      nativeTheme.themeSource = cfg.application.theme;\n      settingsImpl.updateLockedFields(deploymentProfiles.locked);\n    } catch (err: any) {\n      const titlePart = err.name || 'Failed to load settings';\n      const message = err.message || err.toString();\n\n      showErrorDialog(titlePart, message, true);\n\n      // showErrorDialog doesn't exit immediately; avoid running the rest of the function\n      return;\n    }\n    try {\n      // The profile loader did rudimentary type-validation on profiles, but the validator checks for things\n      // like invalid strings for application.pathManagementStrategy.\n      validateEarlySettings(settings.defaultSettings, deploymentProfiles.defaults, {});\n      validateEarlySettings(settings.defaultSettings, deploymentProfiles.locked, {});\n\n      if (commandLineArgs.length) {\n        cfg = updateFromCommandLine(cfg, settingsImpl.getLockedSettings(), commandLineArgs);\n        k8smanager.noModalDialogs = noModalDialogs = TransientSettings.value.noModalDialogs;\n      }\n    } catch (err: any) {\n      noModalDialogs = TransientSettings.value.noModalDialogs;\n      if (err instanceof LockedFieldError || err instanceof DeploymentProfileError || err instanceof FatalCommandLineOptionError) {\n        handleFailure(err).catch((err2: any) => {\n          console.log('Internal error trying to show a failure dialog: ', err2);\n          process.exit(2);\n        });\n\n        // Avoid running the rest of the `whenReady` handler after calling this handleFailure -- shutdown is imminent\n        return;\n      } else if (!noModalDialogs) {\n        showErrorDialog('Invalid command-line arguments', err.message, false);\n      }\n      console.log(`Failed to update command from argument ${ commandLineArgs.join(', ') }`, err);\n    }\n\n    httpCommandServer = new HttpCommandServer(new BackgroundCommandWorker());\n    await httpCommandServer.init();\n    await httpCredentialHelperServer.init();\n\n    await initUI();\n    await checkForBackendLock();\n    await setPathManager(cfg.application.pathManagementStrategy);\n    await integrationManager.enforce();\n\n    mainEvents.emit('settings-update', cfg);\n\n    // Set up the updater; we may need to quit the app if an update is already\n    // queued.\n    if (await setupUpdate(cfg.application.updater.enabled, true)) {\n      gone = true;\n      // The update code will trigger a restart; don't do it here, as it may not\n      // be ready yet.\n      console.log('Will apply update; skipping startup.');\n\n      return;\n    }\n\n    try {\n      await dockerDirManager.ensureCredHelperConfigured();\n    } catch (ex: any) {\n      const errorTitle = 'Error configuring credential helper';\n\n      console.error(`${ errorTitle }:`, ex);\n\n      const title = ex.title ?? errorTitle;\n      const message = ex.message ?? ex.toString();\n\n      showErrorDialog(title, message, true);\n    }\n\n    diagnostics.runChecks().catch(console.error);\n\n    await startBackend();\n  } catch (ex: any) {\n    console.error(`Error starting up: ${ ex }`, ex.stack);\n    gone = true;\n    Electron.app.quit();\n  }\n});\n\nasync function setPathManager(newStrategy: PathManagementStrategy) {\n  if (pathManager) {\n    if (pathManager.strategy === newStrategy) {\n      return;\n    }\n    await pathManager.remove();\n  }\n  pathManager = getPathManagerFor(newStrategy);\n}\n\n/**\n * Reads the 'backend.lock' file and returns its contents if it exists.\n * Returns null if the file doesn't exist.\n */\nasync function readBackendLockFile(): Promise<{ action: string } | null> {\n  try {\n    const fileContents = await fs.promises.readFile(\n      path.join(paths.appHome, 'backend.lock'),\n      'utf-8',\n    );\n\n    return JSON.parse(fileContents);\n  } catch (ex: any) {\n    if (ex.code === 'ENOENT') {\n      return null;\n    } else {\n      throw ex;\n    }\n  }\n}\n\n/**\n * Emits the 'backend-locked-update' event.\n */\nfunction updateBackendLockState(backendIsLocked: string, action?: string): void {\n  mainEvents.emit('backend-locked-update', backendIsLocked, action);\n}\n\n/**\n * Checks for the existence of the 'backend.lock' file and emits the\n * 'backend-locked-update' event to notify listeners about the current lock\n * status.\n */\nasync function doesBackendLockExist(): Promise<boolean> {\n  let backendIsLocked: string;\n\n  const lockFileContents = await readBackendLockFile();\n\n  if (lockFileContents !== null) {\n    backendIsLocked = SNAPSHOT_OPERATION;\n    updateBackendLockState(backendIsLocked, lockFileContents.action);\n  } else {\n    backendIsLocked = '';\n    updateBackendLockState(backendIsLocked);\n  }\n\n  return !!backendIsLocked;\n}\n\n/**\n * Blocks execution until the 'backend.lock' file is no longer present.\n */\nasync function checkForBackendLock() {\n  // Perform an initial check for a lock file\n  if (!await doesBackendLockExist()) {\n    return;\n  }\n\n  const startTime = Date.now();\n\n  // Check every second if a lock file exists\n  while (await doesBackendLockExist()) {\n    // Notify when a lock file has existed for more than 5 minutes\n    if (Date.now() - startTime >= 300_000) {\n      mainEvents.emit(\n        'dialog-info',\n        {\n          dialog:  'SnapshotsDialog',\n          infoKey: 'snapshots.info.lock.info',\n        },\n      );\n    }\n    await util.promisify(setTimeout)(1_000);\n  }\n}\n\nasync function initUI() {\n  await doFirstRunDialog();\n\n  if (gone) {\n    console.log('User triggered quit during first-run');\n\n    return;\n  }\n\n  buildApplicationMenu();\n\n  Electron.app.setAboutPanelOptions({\n    // TODO: Update this to 2021-... as dev progresses\n    // also needs to be updated in electron-builder.yml\n    copyright:          'Copyright © 2021-2026 SUSE LLC',\n    applicationName:    `${ Electron.app.name } by SUSE`,\n    applicationVersion: `Version ${ await getVersion() }`,\n    iconPath:           path.join(paths.resources, 'icons', 'logo-square-512.png'),\n  });\n\n  if (!cfg.application.hideNotificationIcon) {\n    Tray.getInstance(cfg).show();\n  }\n\n  if (!cfg.application.startInBackground) {\n    window.openMain();\n  } else if (Electron.app.dock) {\n    Electron.app.dock.hide();\n  }\n}\n\nasync function doFirstRunDialog() {\n  if (!noModalDialogs && settingsImpl.firstRunDialogNeeded()) {\n    await window.openFirstRunDialog();\n  }\n  firstRunDialogComplete = true;\n}\n\nasync function checkForRootPrivs() {\n  if (isRoot()) {\n    await window.openDenyRootDialog();\n    gone = true;\n    Electron.app.quit();\n  }\n}\n\nasync function checkPrerequisites() {\n  const osPlatform = os.platform();\n  let messageId: window.reqMessageId = 'ok';\n  let args: any[] = [];\n\n  switch (osPlatform) {\n  case 'win32': {\n    // Required: Windows 10-1909(build 18363) or newer\n    const winRel = os.release().split('.');\n\n    if (Number(winRel[0]) < 10 || (Number(winRel[0]) === 10 && Number(winRel[2]) < 18363)) {\n      messageId = 'win32-release';\n    } else {\n      try {\n        const version = await getWSLVersion();\n\n        if (version.outdated_kernel) {\n          messageId = 'win32-kernel';\n          args = [version];\n        }\n      } catch (ex) {\n        console.error(`Failed to check WSL version, ignoring:`, ex);\n      }\n    }\n    break;\n  }\n  case 'linux': {\n    // TODO: This whole testing for nested virtualization is wrong. All we should test for is if\n    // hardware acceleration is available, e.g. checking /proc/cpuinfo for \"vmx\" (Intel) or \"svm\" (AMD).\n    if (process.arch === 'x64') {\n      // Required: Nested virtualization enabled\n      const nestedFiles = [\n        '/sys/module/kvm_amd/parameters/nested',\n        '/sys/module/kvm_intel/parameters/nested'];\n\n      messageId = 'linux-nested';\n      for (const nestedFile of nestedFiles) {\n        try {\n          const data = await fs.promises.readFile(nestedFile, { encoding: 'utf8' });\n\n          if (data && (data.toLowerCase().startsWith('y') || data.startsWith('1'))) {\n            messageId = 'ok';\n            break;\n          }\n        } catch {\n        }\n      }\n    }\n    break;\n  }\n  case 'darwin': {\n    // Required: macOS-10.15(Darwin-19) or newer\n    if (semver.gt('10.15.0', getMacOsVersion())) {\n      messageId = 'macOS-release';\n    }\n    break;\n  }\n  }\n\n  if (messageId !== 'ok') {\n    await window.openUnmetPrerequisitesDialog(messageId, ...args);\n    gone = true;\n    Electron.app.quit();\n  }\n}\n\n/**\n * Check if there are any reasons that would mean it makes no sense to continue\n * starting the app.  Should be invoked before attempting to start the backend.\n */\nasync function checkBackendValid() {\n  const invalidReason = await k8smanager.getBackendInvalidReason();\n\n  if (invalidReason) {\n    await handleFailure(invalidReason);\n    gone = true;\n    Electron.app.quit();\n  }\n}\n\n/**\n * Start the Kubernetes backend.\n *\n * @precondition cfg.kubernetes.version is set.\n */\nasync function startBackend() {\n  await checkBackendValid();\n\n  // A string describing why we're ignoring the request.\n  const ignoreReason = {\n    [K8s.State.STOPPED]:  undefined, // Normal start is accepted.\n    [K8s.State.STARTING]: 'Ignoring duplicate attempt to start backend while starting backend.',\n    [K8s.State.STARTED]:  'Ignoring attempt to start already-started backend.',\n    [K8s.State.STOPPING]: 'Ignoring attempt to start backend while stopping.',\n    [K8s.State.ERROR]:    undefined, // Attempting start from error state is fine.\n    [K8s.State.DISABLED]: 'Ignoring attempt to start already-started backend (Kubernetes disabled).',\n  }[k8smanager.state];\n\n  if (ignoreReason) {\n    console.debug(ignoreReason);\n\n    return;\n  }\n  try {\n    await startK8sManager();\n  } catch (err) {\n    handleFailure(err);\n  } finally {\n    window.send('extensions/changed');\n  }\n}\n\n/**\n * Start the backend.\n *\n * @note Callers are responsible for handling errors thrown from here.\n */\nasync function startK8sManager() {\n  const changedContainerEngine = currentContainerEngine !== cfg.containerEngine.name;\n\n  currentContainerEngine = cfg.containerEngine.name;\n  enabledK8s = cfg.kubernetes.enabled;\n\n  if (changedContainerEngine) {\n    setupImageProcessor();\n  }\n  await k8smanager.start(cfg);\n\n  const { initializeExtensionManager } = await import('@pkg/main/extensions/manager');\n\n  await initializeExtensionManager(k8smanager.containerEngineClient, cfg);\n  window.send('extensions/changed');\n\n  if (!containerExecHandler) {\n    containerExecHandler = new ContainerExecHandler(k8smanager.containerEngineClient);\n  } else {\n    containerExecHandler.updateClient(k8smanager.containerEngineClient);\n  }\n}\n\n/**\n * We need to deactivate the current imageProcessor, if there is one,\n * so it stops processing events,\n * and also tell the image event-handler about the new image processor.\n *\n * Some container engines support namespaces, so we need to specify the current namespace\n * as well. It should be done here so that the consumers of the `current-engine-changed`\n * event will operate in an environment where the image-processor knows the current namespace.\n */\n\nfunction setupImageProcessor() {\n  const imageProcessor = getImageProcessor(cfg.containerEngine.name, k8smanager);\n\n  currentImageProcessor?.deactivate();\n  if (!imageEventHandler) {\n    imageEventHandler = new ImageEventHandler(imageProcessor);\n  }\n\n  imageEventHandler.imageProcessor = imageProcessor;\n  currentImageProcessor = imageProcessor;\n  currentImageProcessor?.activate();\n  currentImageProcessor.namespace = cfg.images.namespace;\n  window.send('k8s-current-engine', cfg.containerEngine.name);\n}\n\ninterface K8sError {\n  errCode: number | string\n}\n\nfunction isK8sError(object: any): object is K8sError {\n  return 'errCode' in object;\n}\n\nElectron.app.on('before-quit', async(event) => {\n  if (gone) {\n    mainEvents.emit('quit');\n\n    return;\n  }\n  event.preventDefault();\n  httpCommandServer?.closeServer();\n  httpCredentialHelperServer.closeServer();\n\n  try {\n    await mainEvents.tryInvoke('extensions/shutdown');\n    await k8smanager?.stop();\n    await mainEvents.tryInvoke('shutdown-integrations');\n\n    console.log(`2: Child exited cleanly.`);\n  } catch (ex: any) {\n    console.log(`2: Child exited with code ${ isK8sError(ex) ? ex.errCode : (ex.errCode ?? '<unknown>') }`);\n    handleFailure(ex);\n  } finally {\n    gone = true;\n    if (process.env['APPIMAGE']) {\n      await integrationManager.removeSymlinksOnly();\n    }\n    Electron.app.quit();\n  }\n});\n\nElectron.app.on('window-all-closed', () => {\n  // On macOS, hide the dock icon.\n  Electron.app.dock?.hide();\n});\n\nElectron.app.on('activate', async() => {\n  // On macOS it's common to re-create a window in the app when the\n  // dock icon is clicked and there are no other windows open.\n  if (!firstRunDialogComplete) {\n    console.log('Still processing the first-run dialog: not opening main window');\n\n    return;\n  }\n  await protocolsRegistered;\n  window.openMain();\n});\n\nmainEvents.on('backend-locked-update', (backendIsLocked, action) => {\n  if (backendIsLocked) {\n    window.send('backend-locked', action);\n  } else {\n    window.send('backend-unlocked');\n  }\n});\n\nmainEvents.on('backend-locked-check', async() => {\n  await doesBackendLockExist();\n});\n\nipcMainProxy.on('backend-state-check', async() => {\n  await doesBackendLockExist();\n});\n\nipcMainProxy.on('settings-read', (event) => {\n  event.reply('settings-read', cfg);\n});\n\n// This is the synchronous version of the above; we still use\n// ipcRenderer.sendSync in some places, so it's required for now.\nipcMainProxy.on('settings-read', (event) => {\n  console.debug(`event settings-read in main: ${ JSON.stringify(cfg) }`);\n  event.returnValue = cfg;\n});\n\nipcMainProxy.on('images-namespaces-read', (event) => {\n  if ([K8s.State.STARTED, K8s.State.DISABLED].includes(k8smanager.state)) {\n    currentImageProcessor?.relayNamespaces();\n  }\n});\n\nipcMainProxy.on('dashboard-open', () => {\n  openDashboard();\n});\n\nipcMainProxy.on('dashboard-close', () => {\n  closeDashboard();\n});\n\nipcMainProxy.on('preferences-open', () => {\n  openPreferences();\n});\n\nipcMainProxy.on('preferences-close', () => {\n  window.getWindow('preferences')?.close();\n});\n\nipcMainProxy.on('preferences-set-dirty', (_event, dirtyFlag) => {\n  preferencesSetDirtyFlag(dirtyFlag);\n});\n\nipcMainProxy.on('get-debugging-statuses', () => {\n  window.send('is-debugging', settingsImpl.runInDebugMode(cfg.application.debug));\n  window.send('always-debugging', settingsImpl.runInDebugMode(false));\n});\nfunction writeSettings(arg: RecursivePartial<RecursiveReadonly<settings.Settings>>) {\n  settingsImpl.save(settingsImpl.merge(cfg, arg));\n  mainEvents.emit('settings-update', cfg);\n}\n\nipcMainProxy.handle('settings-write', (event, arg) => {\n  writeSettings(arg);\n\n  // dashboard requires kubernetes, so we want to close it if kubernetes is disabled\n  if (arg?.kubernetes?.enabled === false) {\n    closeDashboard();\n  }\n\n  event.sender.sendToFrame(event.frameId, 'settings-update', cfg);\n});\n\nmainEvents.on('settings-write', writeSettings);\n\nmainEvents.on('extensions/ui/uninstall', (id) => {\n  window.send('ok:extensions/uninstall', id);\n});\n\nmainEvents.on('dialog-info', (args) => {\n  window.getWindow(args.dialog)?.webContents.send('dialog/info', args);\n});\n\nipcMainProxy.on('extensions/open', (_event, id, path) => {\n  window.openExtension(id, path);\n});\n\nipcMainProxy.on('extensions/close', () => {\n  window.closeExtension();\n});\n\nipcMainProxy.handle('transient-settings-fetch', () => {\n  return Promise.resolve(TransientSettings.value);\n});\n\nipcMainProxy.handle('transient-settings-update', (event, arg) => {\n  TransientSettings.update(arg);\n});\n\nipcMainProxy.on('k8s-state', (event) => {\n  event.returnValue = k8smanager.state;\n});\n\nipcMainProxy.on('k8s-current-engine', () => {\n  window.send('k8s-current-engine', currentContainerEngine);\n});\n\nipcMainProxy.on('k8s-current-port', () => {\n  window.send('k8s-current-port', k8smanager.kubeBackend.desiredPort);\n});\n\nipcMainProxy.on('k8s-reset', async(_, arg) => {\n  await doK8sReset(arg, { interactive: true });\n});\n\nipcMainProxy.handle('api-get-credentials', () => mainEvents.invoke('api-get-credentials'));\n\nipcMainProxy.handle('get-locked-fields', () => settingsImpl.getLockedSettings());\n\nfunction backendIsBusy() {\n  return [K8s.State.STARTING, K8s.State.STOPPING].includes(k8smanager.state);\n}\n\nasync function doK8sReset(arg: 'fast' | 'wipe' | 'fullRestart', context: CommandWorkerInterface.CommandContext): Promise<void> {\n  // If not in a place to restart than skip it\n  if (backendIsBusy()) {\n    console.log(`Skipping reset, invalid state ${ k8smanager.state }`);\n\n    return;\n  }\n\n  try {\n    switch (arg) {\n    case 'fast':\n      await k8smanager.reset(cfg);\n      break;\n    case 'fullRestart':\n      await k8smanager.stop();\n      console.log(`Stopped Kubernetes backend cleanly.`);\n      await startK8sManager();\n      break;\n    case 'wipe':\n      console.log('Deleting VM to reset...');\n      await k8smanager.del();\n      console.log(`Deleted VM to reset exited cleanly.`);\n      await startK8sManager();\n      break;\n    }\n  } catch (ex) {\n    if (context.interactive) {\n      handleFailure(ex);\n    } else {\n      console.error(ex);\n    }\n  }\n}\n\nipcMainProxy.on('k8s-restart', async() => {\n  if (cfg.kubernetes.port !== k8smanager.kubeBackend.desiredPort) {\n    // On port change, we need to wipe the VM.\n    return doK8sReset('wipe', { interactive: true });\n  } else if (cfg.containerEngine.name !== currentContainerEngine || cfg.kubernetes.enabled !== enabledK8s) {\n    return doK8sReset('fullRestart', { interactive: true });\n  }\n  try {\n    switch (k8smanager.state) {\n    case K8s.State.STOPPED:\n    case K8s.State.STARTED:\n    case K8s.State.DISABLED:\n      // Calling start() will restart the backend, possible switching versions\n      // as a side-effect.\n      await startK8sManager();\n      break;\n    }\n  } catch (ex) {\n    handleFailure(ex);\n  }\n});\n\nipcMainProxy.on('k8s-versions', async() => {\n  try {\n    const versions = await k8smanager.kubeBackend.availableVersions;\n    const cachedOnly = await k8smanager.kubeBackend.cachedVersionsOnly();\n\n    window.send('k8s-versions', versions.map(v => v.versionEntry), cachedOnly);\n  } catch (ex) {\n    console.error(`Error handling k8s-versions: ${ ex }`);\n    window.send('k8s-versions', [], true);\n  }\n});\n\nipcMainProxy.on('k8s-progress', () => {\n  window.send('k8s-progress', k8smanager.progress);\n});\n\nipcMainProxy.handle('k8s-progress', () => {\n  return k8smanager.progress;\n});\n\nipcMainProxy.handle('service-fetch', (_, namespace) => {\n  return k8smanager.kubeBackend.listServices(namespace);\n});\n\nipcMainProxy.handle('service-forward', async(_, service, state) => {\n  const namespace = service.namespace ?? 'default';\n\n  if (state) {\n    const hostPort = service.listenPort ?? 0;\n\n    await doForwardPort(namespace, service.name, service.port, hostPort);\n  } else {\n    await doCancelForward(namespace, service.name, service.port);\n  }\n});\n\nasync function doForwardPort(namespace: string, service: string, k8sPort: string | number, hostPort: number) {\n  return await k8smanager.kubeBackend.forwardPort(namespace, service, k8sPort, hostPort);\n}\n\nasync function doCancelForward(namespace: string, service: string, k8sPort: string | number) {\n  return await k8smanager.kubeBackend.cancelForward(namespace, service, k8sPort);\n}\n\nipcMainProxy.on('k8s-integrations', async() => {\n  mainEvents.emit('integration-update', await integrationManager.listIntegrations() ?? {});\n});\n\nipcMainProxy.on('k8s-integration-set', (event, name, newState) => {\n  writeSettings({ WSL: { integrations: { [name]: newState } } });\n});\n\nmainEvents.on('integration-update', (state) => {\n  window.send('k8s-integrations', state);\n});\n\n/**\n * Do a factory reset of the application.  This will stop the currently running\n * cluster (if any), and delete all of its data.  This will also remove any\n * rancher-desktop data, and restart the application.\n *\n * We need to write out rdctl output to a temporary directory because the logs directory\n * will get removed by the factory-reset. This code writes out (to background.log) where this file\n * exists, but if the user isn't tailing that file they won't see the message.\n */\nasync function doFactoryReset(keepSystemImages: boolean) {\n  // Don't wait for this process to return -- the whole point is for us to not be running.\n  const tmpdir = os.tmpdir();\n  const outfile = await fs.promises.open(path.join(tmpdir, 'rdctl-stdout.txt'), 'w');\n  const args = ['reset', '--factory', `--cache=${ (!keepSystemImages) ? 'true' : 'false' }`];\n\n  if (cfg.application.debug) {\n    args.push('--verbose=true');\n  }\n  const rdctl = spawn(path.join(paths.resources, os.platform(), 'bin', 'rdctl'), args,\n    {\n      detached: true, windowsHide: true, stdio: ['ignore', outfile.fd, outfile.fd],\n    });\n\n  rdctl.unref();\n  console.debug(`If reset fails, the rdctl reset output files are in ${ tmpdir }`);\n}\n\nipcMainProxy.on('factory-reset', (event, keepSystemImages) => {\n  doFactoryReset(keepSystemImages);\n});\n\nipcMainProxy.on('show-logs', async(event) => {\n  const error = await Electron.shell.openPath(paths.logs);\n\n  if (error) {\n    const browserWindow = Electron.BrowserWindow.fromWebContents(event.sender);\n    const options: MessageBoxOptions = {\n      message: error,\n      type:    'error',\n      title:   `Error opening logs`,\n      detail:  `Please manually open ${ paths.logs }`,\n    };\n\n    console.error(`Failed to open logs: ${ error }`);\n    if (browserWindow) {\n      await Electron.dialog.showMessageBox(browserWindow, options);\n    } else {\n      await Electron.dialog.showMessageBox(options);\n    }\n  }\n});\n\nipcMainProxy.on('diagnostics/run', () => {\n  diagnostics.runChecks();\n});\n\nipcMainProxy.on('get-app-version', async(event) => {\n  event.reply('get-app-version', await getVersion());\n});\n\nipcMainProxy.on('snapshot', (event, args) => {\n  event.reply('snapshot', args);\n});\n\nipcMainProxy.on('snapshot/cancel', () => {\n  window.send('snapshot/cancel');\n});\n\nipcMainProxy.on('dialog/error', (event, args) => {\n  window.getWindow(args.dialog)?.webContents.send('dialog/error', args);\n});\n\nipcMainProxy.on('dialog/close', (_event, args) => {\n  window.getWindow(args.dialog)?.webContents.send('dialog/close', args);\n});\n\nipcMainProxy.handle('versions/macOs', () => {\n  return getMacOsVersion();\n});\n\nipcMainProxy.handle('host/isArm', () => {\n  return process.arch === 'arm64';\n});\n\nipcMainProxy.on('help/preferences/open-url', async() => {\n  Help.preferences.openUrl(await getVersion());\n});\n\nipcMainProxy.handle('show-message-box', (_event, options: Electron.MessageBoxOptions): Promise<Electron.MessageBoxReturnValue> => {\n  return window.showMessageBox(options, false);\n});\n\nipcMainProxy.handle('show-message-box-rd', async(_event, options: Electron.MessageBoxOptions, modal = false) => {\n  const mainWindow = modal ? window.getWindow('main') : null;\n\n  const dialog = window.openDialog(\n    'Dialog',\n    {\n      modal,\n      parent: mainWindow || undefined,\n      frame:  true,\n      title:  options.title,\n      height: 225,\n    });\n\n  let response: any;\n\n  dialog.webContents.on('ipc-message', (_event, channel, args) => {\n    if (channel === 'dialog/mounted') {\n      dialog.webContents.send('dialog/options', options);\n    }\n\n    if (channel === 'dialog/close') {\n      response = args || { response: options.cancelId };\n      dialog.close();\n    }\n  });\n\n  dialog.on('close', () => {\n    if (response) {\n      return;\n    }\n\n    response = { response: options.cancelId };\n  });\n\n  await (new Promise<void>((resolve) => {\n    dialog.on('closed', resolve);\n  }));\n\n  return response;\n});\n\nipcMainProxy.handle('show-snapshots-confirm-dialog', async(\n  event,\n  options: { window: Partial<Electron.MessageBoxOptions>, format: SnapshotDialog },\n) => {\n  const mainWindow = window.getWindow('main');\n\n  const dialog = window.openDialog(\n    'SnapshotsDialog',\n    {\n      title:   'Snapshots',\n      modal:   true,\n      parent:  mainWindow || undefined,\n      frame:   true,\n      movable: true,\n      height:  365,\n      width:   640,\n    });\n\n  if (os.platform() !== 'linux' && mainWindow && dialog) {\n    window.centerDialog(mainWindow, dialog, 0, 50);\n  }\n\n  let response: any;\n\n  dialog.webContents.on('ipc-message', (_event, channel, args) => {\n    if (channel === 'dialog/mounted') {\n      options.format.type = 'question';\n      dialog.webContents.send('dialog/options', options);\n    }\n\n    if (channel === 'dialog/close') {\n      response = args || { response: options.window.cancelId };\n      dialog.close();\n    }\n  });\n\n  dialog.on('close', () => {\n    if (response) {\n      return;\n    }\n\n    response = { response: options.window.cancelId };\n  });\n\n  await (new Promise<void>((resolve) => {\n    dialog.on('closed', resolve);\n  }));\n\n  return response;\n});\n\nipcMainProxy.handle('show-snapshots-blocking-dialog', async(\n  event,\n  options: { window: Partial<Electron.MessageBoxOptions>, format: SnapshotDialog },\n) => {\n  const dialogId = 'SnapshotsDialog';\n\n  if (window.getWindow(dialogId)) {\n    return;\n  }\n\n  const mainWindow = window.getWindow('main');\n\n  const dialog = window.openDialog(\n    dialogId,\n    {\n      modal:   true,\n      parent:  mainWindow || undefined,\n      frame:   false,\n      movable: false,\n      height:  500,\n      width:   700,\n    },\n    false);\n\n  const onMainWindowMove = () => {\n    if (mainWindow && dialog) {\n      window.centerDialog(mainWindow, dialog);\n    }\n  };\n\n  if (mainWindow && dialog) {\n    if (os.platform() === 'linux') {\n      /** Lock dialog position */\n      mainWindow.on('move', onMainWindowMove);\n    } else {\n      /** Center the dialog on main window, only for MacOs, Windows */\n      window.centerDialog(mainWindow, dialog, 0, 50);\n    }\n  }\n\n  let response: any;\n\n  dialog.webContents.on('ipc-message', (_event, channel, args) => {\n    if (channel === 'dialog/mounted') {\n      if (os.platform() !== 'darwin') {\n        mainWindow?.webContents.send('window/blur', true);\n      }\n\n      options.format.type = 'operation';\n      dialog.webContents.send('dialog/options', options);\n      event.sender.sendToFrame(event.frameId, 'dialog/mounted');\n    }\n\n    if (channel === 'dialog/close') {\n      response = args || { response: options.window.cancelId };\n      dialog.close();\n    }\n  });\n\n  dialog.on('close', () => {\n    if (os.platform() !== 'darwin') {\n      mainWindow?.webContents.send('window/blur', false);\n    }\n\n    if (os.platform() === 'linux' && mainWindow) {\n      mainWindow.off('move', onMainWindowMove);\n    }\n\n    if (response) {\n      return;\n    }\n\n    response = { response: options.window.cancelId };\n  });\n\n  await (new Promise<void>((resolve) => {\n    dialog.on('closed', resolve);\n  }));\n\n  return response;\n});\n\nfunction showErrorDialog(title: string, message: string, fatal?: boolean) {\n  if (noModalDialogs) {\n    console.log(`Fatal Error:\\n${ title }\\n\\n${ message }`);\n  } else {\n    Electron.dialog.showErrorBox(title, message);\n  }\n  if (fatal) {\n    Electron.app.quit();\n  }\n}\n\nasync function handleFailure(payload: any) {\n  let titlePart = 'Error Starting Rancher Desktop';\n  let message = 'There was an unknown error starting Rancher Desktop';\n  let secondaryMessage = '';\n\n  if (payload instanceof K8s.KubernetesError) {\n    ({ name: titlePart, message } = payload);\n  } else if (payload instanceof LockedFieldError) {\n    showErrorDialog(titlePart, payload.message, true);\n\n    return;\n  } else if (payload instanceof DeploymentProfileError) {\n    showErrorDialog('Failed to load the deployment profile', payload.message, true);\n\n    return;\n  } else if (payload instanceof FatalCommandLineOptionError) {\n    showErrorDialog('Error in command-line options', payload.message, true);\n\n    return;\n  } else if (payload instanceof Error) {\n    secondaryMessage = payload.toString();\n  } else if (typeof payload === 'number') {\n    message = `Rancher Desktop was unable to start with the following exit code: ${ payload }`;\n  } else if ('errorCode' in payload) {\n    message = payload.message || message;\n    titlePart = payload.context || titlePart;\n  }\n  console.log(`Rancher Desktop was unable to start:`, payload);\n  try {\n    // getFailureDetails is going to read from existing log files.\n    // Wait 1 second before reading them to allow recent writes to appear in them.\n    await util.promisify(setTimeout)(1_000);\n    const failureDetails: K8s.FailureDetails = await k8smanager.getFailureDetails(payload);\n\n    if (failureDetails) {\n      if (noModalDialogs) {\n        console.log(titlePart);\n        console.log(secondaryMessage || message);\n        // Since the log is to a file, we need to pretty-print it; otherwise the\n        // message will just be `[Object object]`.\n        console.log(JSON.stringify(failureDetails, undefined, 2));\n        gone = true;\n        Electron.app.quit();\n      } else {\n        await window.openKubernetesErrorMessageWindow(titlePart, secondaryMessage || message, failureDetails);\n      }\n\n      return;\n    }\n  } catch (e) {\n    console.log(`Failed to get failure details: `, e);\n  }\n  if (noModalDialogs) {\n    console.log(titlePart);\n    console.log(message);\n    gone = true;\n    Electron.app.quit();\n  } else {\n    showErrorDialog(titlePart, message, payload instanceof K8s.KubernetesError && payload.fatal);\n  }\n}\n\nfunction doFullRestart(context: CommandWorkerInterface.CommandContext) {\n  doK8sReset('fullRestart', context).catch((err: any) => {\n    console.log(`Error restarting: ${ err }`);\n  });\n}\n\nasync function getExtensionManager() {\n  const getEM = (await import('@pkg/main/extensions/manager')).default;\n\n  return await getEM();\n}\n\nfunction newK8sManager() {\n  const arch = process.arch === 'arm64' ? 'aarch64' : 'x86_64';\n  const mgr = K8sFactory(arch);\n\n  mgr.on('state-changed', async(state: K8s.State) => {\n    try {\n      mainEvents.emit('k8s-check-state', mgr);\n\n      if ([K8s.State.STARTED, K8s.State.DISABLED].includes(state)) {\n        if (!cfg.kubernetes.version) {\n          writeSettings({ kubernetes: { version: mgr.kubeBackend.version } });\n        }\n        currentImageProcessor?.relayNamespaces();\n\n        if (enabledK8s) {\n          await Steve.getInstance().start();\n        }\n      }\n\n      // Notify UI after Steve is ready, so the dashboard button is only enabled\n      // when Steve can accept connections.\n      window.send('k8s-check-state', state);\n\n      if (state === K8s.State.STOPPING) {\n        Steve.getInstance().stop();\n      }\n      if (pendingRestartContext !== undefined && !backendIsBusy()) {\n        // If we restart immediately the QEMU process in the VM doesn't always respond to a shutdown messages\n        setTimeout(doFullRestart, 2_000, pendingRestartContext);\n        pendingRestartContext = undefined;\n      }\n    } catch (ex) {\n      console.error(ex);\n    }\n  });\n\n  mgr.on('progress', () => {\n    window.send('k8s-progress', mgr.progress);\n  });\n\n  mgr.on('show-notification', (notificationOptions: Electron.NotificationConstructorOptions) => {\n    (new Electron.Notification(notificationOptions)).show();\n  });\n\n  mgr.kubeBackend.on('current-port-changed', (port: number) => {\n    window.send('k8s-current-port', port);\n  });\n\n  mgr.kubeBackend.on('service-changed', (services: K8s.ServiceEntry[]) => {\n    console.debug(`service-changed: ${ JSON.stringify(services) }`);\n    window.send('service-changed', services);\n  });\n\n  mgr.kubeBackend.on('service-error', (service: K8s.ServiceEntry, errorMessage: string) => {\n    console.debug(`service-error: ${ errorMessage }, ${ JSON.stringify(service) }`);\n    window.send('service-error', service, errorMessage);\n  });\n\n  mgr.kubeBackend.on('versions-updated', async() => {\n    const versions = await mgr.kubeBackend.availableVersions;\n    const cachedOnly = await mgr.kubeBackend.cachedVersionsOnly();\n\n    window.send('k8s-versions', versions.map(v => v.versionEntry), cachedOnly);\n  });\n\n  return mgr;\n}\n\nfunction validateEarlySettings(cfg: settings.Settings, newSettings: RecursivePartial<settings.Settings>, lockedFields: settings.LockedSettingsType): void {\n  // RD hasn't loaded the supported k8s versions yet, so have it defer actually checking the specified version.\n  // If it can't find this version, it will silently move to the closest version.\n  // We'd have to add more code to report that.\n  // It isn't worth adding that code yet. It might never be needed.\n  const newSettingsForValidation = _.omit(newSettings, 'kubernetes.version');\n  const [, errors] = new SettingsValidator().validateSettings(cfg, newSettingsForValidation, lockedFields);\n\n  if (errors.length > 0) {\n    throw new LockedFieldError(`Error in deployment profiles:\\n${ errors.join('\\n') }`);\n  }\n}\n\n/**\n * Implement the methods that HttpCommandServer needs to service its requests.\n * These methods do two things:\n * 1. Verify the semantics of the parameters (the server just checks syntax).\n * 2. Provide a thin wrapper over existing functionality in this module.\n * Getters, on success, return status 200 and a string that may be JSON or simple.\n * Setters, on success, return status 202, possibly with a human-readable status note.\n * The `requestShutdown` method is a special case that never returns.\n */\nclass BackgroundCommandWorker implements CommandWorkerInterface {\n  protected settingsValidator = new SettingsValidator();\n\n  /**\n   * Use the settings validator to validate settings after doing any\n   * initialization.\n   */\n  protected async validateSettings(existingSettings: settings.Settings, newSettings: RecursivePartial<settings.Settings>) {\n    let clearVersionsAfterTesting = false;\n\n    if (newSettings.kubernetes?.version && this.settingsValidator.k8sVersions.length === 0) {\n      // If we're starting up (by running `rdctl start...`) we probably haven't loaded all the k8s versions yet.\n      // We don't want to verify if the proposed version makes sense (if it doesn't, we'll assign the default version later).\n      // Here we just want to make sure that if we're changing the version to a different value from the current one,\n      // the field isn't locked.\n      let currentK8sVersions = (await k8smanager.kubeBackend.availableVersions).map(entry => entry.version.version);\n\n      if (currentK8sVersions.length === 0) {\n        clearVersionsAfterTesting = true;\n        currentK8sVersions = [newSettings.kubernetes.version];\n        if (existingSettings.kubernetes.version) {\n          currentK8sVersions.push(existingSettings.kubernetes.version);\n        }\n      }\n      this.settingsValidator.k8sVersions = currentK8sVersions;\n    }\n\n    const result = this.settingsValidator.validateSettings(existingSettings, newSettings, settingsImpl.getLockedSettings());\n\n    if (clearVersionsAfterTesting) {\n      this.settingsValidator.k8sVersions = [];\n    }\n\n    return result;\n  }\n\n  getSettings() {\n    return jsonStringifyWithWhiteSpace(cfg);\n  }\n\n  getLockedSettings() {\n    return jsonStringifyWithWhiteSpace(settingsImpl.getLockedSettings());\n  }\n\n  getDiagnosticCategories(): string[] | undefined {\n    return diagnostics.getCategoryNames();\n  }\n\n  getDiagnosticIdsByCategory(category: string): string[] | undefined {\n    return diagnostics.getIdsForCategory(category);\n  }\n\n  getDiagnosticChecks(category: string | null, checkID: string | null): Promise<DiagnosticsResultCollection> {\n    return diagnostics.getChecks(category, checkID);\n  }\n\n  runDiagnosticChecks(): Promise<DiagnosticsResultCollection> {\n    return diagnostics.runChecks();\n  }\n\n  factoryReset(keepSystemImages: boolean) {\n    doFactoryReset(keepSystemImages);\n  }\n\n  async k8sReset(context: CommandWorkerInterface.CommandContext, mode: 'fast' | 'wipe') {\n    return await doK8sReset(mode, context);\n  }\n\n  async forwardPort(namespace: string, service: string, k8sPort: string | number, hostPort: number) {\n    return await doForwardPort(namespace, service, k8sPort, hostPort);\n  }\n\n  async cancelForward(namespace: string, service: string, k8sPort: string | number) {\n    return await doCancelForward(namespace, service, k8sPort);\n  }\n\n  /**\n   * Execute the preference update for services that don't require a backend restart.\n   */\n  async handleSettingsUpdate(newConfig: settings.Settings): Promise<void> {\n    const allowedImagesConf = '/usr/local/openresty/nginx/conf/allowed-images.conf';\n    const rcService = k8smanager.backend === 'wsl' ? 'wsl-service' : 'rc-service';\n\n    // Update image allow list patterns, just in case the backend doesn't need restarting\n    // TODO: review why this block is needed at all\n    if (cfg.containerEngine.allowedImages.enabled) {\n      const allowListConf = BackendHelper.createAllowedImageListConf(cfg.containerEngine.allowedImages);\n\n      await k8smanager.executor.writeFile(allowedImagesConf, allowListConf, 0o644);\n      await k8smanager.executor.execCommand({ root: true }, rcService, '--ifstarted', 'rd-openresty', 'reload');\n    } else {\n      await k8smanager.executor.execCommand({ root: true }, rcService, '--ifstarted', 'rd-openresty', 'stop');\n      await k8smanager.executor.execCommand({ root: true }, 'rm', '-f', allowedImagesConf);\n    }\n\n    await k8smanager.handleSettingsUpdate(newConfig);\n  }\n\n  /**\n   * Check semantics of SET commands:\n   * - verify that setting names are recognized, and validate provided values\n   * - returns an array of two strings:\n   *   1. a description of the status of the request, if it was valid\n   *   2. a list of any errors in the request body.\n   * @param specifiedNewSettings: a subset of the Settings object, containing the desired values\n   * @returns [{string} description of final state if no error, {string} error message]\n   */\n  async updateSettings(context: CommandWorkerInterface.CommandContext, specifiedNewSettings: RecursivePartial<settings.Settings>): Promise<[string, string]> {\n    let errors: string[] = [];\n    let needToUpdate = false;\n    let newSettings: RecursivePartial<settings.Settings> = {};\n\n    try {\n      newSettings = settingsImpl.migrateSpecifiedSettingsToCurrentVersion(specifiedNewSettings, false);\n      [needToUpdate, errors] = await this.validateSettings(cfg, newSettings);\n    } catch (ex: any) {\n      errors.push(ex.message);\n    }\n    if (errors.length > 0) {\n      return ['', `errors in attempt to update settings:\\n${ errors.join('\\n') }`];\n    }\n    if (needToUpdate) {\n      writeSettings(newSettings);\n      // cfg is a global, and at this point newConfig has been merged into it :(\n      window.send('settings-update', cfg);\n      window.send('preferences/changed');\n    } else {\n      // Obviously if there are no settings to update, there's no need to restart.\n      return ['no changes necessary', ''];\n    }\n\n    // Update the values that doesn't need a restart of the backend.\n    await this.handleSettingsUpdate(cfg);\n\n    // Check if the newly applied preferences demands a restart of the backend.\n    const restartReasons = await k8smanager.requiresRestartReasons(cfg);\n\n    if (Object.keys(restartReasons).length === 0) {\n      return ['settings updated; no restart required', ''];\n    }\n\n    // Trigger a restart of the backend (possibly delayed).\n    if (!backendIsBusy()) {\n      pendingRestartContext = undefined;\n      setImmediate(doFullRestart, context);\n\n      return ['reconfiguring Rancher Desktop to apply changes (this may take a while)', ''];\n    } else {\n      // Call doFullRestart once the UI is finished starting or stopping\n      pendingRestartContext = context;\n\n      return ['UI is currently busy, but will eventually be reconfigured to apply requested changes', ''];\n    }\n  }\n\n  async proposeSettings(context: CommandWorkerInterface.CommandContext, newSettings: RecursivePartial<settings.Settings>): Promise<[string, string]> {\n    const [, errors] = await this.validateSettings(cfg, newSettings);\n\n    if (errors.length > 0) {\n      return ['', `Errors in proposed settings:\\n${ errors.join('\\n') }`];\n    }\n    const result = await k8smanager?.requiresRestartReasons(newSettings ?? {}) ?? {};\n\n    return [JSON.stringify(result), ''];\n  }\n\n  async requestShutdown() {\n    httpCommandServer?.closeServer();\n    httpCredentialHelperServer.closeServer();\n    await k8smanager.stop();\n    Electron.app.quit();\n  }\n\n  getTransientSettings() {\n    return jsonStringifyWithWhiteSpace(TransientSettings.value);\n  }\n\n  updateTransientSettings(\n    context: CommandWorkerInterface.CommandContext,\n    newTransientSettings: RecursivePartial<TransientSettings>,\n  ): Promise<[string, string]> {\n    const [needToUpdate, errors] = this.settingsValidator.validateTransientSettings(TransientSettings.value, newTransientSettings);\n\n    return Promise.resolve(((): [string, string] => {\n      if (errors.length > 0) {\n        return ['', `errors in attempt to update Transient Settings:\\n${ errors.join('\\n') }`];\n      }\n      if (needToUpdate) {\n        TransientSettings.update(newTransientSettings);\n\n        return ['Updated Transient Settings', ''];\n      }\n\n      return ['No changes necessary', ''];\n    })());\n  }\n\n  async listExtensions() {\n    const extensionManager = await getExtensionManager();\n\n    if (!extensionManager) {\n      return undefined;\n    }\n    const extensions = await extensionManager.getInstalledExtensions();\n    const entries = await Promise.all(extensions.map(async x => [x.id, {\n      version:  x.version,\n      metadata: await x.metadata,\n      labels:   await x.labels,\n    }] as const));\n\n    return Object.fromEntries(entries);\n  }\n\n  async installExtension(image: string, state: 'install' | 'uninstall'): Promise<{ status: number, data?: any }> {\n    const em = await getExtensionManager();\n\n    if (!em) {\n      return { status: 503, data: 'Extension manager is not ready yet.' };\n    }\n    const extension = await em.getExtension(image, { preferInstalled: state === 'uninstall' });\n\n    if (state === 'install') {\n      console.debug(`Installing extension ${ image }...`);\n      try {\n        const { enabled, list } = cfg.application.extensions.allowed;\n\n        if (await extension.install(enabled ? list : undefined)) {\n          return { status: 201 };\n        } else {\n          return { status: 204 };\n        }\n      } catch (ex: any) {\n        if (isExtensionError(ex)) {\n          switch (ex.code) {\n          case ExtensionErrorCode.INVALID_METADATA:\n            return { status: 422, data: `The image ${ image } has invalid extension metadata` };\n          case ExtensionErrorCode.FILE_NOT_FOUND:\n            return { status: 422, data: `The image ${ image } failed to install: ${ ex.message }` };\n          case ExtensionErrorCode.INSTALL_DENIED:\n            return { status: 403, data: `The image ${ image } is not an allowed extension` };\n          }\n        }\n        throw ex;\n      } finally {\n        window.send('extensions/changed');\n      }\n    } else {\n      console.debug(`Uninstalling extension ${ image }...`);\n      try {\n        if (await extension.uninstall()) {\n          window.send('ok:extensions/uninstall', image);\n\n          return { status: 201 };\n        } else {\n          return { status: 204 };\n        }\n      } catch (ex: any) {\n        if (isExtensionError(ex)) {\n          switch (ex.code) {\n          case ExtensionErrorCode.INVALID_METADATA:\n            return { status: 422, data: `The image ${ image } has invalid extension metadata` };\n          }\n        }\n        throw ex;\n      } finally {\n        window.send('extensions/changed');\n      }\n    }\n  }\n\n  async getBackendState(): Promise<BackendState> {\n    const backendIsLocked = await readBackendLockFile();\n\n    return {\n      vmState: k8smanager.state,\n      locked:  !!backendIsLocked,\n    };\n  }\n\n  async setBackendState(state: BackendState): Promise<void> {\n    await doesBackendLockExist();\n    switch (state.vmState) {\n    case State.STARTED:\n      cfg = settingsImpl.load(deploymentProfiles);\n      mainEvents.emit('settings-update', cfg);\n\n      setImmediate(() => {\n        startBackend();\n      });\n\n      return;\n    case State.STOPPED:\n      setImmediate(() => {\n        k8smanager.stop();\n      });\n\n      return;\n    default:\n      throw new Error(`invalid desired VM state \"${ state.vmState }\"`);\n    }\n  }\n\n  async listSnapshots(context: CommandWorkerInterface.CommandContext) {\n    return await Snapshots.list();\n  }\n\n  async createSnapshot(context: CommandWorkerInterface.CommandContext, snapshot: Snapshot) {\n    return await Snapshots.create(snapshot);\n  }\n\n  async restoreSnapshot(context: CommandWorkerInterface.CommandContext, name: string) {\n    return await Snapshots.restore(name);\n  }\n\n  async cancelSnapshot() {\n    return await Snapshots.cancel();\n  }\n\n  async deleteSnapshot(context: CommandWorkerInterface.CommandContext, name: string) {\n    return await Snapshots.delete(name);\n  }\n}\n\n/**\n * Checks if Rancher Desktop was run as root.\n */\nfunction isRoot(): boolean {\n  const validPlatforms = ['linux', 'darwin'];\n\n  if (!['linux', 'darwin'].includes(os.platform())) {\n    throw new Error(`isRoot() can only be called on ${ validPlatforms }`);\n  }\n\n  return os.userInfo().uid === 0;\n}\n\nasync function runRdctlSetup(newSettings: settings.Settings): Promise<void> {\n  // don't do anything with auto-start configuration if running in development\n  if (isDevEnv) {\n    return;\n  }\n\n  const rdctlPath = executable('rdctl');\n  const args = ['setup', `--auto-start=${ newSettings.application.autoStart }`];\n\n  await spawnFile(rdctlPath, args);\n}\n"
  },
  {
    "path": "bats/Makefile",
    "content": ".PHONY: all\nall:\n\t./bats-core/bin/bats --show-output-of-passing-tests ./tests/*/\n\n.PHONY: containers\ncontainers:\n\t./bats-core/bin/bats --show-output-of-passing-tests ./tests/containers/\n\n# https://www.shellcheck.net/wiki/SC1091 -- Not following: xxx was not specified as input (see shellcheck -x)\n# https://www.shellcheck.net/wiki/SC2034 -- xxx appears unused. Verify use (or export if used externally)\n# https://www.shellcheck.net/wiki/SC2154 -- xxx is referenced but not assigned\n# https://www.shellcheck.net/wiki/SC2218 -- This function is only defined later. Move the definition up.\n\nSC_EXCLUDES ?= SC1091,SC2034,SC2154,SC2218\n\n.PHONY: lint\nlint:\n\tfind tests -name '*.bash' | xargs ./scripts/bats-lint.pl\n\tfind tests -name '*.bats' | xargs ./scripts/bats-lint.pl\n\tfind tests -name '*.bash' | xargs shellcheck -s bats -e $(SC_EXCLUDES)\n\tfind tests -name '*.bats' | xargs shellcheck -s bats -e $(SC_EXCLUDES)\n\tfind scripts -name '*.sh' | xargs shellcheck -s bash -e $(SC_EXCLUDES)\n\tfind tests -name '*.bash' | xargs shfmt --diff\n\tfind tests -name '*.bats' | xargs shfmt --diff\n\tfind scripts -name '*.sh' | xargs shfmt --diff\n\nDEPS = bin/darwin/jq bin/linux/jq\n\n# Package bats tests with bats itself and dependencies into a single tarball for distribution\nbats.tar.gz: $(DEPS)\n\tgit submodule update --init --recursive\n\ttar cvfz \"$@\" --exclude-vcs --exclude \"*/test/*\" --exclude \"*/docs/*\" -- *\n\nJQ_VERSION ?= 1.7.1\nJQ_URL=https://github.com/stedolan/jq/releases/download/jq-$(JQ_VERSION)\n\nbin/darwin/jq:\n\tmkdir -p bin/darwin\n\twget --no-verbose $(JQ_URL)/jq-osx-amd64 -O $@\n\tchmod +x $@\n\nbin/linux/jq:\n\tmkdir -p bin/linux\n\twget --no-verbose $(JQ_URL)/jq-linux64 -O $@\n\tchmod +x $@\n\n.PHONY: clean\nclean:\n\trm -f $(DEPS)\n\trm -f bats.tar.gz\n"
  },
  {
    "path": "bats/README.md",
    "content": "## Overview\n\nBATS is a testing framework for Bash shell scripts that provides supporting libraries and helpers for customizable test automation.\n\n## Setup\n\nIt's important to have a Rancher Desktop CI or release build installed and running with no errors before executing the BATS tests.\n\n### On Windows:\n\nClone the Git repository of Rancher Desktop, whether directly inside a WSl distro or on the host Win32.\nIf the repository will be cloned on Win32, prior to cloning it, it's important to set up the Git configuration by running the following commands:\n\n  ```powershell\n  git config --global core.eol lf\n  git config --global core.autocrlf false\n  ```\nNote that changing `crlf` settings is not needed when you clone it inside a WSL distro.\nRegardless of the repository location, the BATS tests can be executed ONLY from inside a WSL distribution. So, if the repository is cloned on Win32, the repository can be located within a WSL distro from /mnt/c, as it represents the `C:` drive on Windows.\n\n### On Linux:\n\nImageMagick is required to take screenshots on failure.\n\n### All platforms:\n\nFrom the root directory of the Git repository, run the following commands to install BATS and its helper libraries into the BATS test directory:\n\n  ```sh\n  git submodule update --init\n  ```\n\n## Running BATS\n\nTo run the BATS test, specify the path to BATS executable from bats-core and run the following commands:\n\nTo run a specific test set from a bats file:\n\n```sh\ncd bats\n./bats-core/bin/bats tests/registry/creds.bats\n```\n\nTo run all BATS tests:\n\n```sh\ncd bats\n./bats-core/bin/bats tests/*/\n```\n\nTo run the BATS test, specifying some of Rancher Desktop's configuration, run the following commands:\n\n```sh\ncd bats\nRD_CONTAINER_RUNTIME=moby RD_USE_IMAGE_ALLOW_LIST=false ./bats-core/bin/bats tests/registry/creds.bats\n```\n\nThere is an experimental subset of BATS tests that pass with an under-construction openSUSE based\ndistribution; that can be selected via the `opensuse` tag:\n\n```sh\ncd bats\n./bats-core/bin/bats --filter-tags opensuse tests/*/\n```\n\n### On Windows:\n\nBATS must be executed from within a WSL distribution. (You have to cd into `/mnt/c/REPOSITORY_LOCATION` from your unix shell.)\n\nTo test the Windows-based tools, set `RD_USE_WINDOWS_EXE` to `true` before running.\n\n### RD_LOCATION\n\nBy default bats will use Rancher Desktop installed in a \"system\" location. If\nthat doesn't exists, it will try a \"user\" location, followed by the local \"dist\"\ndirectory inside the local git directory. The final option if none of the above\napply is to use \"dev\", which uses `yarn dev`. On Linux there is no \"user\"\nlocation.\n\nYou can explicitly request a specific install location by setting `RD_LOCATION` to `system`, `user`, `dist`, or `dev`:\n\n```\ncd bats\nRD_LOCATION=dist ./bats-core/bin/bats ...\n```\n\n### RD_NO_MODAL_DIALOGS\n\nBy default, bats tests are run with the `--no-modal-dialogs` option so fatal errors are written to `background.log`,\nrather than appearing in a blocking modal dialog box. If you *want* those dialog boxes, you can specify\n\n```\ncd bats\nRD_NO_MODAL_DIALOGS=false ./bats-core/bin/bats ...\n```\n\nThe default value for this environment variable is `true`.\n\n## Writing BATS Tests\n\n1. Add BATS test by creating files with `.bats` extension under `./bats/tests/FOLDER_NAME`\n2. A Bats test file is a Bash script with special syntax for defining test cases. BATS syntax and libraries for defining test hooks, writing assertions and treating output can be accessed via BATS [documentation](https://bats-core.readthedocs.io/en/stable/):\n    - [bats-core](https://github.com/rancher-sandbox/bats-core)\n    - [bats-assert](https://github.com/rancher-sandbox/bats-assert)\n    - [bats-file](https://github.com/rancher-sandbox/bats-file)\n    - [bats-support](https://github.com/rancher-sandbox/bats-support)\n\n## BATS linting\n\nAfter finishing to develop a BATS test suite, you can locally verify the syntax and formatting feedback by linting prior to submitting a PR, following the instructions:\n\n  1. Make sure to have installed `shellcheck` and `shfmt`.\n\n     On macOS:\n     - Assuming you have Homebrew:\n       ```sh\n       brew install shfmt shellcheck\n       ```\n     - If you have Go installed, you can also install `shfmt` by running:\n       ```sh\n       go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0\n       ```\n\n     On Linux:\n     - The simplest way to install ShellCheck locally is through your package managers\n       such as `apt/apt-get/yum`. Run commands as per your distro.\n       ```\n       sudo apt install shellcheck\n       ```\n     - `shfmt` is available as a snap application. If your distribution has snap\n       installed, you can install `shfmt` using the command:\n       ```sh\n       sudo snap install shfmt\n       ```\n       The other way to install `shfmt` is by using the following one-liner command:\n       ```sh\n       curl -sS https://webinstall.dev/shfmt | bash\n       ```\n       If you have Go installed, you can also install `shfmt` by running:\n       ```sh\n       go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0\n       ```\n     On Windows:\n     - The simplest way to install `shellcheck` locally is:\n\n       Via chocolatey:\n       ```powershell\n       choco install shellcheck\n       ```\n       Via scoop:\n       ```powershell\n       scoop install shellcheck\n       ```\n     - If you have Go installed, you can install `shfmt` by running:\n       ```powershell\n       go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0\n       ```\n\n  2. Get the syntax and formatting feedback for BATS linting by running from the\n     root directory of the Git repository:\n     ```sh\n     make -C bats lint\n     ```\n  3. Please, make sure to fix the highlighted linting errors prior to submitting\n     a PR. You can automatically apply formatting changes suggested by `shfmt`\n     by running the following command:\n     ```sh\n     shfmt -w ./bats/tests/containers/factory-reset.bats\n     ```\n\n## Running BATS in CI\n\nWe also run BATS in CI via [GitHub Actions]; at the time of writing, we do not\nyet run them automatically due to failing tests.  There are many optional fields\nthat may be set when triggering a run manually:\n\n[GitHub Actions]: https://github.com/rancher-sandbox/rancher-desktop/actions/workflows/bats.yaml\n\n<!-- This table is done in HTML to allow line wrapping -->\n<table>\n  <thead> <tr> <th>Input <th>Description\n  <tbody>\n  <tr><td><code>owner</code>, <code>repo</code>\n    <td>Forms the GitHub repository to test; defaults to the current repository.\n  <tr><td><code>branch</code>\n    <td>The branch to test; defaults to the current branch.\n  <tr><td><code>tests</code>\n    <td>The list of tests, as a whitespace-separated glob expression relative to the\n    <a href=\"tests\"><code>tests</code></a> directory.  The <code>.bats</code>\n    suffix may be omitted on test files.\n  <tr><td><code>platforms</code>\n    <td>A space-separated list of platforms to test on; defaults to everything,\n    and items may be removed to reduce coverage.\n  <tr><td><code>engines</code>\n    <td>A space-separated list of container engines to test on; defaults to\n    everything, and items may be removed to reduce coverage.\n  <tr><td><code>package-id</code>\n    <td>A specific GitHub run ID for the\n    <a href=\"https://github.com/rancher-sandbox/rancher-desktop/actions/workflows/package.yaml\">package action</a>\n    to test.  This allows to test code from runs where it failed to build on\n    platforms that don't need to be tested, or in-process runs as long as the\n    relevant platforms have already completed.\n  </tbody>\n</table>\n\n### Debugging BATS in CI\n\nSometimes we may need to drill down why a test is failing in CI (for example,\nwhen the same test doesn't fail locally).  Some things might be helpful:\n\n- Logs for failing runs can be downloaded by clicking on the :file_folder: icon\n  in the summary table at the bottom of the run.\n- If changes to the application or BATS tests are required, a new [package\n  action] run will need to be manually triggered.  In that case, setting `sign`\n  to `false` in that run will speed it up by a few minutes, by skipping the\n  check for properly signed installers — that can be dealt with when the actual\n  PR is made.\n- When focusing on a particular failing platform, it may be possible to shave\n  off a few minutes by setting the `package-id` field (see above) when starting\n  the BATS run; this lets you start the run once the platform you're interested\n  in has completed packaging, without waiting for other platforms.  This should\n  be set to the number after `…/actions/runs/` in the URL.\n- When testing, it is a good idea to [fork the repository] and run the tests\n  there; this lets you have your own set of GitHub runner quota (which means not\n  waiting for PRs other people create).  It is not necessary to set `owner` and\n  `repo` fields when running the BATS action (because it defaults to the\n  repository the action is running on).  You will, however, need to run the\n  [package action] at least once in your fork.\n- It is much faster to specify `tests`, `platforms`, and `engines` to limit\n  runs to only the tests you care about; the full run takes somewhere over two\n  hours total, even spread out over multiple parallel jobs.\n\n[package action]: https://github.com/rancher-sandbox/rancher-desktop/actions/workflows/package.yaml\n[fork the repository]: https://github.com/rancher-sandbox/rancher-desktop/fork\n"
  },
  {
    "path": "bats/scripts/bats-lint.pl",
    "content": "#!/usr/bin/env perl\n\n# This script checks a BATS script to make sure every `run` or `try` call is\n# followed by a call to `assert` or `refute`, or a reference to `$output` or\n# `$status`. `assert` may be a variable reference like `${assert}`.\n#\n# The `run` or `try` call may be followed by blank lines or `if ...` statements\n# before the assert/refute becomes required.\n\nuse strict;\nuse warnings;\n\nmy $problems = 0;\nmy $run;\nmy $continue;\n\nwhile (<>) {\n    if ($ARGV =~ /\\.bats$/) {\n      # bats files should not override the global setup and teardown functions.\n      # They should define local_* variants instead, which will be called from\n      # the global versions.\n      if (/^((setup|teardown)\\w*)\\(/) {\n          print \"$ARGV:$.: Don't define $1(); define local_$1() instead\\n\";\n          $problems++;\n      }\n\n      if (/\\b run \\b .* \\b load_var \\b/x) {\n          print \"$ARGV:$.: Running load_var in a subshell (via run) does not work\\n\";\n          $problems++;\n      }\n    }\n\n    # The semver comparison functions take arguments that are valid semver;\n    # catch uses of it with invalid versions, like '1.2' instead of '1.2.3'.\n    if (/\n      (semver_(?:n?eq|[lg]te?)) # Semver comparison function\n      [^#\\n]*                   # Eat any number of characters before new line or comment\n      (?<!\\d)                   # Was not preceded by digit (or we'd check that instead)\n      (?<!\\d\\.)                 # Was not preceded by digit-dot (or we'd check that instead)\n      (?!\\d+\\.\\d+\\.\\d+)         # Is not a valid version string\n      (\\b\\d[\\d.]*)              # But starts a version string\n    /x) {\n      print qq'$ARGV:$.: $1 must be called with a valid semver, got \"$2\"\\n';\n      $problems++;\n    }\n\n    # Matches:\n    # - assert_success\n    # - $assert_success\n    # - $ {assert}_success\n    # - if [ $status -eq 0 ]\n    if (/(\\$\\{?)? (assert | refute | \\b output \\b | \\b status \\b)/x) {\n        undef $run;\n        undef $continue;\n    }\n    # Doesn't match on:\n    # - \"empty lines (just whitespace or comment)\"\n    # - if ...\n    if ($run) {\n        if ($continue) {\n            if (!/\\\\$/) {\n                undef $continue;\n            }\n        } elsif (!/^\\s*(#.*|if.*)?$/) {\n            print \"$ARGV:$.: Expected assert or refute after\\n$run\\n\";\n            undef $run;\n            $problems++;\n        }\n    }\n    # Matches any line starting with \"run \"\n    if (/^\\s*(run)\\s/) {\n        $run = $_;\n        $continue = /\\\\$/;\n    }\n    # Reset $. line counter for next input file\n    close ARGV if eof;\n}\n\ndie \"Found $problems problems\\n\" if $problems;\n"
  },
  {
    "path": "bats/scripts/ghcr-mirror.sh",
    "content": "#!/bin/bash\n\n# Mirror Docker Hub images to ghcr.io to avoid pull limits during testing.\n\n# The script uses skopeo instead of docker pull/push because it needs to\n# copy all images of the repo, and not just the one for the current platform.\n#\n# Log into ghcr.io with a personal access token with write:packages scope:\n#   echo $PAT | skopeo login ghcr.io -u $USER --password-stdin\n#   echo $PASS | skopeo login docker.io -u $USER --password-stdin\n# Remove credentials:\n#   skopeo logout --all\n\n# TODO TODO TODO\n# The package visibility needs to be changed to \"public\".\n# I've not found any tool/API to do this from the commandline,\n# so I did this manually via the web UI.\n# At the very least we should check that the images are accessible\n# when logged out of ghcr.io.\n# TODO TODO TODO\n\n# TODO TODO TODO\n# Figure out a way to copy only the amd64 and arm64 images, but not the rest.\n# skopeo doesn't seem to support this yet without additional scripting to parse\n# the manifests. And then we would need to test if we can copy a \"sparse\" manifest\n# to ghcr.io when not all referenced images actually exist.\n# TODO TODO TODO\n\nset -o errexit -o nounset -o pipefail\nset +o xtrace\n\nif ! command -v skopeo >/dev/null; then\n    echo \"This script requires the 'skopeo' utility to be installed\"\n    exit 1\nfi\n\nsource \"$(dirname \"${BASH_SOURCE[0]}\")/../tests/helpers/images.bash\"\n\n# IMAGES is setup by ../tests/helpers/images.bash\n# shellcheck disable=SC2153\nfor IMAGE in \"${IMAGES[@]}\"; do\n    echo \"===== Copying $IMAGE =====\"\n    skopeo copy --all \"docker://$IMAGE\" \"docker://$GHCR_REPO/$IMAGE\"\ndone\n"
  },
  {
    "path": "bats/tests/compose/compose.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\nlocal_setup() {\n    TESTDATA_DIR=\"${PATH_BATS_ROOT}/tests/compose/testdata/\"\n    TESTDATA_DIR_HOST=$(host_path \"$TESTDATA_DIR\")\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'compose up' {\n    ctrctl compose --project-directory \"$TESTDATA_DIR_HOST\" build \\\n        --build-arg IMAGE_NGINX=\"$IMAGE_NGINX\" \\\n        --build-arg IMAGE_PYTHON=\"$IMAGE_PYTHON_3_9_SLIM\"\n    ctrctl compose --project-directory \"$TESTDATA_DIR_HOST\" up -d --no-build\n}\n\nverify_running_container() {\n    try --max 9 --delay 10 curl --silent --show-error \"$1\"\n    assert_success\n    assert_output --partial \"$2\"\n}\n\n@test 'verify app bound to localhost' {\n    verify_running_container \"http://localhost:8080\" \"Welcome to nginx!\"\n    skip_unless_host_ip\n    run curl --verbose --head \"http://${HOST_IP}:8080\"\n    assert_output --partial \"curl: (7) Failed to connect\"\n}\n\n@test 'verify app bound to wildcard IP' {\n    local expected_output=\"Hello World!\"\n    verify_running_container \"http://localhost:8000\" \"$expected_output\"\n    skip_unless_host_ip\n    verify_running_container \"http://${HOST_IP}:8000\" \"$expected_output\"\n}\n\n@test 'verify connectivity via host.docker.internal' {\n    local expected_output=\"Hello World!\"\n    verify_running_container \"http://localhost:8080/app\" \"$expected_output\"\n}\n\n@test 'compose down' {\n    run ctrctl compose --project-directory \"$TESTDATA_DIR_HOST\" down\n    assert_success\n}\n"
  },
  {
    "path": "bats/tests/compose/testdata/Dockerfile.nginx",
    "content": "ARG IMAGE_NGINX\nFROM ${IMAGE_NGINX}\n"
  },
  {
    "path": "bats/tests/compose/testdata/app/Dockerfile",
    "content": "ARG IMAGE_PYTHON=python:3.9-slim\nFROM ${IMAGE_PYTHON}\n\nWORKDIR /app\n\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\n\nCMD [\"python\", \"app.py\"]\n"
  },
  {
    "path": "bats/tests/compose/testdata/app/app.py",
    "content": "from flask import Flask\napp = Flask(__name__)\n\n@app.route('/')\ndef hello():\n\treturn \"Hello World!\"\n\nif __name__ == '__main__':\n\tapp.run(host='0.0.0.0', port=8000)\n"
  },
  {
    "path": "bats/tests/compose/testdata/app/requirements.txt",
    "content": "flask\n"
  },
  {
    "path": "bats/tests/compose/testdata/compose.yaml",
    "content": "services:\n  nginx:\n    container_name: nginx\n    build:\n      args:\n      - IMAGE_NGINX\n      dockerfile: Dockerfile.nginx\n    volumes:\n    - ./nginx.conf:/etc/nginx/nginx.conf\n    ports:\n    - '127.0.0.1:8080:80'\n  web:\n    build:\n      args:\n      - IMAGE_PYTHON\n      context: app\n    # flask requires SIGINT to stop gracefully\n    # (default stop signal from Compose is SIGTERM)\n    stop_signal: SIGINT\n    ports:\n    - '8000:8000'\n"
  },
  {
    "path": "bats/tests/compose/testdata/nginx.conf",
    "content": "worker_processes  1;\n\nerror_log  stderr info;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include       mime.types;\n    sendfile      on;\n    proxy_read_timeout 5s;\n\n    server {\n        listen       80;\n        server_name  localhost;\n\n        # Serve the default nginx welcome page\n        location / {\n            root /usr/share/nginx/html;\n            index index.html;\n        }\n\n        # Proxy requests to /app to the backend service\n        location /app {\n            proxy_pass http://host.docker.internal:8000/;\n        }\n    }\n}\n"
  },
  {
    "path": "bats/tests/containers/allowed-images.bats",
    "content": "load '../helpers/load'\nRD_USE_IMAGE_ALLOW_LIST=true\n\n@test 'start' {\n    factory_reset\n    start_kubernetes\n    wait_for_container_engine\n    wait_for_kubelet\n}\n\n@test 'update the list of patterns first time' {\n    update_allowed_patterns true \"$IMAGE_NGINX\" \"$IMAGE_BUSYBOX\" \"$IMAGE_PYTHON\"\n}\n\n@test 'verify pull nginx succeeds' {\n    ctrctl pull --quiet \"$IMAGE_NGINX\"\n}\n\n@test 'verify pull busybox succeeds' {\n    ctrctl pull --quiet \"$IMAGE_BUSYBOX\"\n}\n\n@test 'verify pull python succeeds' {\n    ctrctl pull --quiet \"$IMAGE_PYTHON\"\n}\n\nassert_pull_fails() {\n    run ctrctl pull \"$1\"\n    assert_failure\n    assert_output --regexp \"(UNAUTHORIZED|Forbidden)\"\n}\n\n@test 'verify pull ruby fails' {\n    try --max 9 --delay 10 assert_pull_fails \"$IMAGE_RUBY\"\n}\n\n@test 'drop python from the allowed-image list, add ruby' {\n    update_allowed_patterns true \"$IMAGE_NGINX\" \"$IMAGE_BUSYBOX\" \"$IMAGE_RUBY\"\n}\n\n@test 'clear images' {\n    for image in IMAGE_NGINX IMAGE_BUSYBOX IMAGE_PYTHON; do\n        ctrctl rmi \"${!image}\"\n    done\n}\n\n@test 'verify pull python fails' {\n    try --max 9 --delay 10 assert_pull_fails \"$IMAGE_PYTHON\"\n}\n\n@test 'verify pull ruby succeeds' {\n    # when using VZ and when traefik is enabled, then pulling the image does not always succeed on the first attempt\n    try --max 9 --delay 10 ctrctl pull --quiet \"$IMAGE_RUBY\"\n}\n\n@test 'clear all patterns' {\n    update_allowed_patterns true\n}\n\n@test 'can run kubectl' {\n    kubectl run nginx --image=\"${IMAGE_NGINX}\" --port=8080\n}\n\nverify_no_nginx() {\n    run kubectl get pods\n    assert_success\n    assert_output --partial \"ImagePullBackOff\"\n}\n\n@test 'but fails to stand up a pod for forbidden image' {\n    try --max 18 --delay 10 verify_no_nginx\n}\n\n@test 'set patterns with the allowed list disabled' {\n    update_allowed_patterns false \"$IMAGE_NGINX\" \"$IMAGE_BUSYBOX\" \"$IMAGE_RUBY\"\n}\n\n@test 'verify pull python succeeds because allowedImages filter is disabled' {\n    # when using VZ and when traefik is enabled, then pulling the image does not always succeed on the first attempt\n    try --max 9 --delay 10 ctrctl pull --quiet \"$IMAGE_PYTHON\"\n}\n"
  },
  {
    "path": "bats/tests/containers/auto-start.bats",
    "content": "load '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'Start up Rancher Desktop' {\n    start_application\n}\n\n@test 'Verify that initial Behavior is all set to false' {\n    run get_setting '.application.autoStart'\n    assert_success\n    assert_output false\n    run get_setting '.application.startInBackground'\n    assert_success\n    assert_output false\n    run get_setting '.application.window.quitOnClose'\n    assert_success\n    assert_output false\n    run get_setting '.application.hideNotificationIcon'\n    assert_success\n    assert_output false\n}\n\n@test 'Enable auto start' {\n    rdctl set --application.auto-start=true\n    run get_setting '.application.autoStart'\n    assert_success\n    assert_output true\n}\n\n@test 'Verify that the auto-start config is created' {\n    if using_dev_mode; then\n        skip \"Autostart prefs don't work in dev mode\"\n    fi\n    if is_linux; then\n        assert_file_exists \"${XDG_CONFIG_HOME:-$HOME/.config}/autostart/rancher-desktop.desktop\"\n    fi\n\n    if is_macos; then\n        assert_file_exists \"$HOME/Library/LaunchAgents/io.rancherdesktop.autostart.plist\"\n    fi\n\n    if is_windows; then\n        run powershell.exe -c \"reg query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v RancherDesktop\"\n        assert_success\n        assert_line --index 2 --partial \"\\Rancher Desktop.exe\"\n    fi\n}\n\n@test 'Disable auto start' {\n    rdctl set --application.auto-start=false\n    run get_setting '.application.autoStart'\n    assert_success\n    assert_output false\n}\n\n@test 'Verify that the auto-start config is removed' {\n    if using_dev_mode; then\n        skip \"Autostart prefs don't work in dev mode\"\n    fi\n    if is_linux; then\n        assert_file_not_exists \"${XDG_CONFIG_HOME:-$HOME/.config}/autostart/rancher-desktop.desktop\"\n    fi\n\n    if is_macos; then\n        assert_file_not_exists \"$HOME/Library/LaunchAgents/io.rancherdesktop.autostart.plist\"\n    fi\n\n    if is_windows; then\n        run powershell.exe -c \"reg query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v RancherDesktop\"\n        assert_failure\n        assert_output --partial \"The system was unable to find the specified registry\"\n    fi\n}\n\n@test 'Enable quit-on-close' {\n    rdctl set --application.window.quit-on-close=true\n    run get_setting '.application.window.quitOnClose'\n    assert_success\n    assert_output true\n}\n\n@test 'Disable quit-on-close' {\n    rdctl set --application.window.quit-on-close=false\n    run get_setting '.application.window.quitOnClose'\n    assert_success\n    assert_output false\n}\n\n@test 'Enable start-in-background' {\n    rdctl set --application.start-in-background=true\n    run get_setting '.application.startInBackground'\n    assert_success\n    assert_output true\n}\n\n@test 'Disable start-in-background' {\n    rdctl set --application.start-in-background=false\n    run get_setting '.application.startInBackground'\n    assert_success\n    assert_output false\n}\n\n@test 'Enable hide-notification-icon' {\n    rdctl set --application.hide-notification-icon=true\n    run get_setting '.application.hideNotificationIcon'\n    assert_success\n    assert_output true\n}\n\n@test 'Disable hide-notification-icon' {\n    rdctl set --application.hide-notification-icon=false\n    run get_setting '.application.hideNotificationIcon'\n    assert_success\n    assert_output false\n}\n"
  },
  {
    "path": "bats/tests/containers/catch-duplicate-api-patterns.bats",
    "content": "load '../helpers/load'\nRD_USE_IMAGE_ALLOW_LIST=true\n\n@test 'catch attempts to add duplicate patterns via the API with enabled on' {\n    factory_reset\n    start_kubernetes\n    wait_for_kubelet\n    wait_for_container_engine\n\n    run update_allowed_patterns true \"$IMAGE_NGINX\" \"$IMAGE_BUSYBOX\" \"$IMAGE_RUBY\" \"$IMAGE_BUSYBOX\"\n    assert_failure\n    assert_output --partial $\"field \\\"containerEngine.allowedImages.patterns\\\" has duplicate entries: \\\"$IMAGE_BUSYBOX\\\"\"\n}\n\n@test 'catch attempts to add duplicate patterns via the API with enabled off' {\n    run update_allowed_patterns false \"$IMAGE_NGINX\" \"$IMAGE_BUSYBOX\" \"$IMAGE_RUBY\" \"$IMAGE_BUSYBOX\"\n    assert_failure\n    assert_output --partial $\"field \\\"containerEngine.allowedImages.patterns\\\" has duplicate entries: \\\"$IMAGE_BUSYBOX\\\"\"\n}\n"
  },
  {
    "path": "bats/tests/containers/docker-buildx-python3-uname.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    if ! using_docker; then\n        skip \"This test only applied to the moby container engine\"\n    fi\n    TEMP=/tmp\n    if is_windows; then\n        # We need to use a directory that exists on the Win32 filesystem\n        # so the docker clients can correctly map the bind mounts.\n        # We can use host_path() on these paths because they will exist\n        # both here and in the rancher-desktop distro.\n        TEMP=\"$(wslpath_from_win32_env TEMP)\"\n    fi\n    BUILDX_BUILDER=rd_bats_builder\n    WORK_DIR=\"$TEMP/$BUILDX_BUILDER\"\n    BUILDX_INSTANCE=amd64builder\n}\n\n@test 'start' {\n    factory_reset\n    start_container_engine\n    wait_for_container_engine\n    # Do any cleanup from previous runs\n    run docker buildx rm \"$BUILDX_INSTANCE\"\n    assert_nothing\n    rm -fr \"$WORK_DIR\"\n}\n\n@test 'create the source directory to work in' {\n    mkdir -p \"$WORK_DIR\"\n    cat >\"${WORK_DIR}/Dockerfile\" <<'EOF'\nFROM  registry.access.redhat.com/ubi8/python-39:1-57\nRUN  python3 -m pip install  tornado\nCMD echo \"Running on $(uname -m)\"\nEOF\n}\n\n@test 'build the container' {\n    docker buildx create --name \"$BUILDX_INSTANCE\"\n    docker buildx use \"$BUILDX_INSTANCE\"\n    cd \"$WORK_DIR\"\n    docker buildx build -t testbuild:00 --platform linux/amd64 --load .\n    run docker run --platform linux/amd64 testbuild:00\n    assert_success\n    assert_output \"Running on x86_64\"\n}\n"
  },
  {
    "path": "bats/tests/containers/factory-reset-containerd-shims.bats",
    "content": "load '../helpers/load'\n\nBOGUS_SHIM=\"${PATH_CONTAINERD_SHIMS}/containerd-shim-bogus-v1\"\n\nlocal_setup_file() {\n    RD_USE_RAMDISK=false # interferes with deleting $PATH_APP_HOME\n\n    delete_all_snapshots\n    rm -rf \"$PATH_CONTAINERD_SHIMS\"\n}\n\nlocal_teardown_file() {\n    rm -rf \"$PATH_CONTAINERD_SHIMS\"\n}\n\n@test 'factory reset' {\n    # On Windows the cache directory is under PATH_APP_HOME.\n    factory_reset --cache\n    assert_not_exists \"$PATH_APP_HOME\"\n}\n\n@test 'factory reset will not remove any shims' {\n    assert_not_exists \"$PATH_CONTAINERD_SHIMS\"\n    create_file \"$BOGUS_SHIM\" <<<''\n    factory_reset\n    assert_exists \"$BOGUS_SHIM\"\n    assert_exists \"$PATH_APP_HOME\"\n}\n\n@test 'factory reset will remove empty shim directory' {\n    rm \"$BOGUS_SHIM\"\n    factory_reset\n    assert_not_exists \"$PATH_CONTAINERD_SHIMS\"\n    assert_not_exists \"$PATH_APP_HOME\"\n}\n"
  },
  {
    "path": "bats/tests/containers/factory-reset-snapshots.bats",
    "content": "load '../helpers/load'\n\nlocal_setup_file() {\n    RD_USE_RAMDISK=false # interferes with deleting $PATH_APP_HOME\n}\n\n@test 'factory reset' {\n    delete_all_snapshots\n    rm -rf \"$PATH_CONTAINERD_SHIMS\"\n    # On Windows the cache directory is under PATH_APP_HOME.\n    factory_reset --cache\n}\n\n@test 'Start up Rancher Desktop with a snapshots subdirectory' {\n    start_container_engine\n    wait_for_container_engine\n    wait_for_backend\n}\n\n@test \"Verify the snapshot dir isn't deleted on factory-reset\" {\n    rdctl shutdown\n    rdctl snapshot create shortlived-snapshot\n    factory_reset --cache\n    assert_not_exists \"$PATH_APP_HOME/rd-engine.json\"\n    assert_exists \"$PATH_SNAPSHOTS\"\n    run ls -A \"$PATH_SNAPSHOTS\"\n    assert_output\n}\n\n@test 'Verify factory-reset deletes an empty snapshots directory' {\n    rdctl snapshot delete shortlived-snapshot\n    factory_reset --cache\n    assert_not_exists \"$PATH_APP_HOME\"\n}\n"
  },
  {
    "path": "bats/tests/containers/factory-reset.bats",
    "content": "load '../helpers/load'\n\nlocal_setup_file() {\n    RD_USE_RAMDISK=false # interferes with deleting $PATH_APP_HOME\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'Start up Rancher Desktop' {\n    start_application\n}\n\n@test 'Verify that the expected directories were created' {\n    before check_directories\n}\n\n@test 'Verify that docker symlinks were created' {\n    before check_docker_symlinks\n}\n\n@test 'Verify that path management was set' {\n    before check_path\n}\n\n@test 'Verify that rancher desktop context was created' {\n    before check_rd_context\n}\n\n@test 'Verify that lima VM was created' {\n    before check_lima\n}\n\n@test 'Verify that WSL distributions were created' {\n    before check_WSL\n}\n\n@test 'Shutdown Rancher Desktop' {\n    rdctl shutdown\n}\n@test 'factory-reset when Rancher Desktop is not running' {\n    touch_updater_longhorn\n    rdctl_factory_reset --verbose\n}\n\n@test 'Verify that the expected directories were deleted' {\n    check_directories\n}\n\n@test 'Verify that docker symlinks were deleted' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted' {\n    check_updater_longhorn_gone\n}\n\n@test 'Start Rancher Desktop 2' {\n    start_application\n}\n\n@test 'factory reset - keep cached k8s images' {\n    rdctl_factory_reset --remove-kubernetes-cache=false --verbose\n}\n\n@test 'Verify that the expected directories were deleted 2' {\n    check_directories\n}\n\n@test 'Verify that docker symlinks were deleted 2' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset 2' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted 2' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted 2' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted 2' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted 2' {\n    check_updater_longhorn_gone\n}\n\n@test 'Start Rancher Desktop 3' {\n    start_application\n}\n\n@test 'factory reset - delete cached k8s images' {\n    rdctl_factory_reset --remove-kubernetes-cache=true --verbose\n}\n\n@test 'Verify that the expected directories were deleted 3' {\n    check_directories\n}\n\n@test 'Verify that docker symlinks were deleted 3' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset 3' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted 3' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted 3' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted 3' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted when cache was retained' {\n    check_updater_longhorn_gone\n}\n\nrdctl_factory_reset() {\n    capture_logs\n    rdctl factory-reset \"$@\"\n\n    if [[ $1 == \"--remove-kubernetes-cache=true\" ]]; then\n        assert_not_exist \"$PATH_CACHE\"\n    else\n        assert_exists \"$PATH_CACHE\"\n    fi\n}\n\ncheck_directories() {\n    # Check if all expected directories are created after starting application/ are deleted after a factory reset\n    delete_dir=(\"$PATH_LOGS\" \"$PATH_APP_HOME/credential-server.json\" \"$PATH_APP_HOME/rd-engine.json\")\n    if is_unix; then\n        # On Windows \"$PATH_CONFIG\" == \"$PATH_APP_HOME\"\n        delete_dir+=(\"$HOME/.rd\" \"$LIMA_HOME\" \"$PATH_CONFIG\")\n        # We can't make any general assertion on AppHome/snapshots - we don't know if it was created or not\n        # So just assert on the other members of AppHome\n        # TODO on macOS (not implemented by `rdctl factory-reset`)\n        # ~/Library/Saved Application State/io.rancherdesktop.app.savedState\n        # this one only exists after an update has been downloaded\n        # ~/Library/Application Support/Caches/rancher-desktop-updater\n    fi\n\n    if is_windows; then\n        # On Windows $PATH_CONFIG is the same as $PATH_APP_HOME\n        delete_dir+=(\"$PATH_CONFIG_FILE\" \"$PATH_DISTRO\" \"$PATH_DISTRO_DATA\")\n        # TODO: What about  $PATH_APP_HOME/vtunnel-config.yaml ?\n    fi\n\n    for dir in \"${delete_dir[@]}\"; do\n        echo \"# $assert that $dir does not exist\" 1>&3\n        \"${assert}_not_exists\" \"$dir\"\n    done\n}\n\ncheck_docker_symlinks() {\n    skip_on_windows\n    # Check if docker-X symlinks were deleted\n    for dfile in docker-buildx docker-compose; do\n        run readlink \"$HOME/.docker/cli-plugins/$dfile\"\n        \"${refute}_output\" \"$HOME/.rd/bin/$dfile\"\n    done\n}\n\ncheck_path() {\n    skip_on_windows\n    # Check if ./rd/bin was removed from the path\n    # TODO add check for config.fish\n    env_profiles=(\n        \"$HOME/.bashrc\"\n        \"$HOME/.zshrc\"\n        \"$HOME/.cshrc\"\n        \"$HOME/.tcshrc\"\n    )\n    for candidate in .bash_profile .bash_login .profile; do\n        if [ -e \"$HOME/$candidate\" ]; then\n            env_profiles+=(\"$HOME/$candidate\")\n            # Only the first candidate that exists will be modified\n            if [ \"${assert}\" = \"refute\" ]; then\n                break\n            fi\n        fi\n    done\n\n    for profile in \"${env_profiles[@]}\"; do\n        echo \"$assert that $profile does not add ~/.rd/bin to the PATH\"\n        # cshrc: setenv PATH \"/Users/jan/.rd/bin\"\\:\"$PATH\"\n        # posix: export PATH=\"/Users/jan/.rd/bin:$PATH\"\n        run grep \"PATH.\\\"$HOME/.rd/bin\" \"$profile\"\n        \"${assert}_failure\"\n    done\n}\n\ncheck_rd_context() {\n    skip_on_windows\n    # Check if the rancher-desktop docker context has been removed\n    if using_docker; then\n        echo \"$assert that the docker context rancher-desktop does not exist\"\n        run grep -r rancher-desktop \"$HOME/.docker/contexts/meta\"\n        \"${assert}_failure\"\n    fi\n}\n\ncheck_lima() {\n    skip_on_windows\n    # Check that the VM has been removed and no longer exists.\n    run limactl ls\n    \"${assert}_output\" --regexp \"No instance found|no such file or directory\"\n}\n\ncheck_WSL() {\n    skip_on_unix\n    # Check if rancher-desktop WSL distros are deleted on Windows\n    run powershell.exe -c \"wsl.exe --list\"\n    \"${refute}_output\" --partial \"rancher-desktop-data\"\n    \"${refute}_output\" --partial \"rancher-desktop\"\n}\n\ncheck_updater_longhorn_gone() {\n    assert_not_exists \"$PATH_CACHE/updater-longhorn.json\"\n}\n\ntouch_updater_longhorn() {\n    touch \"$PATH_CACHE/updater-longhorn.json\"\n}\n"
  },
  {
    "path": "bats/tests/containers/host-connectivity.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\nverify_host_connectivity() {\n    run ctrctl run --rm \"$IMAGE_BUSYBOX\" timeout -s INT 10 ping -c 5 \"$1\"\n    assert_success\n    assert_output --partial \"5 packets transmitted, 5 packets received, 0% packet loss\"\n}\n\n@test 'ping host.docker.internal from a container' {\n    verify_host_connectivity \"host.docker.internal\"\n}\n\n@test 'ping host.rancher-desktop.internal from a container' {\n    verify_host_connectivity \"host.rancher-desktop.internal\"\n}\n"
  },
  {
    "path": "bats/tests/containers/host-network-ports.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\nLOCALHOST=\"127.0.0.1\"\n\nlocal_setup() {\n    if ! is_windows; then\n        skip \"The test doesn't work on non-Windows platforms\"\n    fi\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\nrun_container_with_host_network_driver() {\n    local image=\"python:slim\"\n    ctrctl pull --quiet \"$image\"\n    ctrctl run -d --network=host --restart=no \"$image\" \"$@\"\n}\n\nverify_container_port() {\n    run try --max 9 --delay 10 curl --insecure --verbose --show-error \"$@\"\n    assert_success\n    assert_output --partial 'Directory listing for'\n}\n\n@test 'process is bound to 0.0.0.0 using host network driver' {\n    local container_port=\"8010\"\n    run_container_with_host_network_driver python -m http.server \"$container_port\"\n    verify_container_port \"http://$LOCALHOST:$container_port\"\n    skip_unless_host_ip\n    verify_container_port \"http://${HOST_IP}:$container_port\"\n}\n\n@test 'process is bound to 127.0.0.1 using host network driver' {\n    local container_port=\"8016\"\n    run_container_with_host_network_driver python -m http.server $container_port --bind \"$LOCALHOST\"\n    verify_container_port \"http://$LOCALHOST:$container_port\"\n    skip_unless_host_ip\n    run curl --verbose --head \"http://${HOST_IP}:$container_port\"\n    assert_output --partial \"curl: (7) Failed to connect\"\n}\n"
  },
  {
    "path": "bats/tests/containers/init.bats",
    "content": "# verify that running a container with --init is working\n# bats file_tags=opensuse\n\nload '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'run container with init process' {\n    # BUG BUG BUG\n    # The following `ctrctl run` command includes the `-i` option to work around a docker\n    # bug on Windows: https://github.com/rancher-sandbox/rancher-desktop/issues/3239\n    # It is harmless in other configurations, but should not be required here.\n    # BUG BUG BUG\n    run ctrctl run -i --rm --init \"$IMAGE_BUSYBOX\" ps -ef\n    assert_success\n    # PID   USER     TIME  COMMAND\n    #     1 root      0:00 /sbin/docker-init -- ps -ef\n    #     1 root      0:00 /sbin/tini -- ps -ef\n    assert_line --regexp '^ +1 .+ /sbin/(docker-init|tini) -- ps -ef$'\n}\n"
  },
  {
    "path": "bats/tests/containers/platform.bats",
    "content": "load '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\ncheck_uname() {\n    local platform=\"linux/$1\"\n    local cpu=\"$2\"\n\n    # Pull container separately because `ctrctl run` doesn't have a --quiet option\n    ctrctl pull --quiet --platform \"$platform\" \"$IMAGE_BUSYBOX\"\n\n    # BUG BUG BUG\n    # Adding -i option to work around a bug with the Linux docker CLI in WSL\n    # https://github.com/rancher-sandbox/rancher-desktop/issues/3239\n    # BUG BUG BUG\n    run ctrctl run -i --platform \"$platform\" \"$IMAGE_BUSYBOX\" uname -m\n    if is_true \"${assert_success:-true}\"; then\n        assert_success\n        assert_output \"$cpu\"\n    fi\n}\n\n@test 'deploy amd64 container' {\n    check_uname amd64 x86_64\n}\n\n@test 'deploy arm64 container' {\n    if is_windows; then\n        # TODO why don't we do this?\n        skip \"aarch64 emulation is not included in the Windows version\"\n    fi\n    check_uname arm64 aarch64\n}\n\n@test 'uninstall s390x emulator' {\n    if is_windows; then\n        # On WSL the emulator might still be installed from a previous run\n        ctrctl run --privileged --rm \"$IMAGE_TONISTIIGI_BINFMT\" --uninstall qemu-s390x\n    else\n        skip \"only required on Windows\"\n    fi\n}\n\n@test 'deploy s390x container does not work' {\n    assert_success=false check_uname s390x s390x\n    assert_failure\n    assert_output --partial \"exec /bin/uname: exec format error\"\n}\n\n@test 'install s390x emulator' {\n    ctrctl run --privileged --rm \"$IMAGE_TONISTIIGI_BINFMT\" --install s390x\n}\n\n@test 'deploy s390x container' {\n    check_uname s390x s390x\n}\n"
  },
  {
    "path": "bats/tests/containers/published-ports.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\nrun_container_with_published_port() {\n    ctrctl pull --quiet \"$IMAGE_NGINX\"\n    ctrctl run -d -p \"$@\" --restart=no \"$IMAGE_NGINX\"\n}\n\nverify_container_published_port() {\n    run try --max 9 --delay 10 curl --insecure --verbose --show-error \"$@\"\n    assert_success\n    assert_output --partial 'Welcome to nginx!'\n}\n\n@test 'container published port binding to localhost' {\n    run_container_with_published_port \"127.0.0.1:8080:80\"\n    verify_container_published_port \"http://127.0.0.1:8080\"\n}\n\n@test 'container published port binding to localhost should not be accessible via 0.0.0.0' {\n    skip_unless_host_ip\n    run curl --verbose --head \"http://${HOST_IP}:8080\"\n    assert_output --partial \"curl: (7) Failed to connect\"\n}\n\n@test 'container published port binding to 0.0.0.0' {\n    skip_unless_host_ip\n    run_container_with_published_port \"8081:80\"\n    verify_container_published_port \"http://${HOST_IP}:8081\"\n}\n"
  },
  {
    "path": "bats/tests/containers/published-udp-ports.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    skip_on_unix\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\nbuild_alpine_socat_image() {\n    cat <<EOF | ctrctl build -t socat-udp-test -f- .\nFROM ${IMAGE_ALPINE}\nRUN apk add --no-cache socat\nCMD [\"sh\", \"-c\", \"socat -v -T1 UDP-RECVFROM:\\${PORT},fork STDOUT\"]\nEOF\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n    build_alpine_socat_image\n}\n\nrun_container_with_published_udp_port_and_connect() {\n    local ip=$1\n    local port=$2\n    local netcat_connect_addr=$3\n    ctrctl run -d --name socat-udp-\"$port\" -p \"$ip\":\"$port\":\"$port\"/udp --env PORT=\"$port\" socat-udp-test\n    run try --max 10 --delay 10 nc -u -w1 \"$netcat_connect_addr\" \"$port\" <<<\"hello from nc UDP port $port\"\n    assert_success\n    run ctrctl logs socat-udp-\"$port\"\n    assert_success\n}\n\n@test 'container published UDP port binding to localhost' {\n    port=$(shuf -i 20000-30000 -n 1)\n    run_container_with_published_udp_port_and_connect \"127.0.0.1\" \"$port\" \"127.0.0.1\"\n    assert_output --partial \"hello from nc UDP port $port\"\n}\n\n@test 'container published port binding to localhost should not be accessible via non localhost' {\n    port=$(shuf -i 20000-30000 -n 1)\n    skip_unless_host_ip\n    run_container_with_published_udp_port_and_connect \"127.0.0.1\" \"$port\" \"${HOST_IP}\"\n    refute_output --partial \"hello from nc UDP port $port\"\n}\n\n@test 'container published UDP port binding to 0.0.0.0' {\n    port=$(shuf -i 20000-30000 -n 1)\n    skip_unless_host_ip\n    run_container_with_published_udp_port_and_connect \"0.0.0.0\" \"$port\" \"${HOST_IP}\"\n    assert_output --partial \"hello from nc UDP port $port\"\n}\n"
  },
  {
    "path": "bats/tests/containers/reset.bats",
    "content": "load '../helpers/load'\n\nlocal_setup_file() {\n    RD_USE_RAMDISK=false # interferes with deleting $PATH_APP_HOME\n}\n\n@test 'factory reset' {\n    rdctl_reset --factory --cache\n}\n\n@test 'Start up Rancher Desktop' {\n    start_application\n}\n\n@test 'Verify that the expected directories were created' {\n    CACHE=1 before check_directories\n}\n\n@test 'Verify that docker symlinks were created' {\n    before check_docker_symlinks\n}\n\n@test 'Verify that path management was set' {\n    before check_path\n}\n\n@test 'Verify that rancher desktop context was created' {\n    before check_rd_context\n}\n\n@test 'Verify that lima VM was created' {\n    before check_lima\n}\n\n@test 'Verify that WSL distributions were created' {\n    before check_WSL\n}\n\n@test 'Shutdown Rancher Desktop' {\n    rdctl shutdown\n}\n\n@test 'factory-reset when Rancher Desktop is not running' {\n    touch_updater_longhorn\n    rdctl_reset --factory\n}\n\n@test 'Verify that the expected directories were deleted' {\n    CACHE=1 check_directories\n}\n\n@test 'Verify that docker symlinks were deleted' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted' {\n    check_updater_longhorn_gone\n}\n\n@test 'Start Rancher Desktop 2' {\n    start_application\n}\n\n@test 'factory reset while running - keep caches' {\n    rdctl reset --factory\n}\n\n@test 'Verify that the expected directories were deleted 2' {\n    CACHE=1 check_directories\n}\n\n@test 'Verify that docker symlinks were deleted 2' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset 2' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted 2' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted 2' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted 2' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted 2' {\n    check_updater_longhorn_gone\n}\n\n@test 'Start Rancher Desktop 3' {\n    start_application\n}\n\n@test 'factory reset while running - delete caches' {\n    rdctl_reset --factory --cache\n}\n\n@test 'Verify that the expected directories were deleted 3' {\n    CACHE=0 check_directories\n}\n\n@test 'Verify that docker symlinks were deleted 3' {\n    check_docker_symlinks\n}\n\n@test 'Verify that path management was unset 3' {\n    check_path\n}\n\n@test 'Verify that rancher desktop context was deleted 3' {\n    check_rd_context\n}\n\n@test 'Verify that lima VM was deleted 3' {\n    check_lima\n}\n\n@test 'Verify that WSL distributions were deleted 3' {\n    check_WSL\n}\n\n@test 'Verify updater-longhorn.json was deleted when cache was retained' {\n    check_updater_longhorn_gone\n}\n\n@test 'Start up Rancher Desktop (for non-factory reset)' {\n    start_application\n}\n\n@test 'Deploy kubernetes workloads' {\n    CONTAINERD_NAMESPACE=k8s.io ctrctl image pull --quiet \"${IMAGE_NGINX:?}\"\n    kubectl create deployment --replicas 2 --image \"${IMAGE_NGINX:?}\" bats-nginx\n    kubectl wait --for=condition=Available deployment/bats-nginx\n}\n\n@test 'Make modifications to the VM' {\n    rdctl shell sudo cp /etc/os-release /etc/marker-file\n    rdctl shell ls -l /etc/marker-file\n}\n\n@test 'Reset only Kubernetes' {\n    rdctl_reset --k8s\n    wait_for_kubelet\n}\n\n@test 'Verify Kubernetes workloads removed' {\n    run kubectl get deployment/bats-nginx\n    assert_failure\n}\n\n@test 'Verify VM modifications persist' {\n    rdctl shell ls -l /etc/marker-file\n}\n\n@test 'Re-deploy kubernetes workloads' {\n    CONTAINERD_NAMESPACE=k8s.io ctrctl image pull --quiet \"${IMAGE_NGINX:?}\"\n    kubectl create deployment --replicas 2 --image \"${IMAGE_NGINX:?}\" bats-nginx\n    kubectl wait --for=condition=Available deployment/bats-nginx\n}\n\n@test 'Reset VM' {\n    run rdctl_reset --vm\n    assert_success\n    assert_output 'Rancher Desktop wipe reset successful'\n}\n\n@test 'Verify VM modifications removed' {\n    wait_for_shell\n    rdctl shell ls -l /etc # ensure `ls` works correctly.\n    run rdctl shell ls -l /etc/marker-file\n    assert_failure\n}\n\n@test 'Verify Kubernetes workloads removed again' {\n    wait_for_kubelet\n    run kubectl get deployment/bats-nginx\n    assert_failure\n}\n\nrdctl_reset() {\n    capture_logs\n    rdctl reset --verbose \"$@\"\n}\n\ncheck_directories() {\n    # Check if all expected directories are created after starting application/ are deleted after a factory reset\n    delete_dir=(\"$PATH_LOGS\" \"$PATH_APP_HOME/credential-server.json\" \"$PATH_APP_HOME/rd-engine.json\")\n    if is_unix; then\n        # On Windows \"$PATH_CONFIG\" == \"$PATH_APP_HOME\"\n        delete_dir+=(\"$HOME/.rd\" \"$LIMA_HOME\" \"$PATH_CONFIG\")\n        # We can't make any general assertion on AppHome/snapshots - we don't know if it was created or not\n        # So just assert on the other members of AppHome\n        # TODO on macOS (not implemented by `rdctl factory-reset`)\n        # ~/Library/Saved Application State/io.rancherdesktop.app.savedState\n        # this one only exists after an update has been downloaded\n        # ~/Library/Application Support/Caches/rancher-desktop-updater\n    fi\n\n    if is_windows; then\n        # On Windows $PATH_CONFIG is the same as $PATH_APP_HOME\n        delete_dir+=(\"$PATH_CONFIG_FILE\" \"$PATH_DISTRO\" \"$PATH_DISTRO_DATA\")\n        # TODO: What about  $PATH_APP_HOME/vtunnel-config.yaml ?\n    fi\n\n    for dir in \"${delete_dir[@]}\"; do\n        echo \"# ${assert:?} that $dir does not exist\" 1>&3\n        \"${assert}_not_exists\" \"$dir\"\n    done\n\n    if is_false \"${CACHE:-1}\"; then\n        echo \"# assert that cache does not exist\" >&3\n        assert_not_exists \"$PATH_CACHE\"\n    else\n        echo \"# assert that cache does exists\" >&3\n        assert_exists \"$PATH_CACHE\"\n    fi\n}\n\ncheck_docker_symlinks() {\n    skip_on_windows\n    # Check if docker-X symlinks were deleted\n    for dfile in docker-buildx docker-compose; do\n        run readlink \"$HOME/.docker/cli-plugins/$dfile\"\n        \"${refute:?}_output\" \"$HOME/.rd/bin/$dfile\"\n    done\n}\n\ncheck_path() {\n    skip_on_windows\n    # Check if ./rd/bin was removed from the path\n    # TODO add check for config.fish\n    env_profiles=(\n        \"$HOME/.bashrc\"\n        \"$HOME/.zshrc\"\n        \"$HOME/.cshrc\"\n        \"$HOME/.tcshrc\"\n    )\n    for candidate in .bash_profile .bash_login .profile; do\n        if [ -e \"$HOME/$candidate\" ]; then\n            env_profiles+=(\"$HOME/$candidate\")\n            # Only the first candidate that exists will be modified\n            if [ \"${assert}\" = \"refute\" ]; then\n                break\n            fi\n        fi\n    done\n\n    for profile in \"${env_profiles[@]}\"; do\n        echo \"$assert that $profile does not add ~/.rd/bin to the PATH\"\n        # cshrc: setenv PATH \"/Users/jan/.rd/bin\"\\:\"$PATH\"\n        # posix: export PATH=\"/Users/jan/.rd/bin:$PATH\"\n        run grep \"PATH.\\\"$HOME/.rd/bin\" \"$profile\"\n        \"${assert}_failure\"\n    done\n}\n\ncheck_rd_context() {\n    skip_on_windows\n    # Check if the rancher-desktop docker context has been removed\n    if using_docker; then\n        echo \"$assert that the docker context rancher-desktop does not exist\"\n        run grep -r rancher-desktop \"$HOME/.docker/contexts/meta\"\n        \"${assert}_failure\"\n    fi\n}\n\ncheck_lima() {\n    skip_on_windows\n    # Check that the VM has been removed and no longer exists.\n    run limactl ls\n    \"${assert}_output\" --regexp \"No instance found|no such file or directory\"\n}\n\ncheck_WSL() {\n    skip_on_unix\n    # Check if rancher-desktop WSL distros are deleted on Windows\n    run powershell.exe -c \"wsl.exe --list\"\n    \"${refute}_output\" --partial \"rancher-desktop-data\"\n    \"${refute}_output\" --partial \"rancher-desktop\"\n}\n\ncheck_updater_longhorn_gone() {\n    assert_not_exists \"$PATH_CACHE/updater-longhorn.json\"\n}\n\ntouch_updater_longhorn() {\n    touch \"$PATH_CACHE/updater-longhorn.json\"\n}\n"
  },
  {
    "path": "bats/tests/containers/run-rancher.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\nRD_FILE_RAMDISK_SIZE=12 # We need more disk to run the Rancher image.\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'run rancher' {\n    local rancher_image\n    rancher_image=\"rancher/rancher:$(rancher_image_tag)\"\n\n    ctrctl pull --quiet \"$rancher_image\"\n    ctrctl run --privileged -d --restart=no -p 8080:80 -p 8443:443 --name rancher \"$rancher_image\"\n}\n\n@test 'verify rancher' {\n    local max_tries=9\n    if [[ -n ${CI:-} ]]; then\n        max_tries=30\n    fi\n    run try --max $max_tries --delay 10 curl --insecure --silent --show-error \"https://localhost:8443/dashboard/auth/login\"\n    assert_success\n    assert_output --partial \"Rancher Dashboard\"\n    run ctrctl logs rancher\n    assert_success\n    assert_output --partial \"Bootstrap Password:\"\n}\n"
  },
  {
    "path": "bats/tests/containers/split-dns-vpn.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\nREGISTRY_URL=$(echo \"$RD_VPN_TEST_IMAGE\" | cut -d'/' -f1)\n\nlocal_setup() {\n    if ! using_vpn_test_image; then\n        skip \"This test requires a connection to the designated VPN.\"\n    fi\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'Can access private registry over VPN from host' {\n    run curl -I -k \"https://$REGISTRY_URL/v2\"\n    assert_success\n    # We avoid assert_line here due to the trailing carriage return (\\r) issues.\n    assert_output --partial \"docker-distribution-api-version: registry/2.0\"\n}\n\n@test 'Can pull image from private registry over VPN' {\n    run ctrctl pull --quiet \"$RD_VPN_TEST_IMAGE\"\n    assert_success\n}\n\n@test 'Can verify container access to the registry' {\n    run ctrctl run --rm \"$IMAGE_NGINX\" curl -I -k \"https://$REGISTRY_URL/v2\"\n    assert_success\n    assert_output --partial \"docker-distribution-api-version: registry/2.0\"\n}\n\n@test 'Verify that a container can ping host.rancher-desktop.internal when the VPN is enabled' {\n    run ctrctl run --rm \"$IMAGE_BUSYBOX\" timeout -s INT 10 ping -c 5 host.rancher-desktop.internal\n    assert_success\n    assert_output --partial \"5 packets transmitted, 5 packets received, 0% packet loss\"\n}\n"
  },
  {
    "path": "bats/tests/containers/switch-engines.bats",
    "content": "# Test case 20\n\nload '../helpers/load'\nRD_CONTAINER_ENGINE=moby\n\nswitch_container_engine() {\n    local name=$1\n    RD_CONTAINER_ENGINE=\"${name}\"\n    # Make sure the backend is idle, to prevent wait_for_container_engine from\n    # erroring because the wrong engine is up.\n    wait_for_backend\n    rdctl set --container-engine.name=\"${name}\"\n    wait_for_container_engine\n}\n\npull_containers() {\n    ctrctl pull --quiet \"$IMAGE_NGINX\"\n    ctrctl pull --quiet \"$IMAGE_BUSYBOX\"\n    ctrctl run -d -p 8085:80 --restart=no \"$IMAGE_NGINX\"\n    ctrctl run -d --restart=always \"$IMAGE_BUSYBOX\" /bin/sh -c \"sleep inf\"\n    run ctrctl ps --format '{{json .Image}}'\n    assert_output --partial \"$IMAGE_NGINX\"\n    assert_output --partial \"$IMAGE_BUSYBOX\"\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start moby and pull nginx' {\n    start_container_engine\n    wait_for_container_engine\n    pull_containers\n}\n\n@test \"switch to containerd\" {\n    switch_container_engine containerd\n    pull_containers\n}\n\nverify_post_switch_containers() {\n    run ctrctl ps --format '{{json .Image}}'\n    assert_output --partial \"$IMAGE_BUSYBOX\"\n    refute_output --partial \"$IMAGE_NGINX\"\n}\n\nswitch_back_verify_post_switch_containers() {\n    local name=$1\n    switch_container_engine \"${name}\"\n    try --max 12 --delay 5 verify_post_switch_containers\n}\n\n@test 'switch back to moby and verify containers' {\n    switch_back_verify_post_switch_containers moby\n}\n\n@test 'switch back to containerd and verify containers' {\n    switch_back_verify_post_switch_containers containerd\n}\n"
  },
  {
    "path": "bats/tests/containers/volumes.bats",
    "content": "load '../helpers/load'\n\nget_tempdir() {\n    if ! is_windows || ! using_windows_exe; then\n        echo \"$BATS_TEST_TMPDIR\"\n        return\n    fi\n    # On Windows, create a temporary directory that is in the Windows temporary\n    # directory so that it mounts correctly.  Note that in CI we end up running\n    # with PSModulePath set to pwsh (7.x) paths, and that breaks the code for\n    # PowerShell 5.1.  So we need to have alternative code in that case.\n    # See also https://github.com/PowerShell/PowerShell/issues/14100\n    if command -v pwsh.exe &>/dev/null; then\n        # shellcheck disable=SC2016 # Don't expand PowerShell expansion\n        local command='\n            $([System.IO.Directory]::CreateTempSubdirectory()).FullName\n        '\n        run pwsh.exe -Command \"$command\"\n        assert_success\n    else\n        # PowerShell 5.1 is built against .net Framework 4.x and doesn't have\n        # [System.IO.Directory]::CreateTempSubdirectory(); create a temporary\n        # file and use its name instead.\n        # shellcheck disable=SC2016 # Don't expand PowerShell expansion\n        local command='\n            $name = New-TemporaryFile\n            Remove-Item -Path $name\n            # In case anti-virus etc. holds files open, wait for a second to let\n            # things settle before we create a new directory with the same name.\n            Start-Sleep -Seconds 1\n            New-Item -Type Directory -Path $name | Out-Null\n            $name.FullName\n        '\n        run powershell.exe -Command \"$command\"\n        assert_success\n    fi\n    run wslpath -u \"$output\"\n    assert_success\n    echo \"$output\" | tr -d \"\\r\"\n}\n\nlocal_setup() {\n    run get_tempdir\n    assert_success\n    export WORK_PATH=$output\n    run host_path \"$WORK_PATH\"\n    assert_success\n    export HOST_WORK_PATH=$output\n    export EXPECT_FAILURE=false\n}\n\nlocal_teardown() {\n    # Only do manual deletion on Windows; elsewhere we use BATS_TEST_TMPDIR so\n    # BATS is expected to do the cleanup.\n    if is_windows && [[ -n $HOST_WORK_PATH ]]; then\n        powershell.exe -Command \"Remove-Item -Recurse -LiteralPath '$HOST_WORK_PATH'\"\n    fi\n}\n\nknown_failure_on_mount_type() {\n    local mount_type=$1\n    local actual_type=$RD_MOUNT_TYPE\n\n    if is_windows; then\n        if using_windows_exe; then\n            actual_type=win32\n        else\n            actual_type=wsl\n        fi\n    fi\n    if [ \"$actual_type\" = \"$mount_type\" ]; then\n        comment \"Test is known to fail on $RD_MOUNT_TYPE mounts\"\n        assert=refute\n        refute=assert\n        EXPECT_FAILURE=true\n    fi\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    if is_linux; then\n        # On linux, mount BATS_RUN_TMPDIR into the VM so that we can use\n        # BATS_TEST_TMPDIR as a volume.\n        local override_dir=\"${HOME}/.local/share/rancher-desktop/lima/_config\"\n        mkdir -p \"$override_dir\"\n        {\n            echo \"mounts:\"\n            echo \"- location: ${BATS_RUN_TMPDIR}\"\n            echo \"  writable: true\"\n        } >\"$override_dir/override.yaml\"\n    fi\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'read-only volume mount' {\n    # Read a file that was created outside the container.\n    file_name=foo\n    file_path=$WORK_PATH/$file_name\n    file_content=hello\n\n    assert_not_exists \"$file_path\"\n    create_file \"$file_path\" <<<$file_content\n\n    # Use `--separate-stderr` to avoid image pull messages.\n    run --separate-stderr \\\n        ctrctl run --volume \"$HOST_WORK_PATH:/mount:ro\" \\\n        \"$IMAGE_BUSYBOX\" cat /mount/$file_name\n    assert_success\n    assert_output $file_content\n}\n\n@test 'read-write volume mount' {\n    file_name=foo\n    file_path=$WORK_PATH/$file_name\n    file_content=hello\n\n    # Create a file from the container.\n    assert_not_exists \"$file_path\"\n    ctrctl run --volume \"$HOST_WORK_PATH:/mount:rw\" \\\n        \"$IMAGE_BUSYBOX\" sh -c \"echo $file_content > /mount/$file_name\"\n\n    # Check that the file was written to.\n    assert_file_contains \"$file_path\" $file_content\n}\n\n@test 'read-write single file using --mount' {\n    file_name=foo\n    file_content=hello\n\n    create_file \"$WORK_PATH/$file_name\" <<<$file_content\n    run --separate-stderr \\\n        ctrctl run --mount \"source=$HOST_WORK_PATH/$file_name,target=/mount,type=bind\" \\\n        \"$IMAGE_BUSYBOX\" cat /mount\n    assert_success\n    assert_output $file_content\n}\n\n@test 'read-write volume mount as user' {\n    known_failure_on_mount_type 9p\n\n    file_name=foo\n    file_contents=hello\n    host_file_path=$HOST_WORK_PATH/$file_name\n\n    # Create a file from within the container.\n    run ctrctl run --volume \"$HOST_WORK_PATH:/mount:rw\" \\\n        --user 1000:1000 \"$IMAGE_BUSYBOX\" sh -c \"echo $file_contents > /mount/$file_name\"\n    \"${assert}_success\"\n    run cat \"$WORK_PATH/$file_name\"\n    \"${assert}_success\"\n    if is_true \"$EXPECT_FAILURE\"; then\n        skip \"Test expected to fail\"\n    fi\n    assert_output $file_contents\n\n    # Try to append to the file.\n    ctrctl run --volume \"$HOST_WORK_PATH:/mount:rw\" \\\n        --user 1000:1000 \"$IMAGE_BUSYBOX\" sh -c \"echo $file_contents | tee -a /mount/$file_name\"\n    # Check that the file was modified.\n    run cat \"$WORK_PATH/$file_name\"\n    assert_success\n    assert_output $file_contents$'\\n'$file_contents\n    if is_windows && using_windows_exe; then\n        # On Windows, the directory may be owned by a group that the user is in;\n        # additionally, there isn't an easy API to get effective access (!?).\n        if command -v pwsh.exe &>/dev/null; then\n            # shellcheck disable=SC2016 # Don't expand PowerShell expansion\n            local command='\n                $typeName = \"System.Security.Principal.SecurityIdentifier, System.Security.Principal.Windows\"\n                $type = [System.Type]::GetType($typeName)\n                $owner = $(Get-Acl '\"'$host_file_path'\"').GetOwner($type)\n                $owner.Value\n            '\n            run pwsh.exe -Command \"$command\"\n            assert_success\n        else\n            # shellcheck disable=SC2016 # Don't expand PowerShell expansion\n            local command='\n                $type = [System.Type]::GetType(\"System.Security.Principal.SecurityIdentifier\")\n                $owner = $(Get-Acl '\"'$host_file_path'\"').GetOwner($type)\n                $owner.Value\n            '\n            run powershell.exe -Command \"$command\"\n            assert_success\n        fi\n        local undo\n        undo=$(shopt -p extglob || true)\n        shopt -s extglob\n        local owner=${output%%*([[:space:]])}\n        eval \"$undo\"\n        # shellcheck disable=SC2016 # Don't expand PowerShell expansion\n        command='\n            $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()\n            $groups = $identity.Groups\n            $groups.Add($identity.User)\n            $groups | ForEach-Object { $_.Value }\n        '\n        run powershell.exe -Command \"$command\"\n        assert_success\n        run cat <<<\"${output//$'\\r'/}\" # Remove carriage returns\n        assert_success\n        assert_line \"$owner\"\n    else\n        # Check that the file is owned by the current user.\n        stat_arg=-f # Assume BSD stat\n        if { stat --version || true; } | grep 'GNU coreutils'; then\n            stat_arg=-c\n        fi\n        run stat \"$stat_arg\" '%u:%g' \"$WORK_PATH/foo\"\n        assert_success\n        assert_output \"$(id -u):$(id -g)\"\n    fi\n}\n\n@test 'host directory does not exist' {\n    if using_docker; then\n        known_failure_on_mount_type reverse-sshfs\n        known_failure_on_mount_type 9p\n    fi\n\n    file_name=foo\n    dir_name=baz\n    file_contents=hello\n\n    # Create a file from the container.\n    assert_not_exists \"$WORK_PATH/$dir_name\"\n    run ctrctl run --volume \"$HOST_WORK_PATH/$dir_name:/mount:rw\" \\\n        \"$IMAGE_BUSYBOX\" sh -c \"echo $file_contents > /mount/$file_name\"\n    \"${assert}_success\"\n    # Check that the file was written to.\n    if is_true \"$EXPECT_FAILURE\"; then\n        assert_file_not_exists \"$WORK_PATH/$dir_name\"\n    else\n        assert_file_exists \"$WORK_PATH/$dir_name/$file_name\"\n        assert_file_contains \"$WORK_PATH/$dir_name/$file_name\" $file_contents\n    fi\n}\n\n@test 'directory contains space' {\n    dir_name=\"hello world\"\n    file_name=foo\n    file_contents=hello\n\n    assert_not_exists \"$WORK_PATH/$dir_name\"\n    mkdir \"$WORK_PATH/$dir_name\"\n    ctrctl run --volume \"$HOST_WORK_PATH/$dir_name:/mount:rw\" \\\n        \"$IMAGE_BUSYBOX\" sh -c \"echo $file_contents > /mount/$file_name\"\n    assert_file_exists \"$WORK_PATH/$dir_name/$file_name\"\n    assert_file_contains \"$WORK_PATH/$dir_name/$file_name\" $file_contents\n}\n\n@test 'directory contains non-ascii' {\n    dir_name=snow☃︎man\n    file_name=foo\n    file_contents=hello\n\n    assert_not_exists \"$WORK_PATH/$dir_name\"\n    mkdir \"$WORK_PATH/$dir_name\"\n    ctrctl run --volume \"$HOST_WORK_PATH/$dir_name:/mount:rw\" \\\n        \"$IMAGE_BUSYBOX\" sh -c \"echo $file_contents > /mount/$file_name\"\n    assert_file_exists \"$WORK_PATH/$dir_name/$file_name\"\n    assert_file_contains \"$WORK_PATH/$dir_name/$file_name\" \"$file_contents\"\n}\n\n@test 'directory should be owned by current user' {\n    known_failure_on_mount_type virtiofs\n    known_failure_on_mount_type 9p\n    known_failure_on_mount_type reverse-sshfs\n    known_failure_on_mount_type win32\n\n    user_id=3678:2974\n\n    run --separate-stderr \\\n        ctrctl run --volume \"$HOST_WORK_PATH:/mount:ro\" \\\n        --user $user_id \"$IMAGE_BUSYBOX\" stat -c '%u:%g' /mount\n    assert_success\n    \"${assert}_output\" $user_id\n}\n\n@test 'change ownership of mounted file' {\n    known_failure_on_mount_type reverse-sshfs\n    known_failure_on_mount_type 9p\n\n    file_name=foo\n    file_contents=hello\n\n    run ctrctl run --volume \"$HOST_WORK_PATH:/mount:rw\" \\\n        --user 0 \"$IMAGE_BUSYBOX\" \\\n        sh -c \"echo $file_contents > /mount/$file_name; chown 1234:5678 /mount/$file_name\"\n    \"${assert}_success\"\n    assert_file_exists \"$WORK_PATH/$file_name\"\n    assert_file_contains \"$WORK_PATH/$file_name\" \"$file_contents\"\n}\n\n@test 'change file permissions' {\n    file_name=foo\n\n    assert_not_exists \"$WORK_PATH/$file_name\"\n    local command=\"\n        touch /mount/$file_name\n        chmod 0755 /mount/$file_name\n        stat -c %A /mount/$file_name\n    \"\n    run --separate-stderr \\\n        ctrctl run --volume \"$HOST_WORK_PATH:/mount:rw\" \\\n        \"$IMAGE_BUSYBOX\" sh -c \"$command\"\n    assert_success\n    \"${assert}_output\" -rwxr-xr-x # spellcheck-ignore-line\n}\n"
  },
  {
    "path": "bats/tests/containers/wasm.bats",
    "content": "# shellcheck disable=SC2030,SC2031\n# See https://github.com/koalaman/shellcheck/issues/2431\n# https://www.shellcheck.net/wiki/SC2030 -- Modification of output is local (to subshell caused by @bats test)\n# https://www.shellcheck.net/wiki/SC2031 -- output was modified in a subshell. That change might be lost\n\nload '../helpers/load'\n\n# Bundled shims are this version or newer.\nBUNDLED_VERSION=0.11.1\n# Manually managed versions intentionally use an older version\n# so we can verify that they still override the bundled version.\nMANUAL_VERSION=0.10.0\n\nlocal_setup() {\n    if using_containerd; then\n        skip \"this test only works on moby right now\"\n    fi\n    skip \"spin shim is broken with docker 28+; see #9476\"\n}\n\nlocal_teardown_file() {\n    rm -rf \"$PATH_CONTAINERD_SHIMS\"\n}\n\n@test 'factory reset' {\n    factory_reset\n    rm -rf \"$PATH_CONTAINERD_SHIMS\"\n}\n\n@test 'start engine without wasm support' {\n    start_container_engine --experimental.container-engine.web-assembly.enabled=false\n    wait_for_container_engine\n}\n\nshim_version() {\n    local shim=$1\n    local version=$2\n\n    run rdctl shell \"containerd-shim-${shim}-${version}\" -v\n    assert_success\n    semver \"$output\"\n}\n\n@test 'verify spin shim is not installed on PATH' {\n    run shim_version spin v2\n    assert_failure\n    assert_output --regexp 'containerd-shim-spin-v2.*(not found|No such file)'\n}\n\nhello() {\n    local shim=$1\n    local version=$2\n    local lang=$3\n    local port=$4\n    local internal_port=$5\n\n    # The '/' at the very end of the command is required by the container entrypoint.\n    ctrctl run \\\n        --detach \\\n        --name \"${shim}-demo-${port}\" \\\n        --runtime \"io.containerd.${shim}.${version}\" \\\n        --platform wasi/wasm \\\n        --publish \"${port}:${internal_port}\" \\\n        \"ghcr.io/deislabs/containerd-wasm-shims/examples/${shim}-${lang}-hello:v${MANUAL_VERSION}\" /\n}\n\n@test 'verify shim is not configured in container engine' {\n    run hello spin v2 rust 8080 80\n    assert_nothing                           # We assert after removing the container.\n    ctrctl rm --force spin-demo-8080 || true # Force delete the container if it got created.\n    assert_failure\n    assert_output --regexp 'operating system is not supported|binary not installed'\n}\n\n@test 'enable wasm support' {\n    pid=$(get_service_pid \"$CONTAINER_ENGINE_SERVICE\")\n    rdctl set --experimental.container-engine.web-assembly.enabled\n    try --max 15 --delay 5 refute_service_pid \"$CONTAINER_ENGINE_SERVICE\" \"$pid\"\n    wait_for_container_engine\n}\n\n@test \"check spin shim version >= ${BUNDLED_VERSION}\" {\n    run shim_version spin v2\n    assert_success\n    semver_gte \"$output\" \"$BUNDLED_VERSION\"\n}\n\n@test 'deploy sample spin app' {\n    hello spin v2 rust 8080 80\n}\n\ncheck_container_logs() {\n    run ctrctl logs spin-demo-8080\n    assert_success\n    assert_output --partial \"Available Routes\"\n}\n\n@test 'check wasm container logs' {\n    try --max 5 --delay 2 check_container_logs\n}\n\n@test 'verify wasm container is running' {\n    run curl --silent --fail http://localhost:8080/hello\n    assert_success\n    assert_output --partial \"Hello world from Spin!\"\n\n    run curl --silent --fail http://localhost:8080/go-hello\n    assert_success\n    assert_output --partial \"Hello Spin Shim!\"\n}\n\ndownload_shim() {\n    local shim=$1\n    local version=$2\n\n    local base_url=\"https://github.com/deislabs/containerd-wasm-shims/releases/download/v${MANUAL_VERSION}\"\n    local filename=\"containerd-wasm-shims-${version}-${shim}-linux-${ARCH}.tar.gz\"\n    local host_archive\n\n    # Since we end up using curl.exe on Windows, pass the host path to curl.\n    host_archive=$(host_path \"${PATH_CONTAINERD_SHIMS}/${filename}\")\n\n    mkdir -p \"$PATH_CONTAINERD_SHIMS\"\n    curl --location --output \"$host_archive\" \"${base_url}/${filename}\"\n    tar xfz \"${PATH_CONTAINERD_SHIMS}/${filename}\" --directory \"$PATH_CONTAINERD_SHIMS\"\n    rm \"${PATH_CONTAINERD_SHIMS}/${filename}\"\n}\n\n@test 'install user-managed shims' {\n    download_shim spin v2\n    download_shim wws v1\n\n    rdctl shutdown\n    launch_the_application\n    wait_for_container_engine\n}\n\nverify_shim() {\n    local shim=$1\n    local version=$2\n    local lang=$3\n    local port=$4\n    local external_port=$5\n\n    run shim_version \"${shim}\" \"${version}\"\n    assert_success\n    semver_eq \"$output\" \"$MANUAL_VERSION\"\n\n    hello \"$shim\" \"$version\" \"$lang\" \"$port\" \"$external_port\"\n    try --max 10 --delay 3 curl --silent --fail \"http://localhost:${port}/hello\"\n}\n\n@test 'verify spin shim' {\n    verify_shim spin v2 rust 8181 80\n    assert_output --partial \"Hello world from Spin!\"\n}\n\n@test 'verify wws shim' {\n    verify_shim wws v1 js 8282 3000\n    assert_output --partial \"Hello from Wasm Workers Server\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/allow-list.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    CONTAINERD_NAMESPACE=rancher-desktop-extensions\n    TESTDATA_DIR_HOST=$(host_path \"${PATH_BATS_ROOT}/tests/extensions/testdata/\")\n}\n\nwrite_allow_list() { # list\n    local list=${1:-}\n    local allowed=true\n\n    if [ -z \"$list\" ]; then\n        allowed=false\n    fi\n\n    # Note that the list preference is not writable using `rdctl set`, and we\n    # need to do a direct API call instead.\n    rdctl api /v1/settings --input - <<<'{\n        \"version\": 8,\n        \"application\": {\n            \"extensions\": {\n                \"allowed\": {\n                    \"enabled\": '\"${allowed}\"',\n                    \"list\": '\"${list:-[]}\"'\n                }\n            }\n        }\n    }'\n}\n\ncheck_extension_installed() { # refute, name\n    run rdctl extension ls\n    assert_success\n    \"${1:-assert}_output\" --partial \"${2:-rd/extension/basic}\"\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'build extension testing image' {\n    ctrctl build \\\n        --tag \"rd/extension/basic\" \\\n        --build-arg \"variant=basic\" \\\n        \"$TESTDATA_DIR_HOST\"\n\n    run ctrctl image list --format '{{ .Repository }}'\n    assert_success\n    assert_line \"rd/extension/basic\"\n}\n\n@test 'when no extension allow list is set up, all extensions can install' {\n    wait_for_extension_manager\n    write_allow_list ''\n    rdctl extension install rd/extension/basic\n    check_extension_installed\n    rdctl extension uninstall rd/extension/basic\n}\n\n@test 'empty allow list disables extension installs' {\n    write_allow_list '[]'\n    run rdctl extension install rd/extension/basic\n    assert_failure\n    check_extension_installed refute\n}\n\n@test 'when an extension is explicitly allowed, it can be installed' {\n    write_allow_list '[\"irrelevant/image\",\"rd/extension/basic:latest\"]'\n    rdctl extension install rd/extension/basic:latest\n    check_extension_installed\n    rdctl extension uninstall rd/extension/basic\n    check_extension_installed refute\n}\n\n@test 'when an extension is not in the allowed list, it cannot be installed' {\n    write_allow_list '[\"rd/extension/other\",\"registry.test/image\"]'\n    run rdctl extension install rd/extension/basic\n    assert_failure\n    check_extension_installed refute\n}\n\n@test 'when no tags given, any tag is allowed' {\n    write_allow_list '[\"rd/extension/basic\"]'\n    ctrctl tag rd/extension/basic rd/extension/basic:0.0.3\n    rdctl extension install rd/extension/basic:0.0.3\n    check_extension_installed\n    rdctl extension uninstall rd/extension/basic\n    check_extension_installed refute\n}\n\n@test 'when tags are given, only the specified tag is allowed' {\n    sleep 20\n    write_allow_list '[\"rd/extension/basic:0.0.2\"]'\n    ctrctl tag rd/extension/basic rd/extension/basic:0.0.3\n    run rdctl extension install rd/extension/basic:0.0.3\n    assert_failure\n    check_extension_installed refute\n}\n\n@test 'extensions can be allowed by organization' {\n    write_allow_list '[\"rd/extension/\"]'\n    rdctl extension install rd/extension/basic\n    check_extension_installed\n    rdctl extension uninstall rd/extension/basic\n    check_extension_installed refute\n}\n\n@test 'extensions can be allowed by repository host' {\n    write_allow_list '[\"registry.test/\"]'\n    ctrctl tag rd/extension/basic registry.test/basic:0.0.3\n    rdctl extension install registry.test/basic:0.0.3\n    check_extension_installed '' registry.test/basic\n    rdctl extension uninstall registry.test/basic\n    check_extension_installed refute registry.test/basic\n}\n"
  },
  {
    "path": "bats/tests/extensions/containers.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    CONTAINERD_NAMESPACE=rancher-desktop-extensions\n    TESTDATA_DIR_HOST=$(host_path \"${PATH_BATS_ROOT}/tests/extensions/testdata/\")\n}\n\nlocal_teardown_file() {\n    if using_docker; then\n        docker context use default\n        docker context rm bats-invalid-context\n    fi\n}\n\nid() { # variant\n    echo \"rd/extension/$1\"\n}\n\nencoded_id() { # variant\n    id \"$1\" | tr -d '\\r\\n' | base64 | tr '+/' '-_' | tr -d '='\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'default to custom docker context' {\n    if ! using_docker; then\n        skip 'docker context only applies when using docker backend'\n    fi\n    # Remove the context if it previously existed.\n    run docker context rm --force bats-invalid-context\n    assert_nothing\n    docker context create bats-invalid-context --docker 'host=tcp://invalid.test:99999'\n    docker context use bats-invalid-context\n}\n\n@test 'no extensions installed' {\n    wait_for_extension_manager\n    run rdctl api /v1/extensions\n    assert_success\n    assert_output $'\\x7b'$'\\x7d' # empty JSON dict, {}\n    assert_dir_not_exist \"$PATH_EXTENSIONS\"\n}\n\n@test 'build extension testing images' {\n    local extension\n    for extension in vm-image vm-compose; do\n        ctrctl build \\\n            --tag rd/extension/$extension \\\n            --build-arg variant=$extension \"$TESTDATA_DIR_HOST\"\n    done\n}\n\n@test 'image - install' {\n    rdctl api --method=POST \"/v1/extensions/install?id=$(id vm-image)\"\n\n    run rdctl api /v1/extensions\n    assert_success\n    run jq_output \".[\\\"$(id vm-image)\\\"].version\"\n    assert_output latest\n}\n\n@test 'image - check for running container' {\n    run ctrctl container ls\n    assert_success\n    assert_line --regexp \"$(id vm-image).*[[:space:]]Up[[:space:]]\"\n}\n\n@test 'image - uninstall' {\n    rdctl api --method=POST \"/v1/extensions/uninstall?id=$(id vm-image)\"\n\n    run ctrctl container ls --all\n    assert_success\n    refute_line --partial \"$(id vm-image)\"\n}\n\n@test 'compose - install' {\n    rdctl api --method=POST \"/v1/extensions/install?id=$(id vm-compose)\"\n\n    run rdctl api /v1/extensions\n    assert_success\n    run jq_output \".[\\\"$(id vm-compose)\\\"].version\"\n    assert_output latest\n}\n\n@test 'compose - check for running container' {\n    run ctrctl container ls\n    assert_success\n    assert_line --regexp \"$(id vm-compose).*[[:space:]]Up[[:space:]]\"\n}\n\n@test 'compose - check for dangling symlinks' {\n    if ! using_containerd; then\n        skip 'This test only applies to containerd'\n    fi\n    assert_exists \"$PATH_EXTENSIONS/$(encoded_id vm-compose)/compose/link\"\n    assert_not_exists \"$PATH_EXTENSIONS/$(encoded_id vm-compose)/compose/dangling-link\"\n}\n\n@test 'compose - uninstall' {\n    rdctl api --method=POST \"/v1/extensions/uninstall?id=$(id vm-compose)\"\n\n    run ctrctl container ls --all\n    assert_success\n    refute_line --partial \"$(id vm-compose)\"\n}\n\n@test 'compose - with a long name' {\n    local name\n    name=\"$(id vm-compose)-with-an-unusually-long-name-yes-it-is-very-long\"\n\n    ctrctl tag \"$(id vm-compose)\" \"$name\"\n    rdctl extension install \"$name\"\n    run ctrctl container ls --all\n    assert_success\n    assert_line --partial \"$(id vm-compose)\"\n    rdctl extension uninstall \"$name\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/install.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    CONTAINERD_NAMESPACE=rancher-desktop-extensions\n    TESTDATA_DIR=\"${PATH_BATS_ROOT}/tests/extensions/testdata/\"\n    TESTDATA_DIR_HOST=$(host_path \"$TESTDATA_DIR\")\n}\n\nassert_file_contents_equal() { # $have $want\n    local have=\"$1\" want=\"$2\"\n    assert_file_exist \"$have\"\n    assert_file_exist \"$want\"\n\n    local have_hash want_hash\n    # md5sum is not available on macOS unless you install GNU coreutils\n    have_hash=\"$(openssl md5 -r \"$have\" | cut -d ' ' -f 1)\"\n    want_hash=\"$(openssl md5 -r \"$want\" | cut -d ' ' -f 1)\"\n    if [ \"$have_hash\" != \"$want_hash\" ]; then\n        printf \"expected : %s (%s)\\nactual   : %s (%s)\" \\\n            \"$want\" \"$want_hash\" \"$have\" \"$have_hash\" |\n            batslib_decorate \"files are different\" |\n            fail\n    fi\n}\n\nid() { # variant\n    echo \"rd/extension/$1\"\n}\n\nencoded_id() { # variant\n    id \"$1\" | tr -d '\\r\\n' | base64 | tr '+/' '-_' | tr -d '='\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start container engine' {\n    start_container_engine\n    wait_for_container_engine\n    wait_for_extension_manager\n}\n\n@test 'no extensions installed' {\n    run rdctl extension ls\n    assert_success\n    assert_output \"No extensions are installed.\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS\"\n}\n\n@test 'build various extension testing images' {\n    local extension\n    local variants=(\n        basic host-binaries missing-icon missing-icon-file ui\n    )\n    for extension in \"${variants[@]}\"; do\n        ctrctl build \\\n            --tag \"rd/extension/$extension\" \\\n            --build-arg \"variant=$extension\" \\\n            \"$TESTDATA_DIR_HOST\"\n    done\n    run ctrctl image list --format '{{ .Repository }}'\n    assert_success\n    for extension in \"${variants[@]}\"; do\n        assert_line \"rd/extension/$extension\"\n    done\n}\n\n@test 'extension API - require auth' {\n    local port\n    run cat \"${PATH_APP_HOME}/rd-engine.json\"\n    assert_success\n    port=\"$(jq_output .port)\"\n    assert [ -n \"$port\" ]\n    run curl --fail \"http://127.0.0.1:${port}/v1/settings\"\n    assert_failure\n    assert_output --partial \"The requested URL returned error: 401\"\n}\n\n@test 'basic extension - install' {\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id basic)\"\n    rdctl extension install \"$(id basic)\"\n}\n\n@test 'basic extension - check extension is installed' {\n    run rdctl extension ls\n    assert_success\n    assert_line --partial \"rd/extension/basic\"\n}\n\n@test 'basic extension - check extension contents' {\n    assert_dir_exist \"$PATH_EXTENSIONS/$(encoded_id basic)\"\n    assert_file_contents_equal \"$PATH_EXTENSIONS/$(encoded_id basic)/icon.svg\" \"$TESTDATA_DIR/extension-icon.svg\"\n}\n\n@test 'basic extension - upgrades' {\n    local tag\n    ctrctl image tag \"$(id basic)\" \"$(id basic):0.0.1\"\n    ctrctl image tag \"$(id basic)\" \"$(id basic):v0.0.2\"\n\n    run rdctl extension ls\n    assert_success\n    assert_line --partial \"$(id basic):latest\"\n\n    rdctl extension install \"$(id basic)\"\n    run rdctl extension ls\n    assert_success\n    # The highest semver tag should be installed, replacing the existing one.\n    assert_line --partial \"$(id basic):v0.0.2\"\n}\n\n@test 'basic extension - uninstalling not installed version' {\n    rdctl extension uninstall \"$(id basic):0.0.1\"\n    run rdctl extension ls\n    assert_success\n    # Trying to uninstall a version that isn't installed should be a no-op\n    assert_line --partial \"$(id basic):v0.0.2\"\n}\n\n@test 'basic extension - uninstall' {\n    ctrctl image tag \"$(id basic)\" \"$(id basic):0.0.3\"\n    # Uninstall should remove whatever version is installed, not the newest.\n    rdctl extension uninstall \"$(id basic)\"\n\n    run rdctl extension ls\n    assert_success\n    assert_output \"No extensions are installed.\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id basic)\"\n}\n\n@test 'missing-icon - attempt to install' {\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id missing-icon)\"\n    run rdctl extension install \"$(id missing-icon)\"\n    assert_failure\n    assert_output --partial \"has invalid extension metadata\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id missing-icon)\"\n}\n\n@test 'missing-icon-file - attempt to install' {\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id missing-icon-file)\"\n    run rdctl extension install \"$(id missing-icon-file)\"\n    assert_failure\n    assert_output --partial \"Could not copy icon file does-not-exist.svg\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id missing-icon-file)\"\n}\n\n@test 'host-binaries - install' {\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)\"\n    run rdctl extension install \"$(id host-binaries)\"\n    assert_success\n}\n\n@test 'host-binaries - check extension is installed' {\n    run rdctl extension ls\n    assert_success\n    assert_output --partial \"rd/extension/host-binaries:latest\"\n}\n\n@test 'host-binaries - check files' {\n    assert_dir_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)\"\n    if is_windows; then\n        assert_file_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)/bin/dummy.exe\"\n        assert_file_not_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)/bin/dummy.sh\"\n    else\n        assert_file_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)/bin/dummy.sh\"\n        assert_file_not_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)/bin/dummy.exe\"\n    fi\n}\n\n@test 'host-binaries - upgrade' {\n    # We test upgrades with host-binaries as there was a bug about reinstalling\n    # an extension with host binaries.\n    ctrctl image tag \"$(id host-binaries)\" \"$(id host-binaries):0.0.1\"\n    ctrctl image tag \"$(id host-binaries)\" \"$(id host-binaries):v0.0.2\"\n\n    run rdctl extension ls\n    assert_success\n    assert_line --partial \"$(id host-binaries):latest\"\n\n    rdctl extension install \"$(id host-binaries)\"\n    run rdctl extension ls\n    assert_success\n    # The highest semver tag should be installed, replacing the existing one.\n    assert_line --partial \"$(id host-binaries):v0.0.2\"\n}\n\n@test 'host-binaries - uninstalling not installed version' {\n    rdctl extension uninstall \"$(id host-binaries):0.0.1\"\n    run rdctl extension ls\n    assert_success\n    # Trying to uninstall a version that isn't installed should be a no-op\n    assert_line --partial \"$(id host-binaries):v0.0.2\"\n}\n\n@test 'host-binaries - uninstall' {\n    ctrctl image tag \"$(id host-binaries)\" \"$(id host-binaries):0.0.3\"\n    # Uninstall should remove whatever version is installed, not the newest.\n\n    rdctl extension uninstall \"$(id host-binaries)\"\n\n    run rdctl extension ls\n    assert_success\n    assert_output \"No extensions are installed.\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id host-binaries)\"\n}\n\n@test 'ui - install' {\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id ui)\"\n    run rdctl extension install \"$(id ui)\"\n    assert_success\n}\n\n@test 'ui - check files' {\n    assert_file_exist \"$PATH_EXTENSIONS/$(encoded_id ui)/ui/dashboard-tab/ui/index.html\"\n}\n\n@test 'ui - uninstall' {\n    rdctl extension uninstall \"$(id ui)\"\n\n    run rdctl extension ls\n    assert_success\n    assert_output \"No extensions are installed.\"\n    assert_dir_not_exist \"$PATH_EXTENSIONS/$(encoded_id ui)\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/Dockerfile",
    "content": "FROM registry.suse.com/bci/golang:latest AS builder\nWORKDIR /usr/src/app\nCOPY bin/dummy.go .\nENV GOOS=windows\nRUN go build -o /dummy.exe -ldflags '-s -w' dummy.go\n\nFROM registry.suse.com/bci/golang:latest AS server-builder\nWORKDIR /usr/src/app\nCOPY bin/server.go .\nENV GOOS=linux\nRUN go build -o /server -ldflags '-s -w' server.go\n\nFROM registry.suse.com/bci/bci-minimal:16.0\nARG variant=basic\n\nADD ${variant}.json /metadata.json\nADD extension-icon.svg /extension-icon.svg\nADD ui /ui/\nADD bin /bin/\nCOPY --from=builder /dummy.exe /bin/\nCOPY --from=server-builder /server /bin/\nADD compose.yaml /compose/\nRUN ln -s does/not/exist /compose/dangling-link\nRUN ln -s compose.yaml /compose/link\n\nENTRYPOINT [\"/bin/server\"]\n"
  },
  {
    "path": "bats/tests/extensions/testdata/Makefile",
    "content": "all: \\\n  image-basic image-missing-icon image-ui \\\n  image-vm-image image-vm-compose image-host-binaries image-host-apis\n\nTOOL ?= docker\nNAMESPACE := $(if $(filter %nerdctl,${TOOL}),--namespace=rancher-desktop-extensions)\n\nNAMESPACE = $(if $(filter %nerdctl,${TOOL}),--namespace=rancher-desktop-extensions)\n\nimage-%:\n\t${TOOL} ${NAMESPACE} build -t rd/extension/$(@:image-%=%) --build-arg variant=$(@:image-%=%) .\n"
  },
  {
    "path": "bats/tests/extensions/testdata/README.md",
    "content": "This directory contains sample docker extensions.\n\n### basic\nA basic extension, containing the bare minimum (just an icon).\n\n### missing-icon\nAs above, but even the icon is missing.\n\n### missing-icon-file\nLike the basic extension, but the icon file specified does not exist.\n\n### ui\nPresents basic UI.  We do not yet test interaction with UI.\n\n### vm-image\nContains a docker image to run.\n\n### vm-compose\nContains a docker compose file to run. (Not supported.)\n\n### host-binaries\nContains binaries to be copied to the host.\n"
  },
  {
    "path": "bats/tests/extensions/testdata/basic.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/bin/dummy.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n)\n\nfunc main() {\n\tctx := context.Background()\n\tcmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr := cmd.Run()\n\tif exitError, ok := err.(*exec.ExitError); ok {\n\t\tif exitError.ExitCode() > -1 {\n\t\t\tos.Exit(exitError.ExitCode())\n\t\t}\n\t}\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/bin/dummy.sh",
    "content": "#!/bin/sh\n\nexec \"$@\"\n"
  },
  {
    "path": "bats/tests/extensions/testdata/bin/server.go",
    "content": "// command server listens on the Unix socket `/run/guest-services/hello.sock`\n// (see `everything.json`) to exercise the ability for the front end to talk to\n// the back end.\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"strconv\"\n\t\"syscall\"\n)\n\nconst (\n\taddr = \"/run/guest-services/hello.sock\"\n)\n\n// Listen on a port and return the listener\nfunc listen() (net.Listener, error) {\n\terr := os.Remove(addr)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tslog.Error(\"failed to remove old socket\", \"socket\", addr, \"error\", err)\n\t}\n\tlistener, err := net.Listen(\"unix\", addr)\n\tif err == nil {\n\t\treturn listener, nil\n\t}\n\tlistener, err = net.Listen(\"tcp\", \"\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to listen on fallback TCP: %w\", err)\n\t}\n\treturn listener, nil\n}\n\n// Handle HTTP POST requests\nfunc handlePost(w http.ResponseWriter, req *http.Request) {\n\tdata := map[string]any{\"headers\": req.Header}\n\tbody, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t_, _ = io.WriteString(w, fmt.Sprintf(\"failed to read body: %s\", err))\n\t\treturn\n\t}\n\tdata[\"body\"] = string(body)\n\tencoder := json.NewEncoder(w)\n\tencoder.SetIndent(\"\", \"  \")\n\tif err := encoder.Encode(data); err != nil {\n\t\t// This ends up after partially written JSON, but that's the best we can do\n\t\t// and should still show up in the result.\n\t\t_, _ = io.WriteString(w, fmt.Sprintf(\"failed to encode response: %w\", err))\n\t}\n}\n\n// Handle POST returning given status\nfunc handleWithStatus(w http.ResponseWriter, req *http.Request) {\n\tstatusText := req.PathValue(\"status\")\n\tstatusCode, err := strconv.Atoi(statusText)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t_, _ = fmt.Fprintf(w, \"failed to parse status %s\", statusText)\n\t\treturn\n\t}\n\tw.WriteHeader(statusCode)\n\t_, _ = fmt.Fprintf(w, \"returning status code %d\", statusCode)\n}\n\nfunc main() {\n\tlistener, err := listen()\n\tif err != nil {\n\t\tslog.Error(\"failed to listen\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n\thttp.DefaultServeMux.Handle(\"GET /get/\", http.StripPrefix(\"/get/\", http.FileServer(http.Dir(\"/\"))))\n\thttp.DefaultServeMux.HandleFunc(\"POST /post\", handlePost)\n\thttp.DefaultServeMux.HandleFunc(\"/status/{status}\", handleWithStatus)\n\n\tserver := &http.Server{}\n\tch := make(chan os.Signal)\n\terrCh := make(chan error)\n\tsignal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)\n\tgo func() {\n\t\t<-ch\n\t\terrCh <- server.Shutdown(context.Background())\n\t}()\n\n\tslog.Info(\"Serving HTTP\", \"address\", listener.Addr().String())\n\terr = server.Serve(listener)\n\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\tslog.Error(\"server closed\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n\tif err = <-errCh; err != nil {\n\t\tslog.Error(\"failed to shutdown server\", \"error\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/compose.yaml",
    "content": "name: sample-compose\nservices:\n  backend-service:\n    image: \"${DESKTOP_PLUGIN_IMAGE}\"\n"
  },
  {
    "path": "bats/tests/extensions/testdata/everything.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\",\n  \"host\": {\n    \"binaries\": [\n      {\n        \"darwin\": [\n          {\n            \"path\": \"/bin/dummy.sh\"\n          }\n        ],\n        \"windows\": [\n          {\n            \"path\": \"/bin/dummy.exe\"\n          }\n        ],\n        \"linux\": [\n          {\n            \"path\": \"/bin/dummy.sh\"\n          }\n        ]\n      }\n    ]\n  },\n  \"ui\": {\n    \"dashboard-tab\": {\n      \"title\": \"Sample Extension With Everything\",\n      \"root\": \"/ui\",\n      \"src\": \"index.html\",\n      \"backend\": {\n        \"socket\": \"hello.sock\"\n      }\n    }\n  },\n  \"vm\": {\n    \"composefile\": \"/compose/compose.yaml\",\n    \"exposes\": {\n      \"socket\": \"hello.sock\"\n    }\n  }\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/host-apis.json",
    "content": "{\n  \"info\": \"This is used to do manual testing of various host APIs\",\n  \"icon\": \"extension-icon.svg\",\n  \"ui\": {\n    \"dashboard-tab\": {\n      \"title\": \"RDX Host-APIs Test\",\n      \"root\": \"/ui\",\n      \"src\": \"host-apis.html\"\n    }\n  }\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/host-binaries.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\",\n  \"host\": {\n    \"binaries\": [\n      {\n        \"darwin\": [\n          {\n            \"path\": \"/bin/dummy.sh\"\n          }\n        ],\n        \"windows\": [\n          {\n            \"path\": \"/bin/dummy.exe\"\n          }\n        ],\n        \"linux\": [\n          {\n            \"path\": \"/bin/dummy.sh\"\n          }\n        ]\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/missing-icon-file.json",
    "content": "{\n  \"icon\": \"does-not-exist.svg\",\n  \"info\": \"This extension uses an icon that does not exist.\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/missing-icon.json",
    "content": "{\n  \"info\": \"This extension definition is missing an icon.\"\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/ui/host-apis.html",
    "content": "<!DOCTYPE html>\n<html>\n\n<head>\n  <script type=\"application/javascript\">\n\n    function openExternal() {\n      ddClient.host.openExternal(\"https://rancherdesktop.io\");\n    }\n\n    function openDialog() {\n      const properties=Array.from(document.getElementById('dialogOptions').selectedOptions).map(v => v.value);\n      const options=properties.length>0? {properties}:undefined;\n      ddClient.desktopUI.dialog.showOpenDialog(options).then(result => {\n        console.log('openDialog result:',result);\n        document.getElementById('dialogCancelled').checked=result.canceled;\n        document.getElementById('dialogFilePaths').innerHTML=\n          result.filePaths.map(p => `<option>${p}</option>`).join('');\n        document.getElementById('dialogBookmarks').innerHTML=\n          (result.bookmarks??[]).map(p => `<option>${p}</option>`).join('');\n      });\n    }\n\n    /**\n     * Open a toast notification\n     * @param type ['success' | 'warning' | 'error'] The type of toast to invoke.\n     */\n    function toast(type) {\n      ddClient.desktopUI.toast[type](`This is a ${type} toast`);\n    }\n\n    let platform='unknown';\n    if(/^mac/i.test(navigator.platform)) {\n      platform='darwin';\n    } else if(/^win/i.test(navigator.platform)) {\n      platform='win32';\n    } else if(/^linunx/i.test(navigator.platform)) {\n      platform='linux';\n    }\n    document.documentElement.setAttribute('platform',platform);\n  </script>\n\n  <style>\n    #dialog {\n      display: flex;\n    }\n\n    #dialog>* {\n      flex: 1;\n    }\n\n    :root:not([platform=\"darwin\"]) #dialogOptions>option[darwin],\n    :root:not([platform=\"win32\"]) #dialogOptions>option[win32],\n    :root:not([platform=\"linux\"]) #dialogOptions>option[linux] {\n      display: none;\n    }\n\n    #dialog table {\n      border: 1px solid;\n    }\n\n    #dialog table td,\n    #dialog table td>select {\n      width: 100%;\n    }\n\n  </style>\n</head>\n\n<body>\n  <h1>Rancher Desktop Extensions Host API Testing</h1>\n\n  <h2>Open external</h2>\n  <p>Clicking the button should open the Rancher Desktop home page in a browser.</p>\n  <button onclick=\"openExternal()\">Open in web page</button>\n  <hr>\n\n  <h2>Select file</h2>\n  <section id=\"dialog\">\n    <div>\n      <p>Clicking on the button should show a file open dialog.</p>\n      <select id=\"dialogOptions\" multiple>\n        <option value=\"openFile\">Open File</option>\n        <option value=\"openDirectory\">Open Directory</option>\n        <option value=\"multiSelections\">Multiple Selection</option>\n        <option value=\"showHiddenFiles\">Show Hidden Files</option>\n        <option value=\"createDirectory\" darwin>Create Directory</option>\n        <option value=\"promptToCreate\" win32>Prompt to Create</option>\n        <option value=\"noResolveAliases\" darwin>No Resolve Symlinks</option>\n        <option value=\"treatPackageAsDirectory\" darwin>Treat Package as Directory</option>\n        <option value=\"dontAddToRecent\" win32>Don't Add to Recent</option>\n      </select>\n      <button onclick=\"openDialog()\">Choose File</button>\n    </div>\n    <div>\n      <table>\n        <th>\n        <td colspan=\"2\">Dialog Output</td>\n        </th>\n        <tr>\n          <th>cancelled</th>\n          <td><input type=\"checkbox\" id=\"dialogCancelled\"></td>\n        </tr>\n        <tr>\n          <th>file paths</th>\n          <td><select multiple id=\"dialogFilePaths\"></select></td>\n        </tr>\n      </table>\n    </div>\n  </section>\n  <hr>\n\n  <h2>Toast</h2>\n  <p>\n    Clicking on each of the buttons should pop up a notification where the\n    notification title matches the type clicked on.\n  </p>\n  <button onclick=\"toast('success')\">Success</button>\n  <button onclick=\"toast('warning')\">Warning</button>\n  <button onclick=\"toast('error')\">Error</button>\n</body>\n\n</html>\n"
  },
  {
    "path": "bats/tests/extensions/testdata/ui/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Test Extension</title>\n  </head>\n  <body>\n    <h1>Test Extension</h1>\n    <p>This is a test extension.</p>\n    <script>\n      console.log(ddClient);\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "bats/tests/extensions/testdata/ui.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\",\n  \"ui\": {\n    \"dashboard-tab\": {\n      \"title\": \"Sample Extension\",\n      \"root\": \"/ui\",\n      \"src\": \"index.html\"\n    }\n  }\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/vm-compose.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\",\n  \"info\": \"This image uses vm.composefile\",\n  \"vm\": {\n    \"composefile\": \"/compose/compose.yaml\"\n  }\n}\n"
  },
  {
    "path": "bats/tests/extensions/testdata/vm-image.json",
    "content": "{\n  \"icon\": \"extension-icon.svg\",\n  \"info\": \"This extension contains a reference to an image.\",\n  \"vm\": {\n    \"image\": \"rd/extension/vm-image\"\n  }\n}\n"
  },
  {
    "path": "bats/tests/helpers/commands.bash",
    "content": "EXE=\"\"\nPLATFORM=$OS\nif is_windows; then\n    PLATFORM=linux\n    if using_windows_exe; then\n        EXE=\".exe\"\n        PLATFORM=win32\n    fi\nfi\n\nif using_containerd; then\n    CONTAINER_ENGINE_SERVICE=containerd\nelse\n    CONTAINER_ENGINE_SERVICE=docker\nfi\n\nif is_macos; then\n    CRED_HELPER=\"$PATH_RESOURCES/$PLATFORM/bin/docker-credential-osxkeychain\"\nelif is_linux; then\n    if command -v pass; then\n        CRED_HELPER=\"$PATH_RESOURCES/$PLATFORM/bin/docker-credential-pass\"\n    else\n        CRED_HELPER=\"$PATH_RESOURCES/$PLATFORM/bin/docker-credential-secretservice\"\n    fi\nelif is_windows; then\n    # Our docker-cli for WSL defaults to \"wincred.exe\" as well\n    CRED_HELPER=\"$PATH_RESOURCES/win32/bin/docker-credential-wincred.exe\"\nfi\n\nif is_windows; then\n    RD_DOCKER_CONTEXT=default\nelse\n    RD_DOCKER_CONTEXT=rancher-desktop\nfi\n\nCONTAINERD_NAMESPACE=default\nWSL_DISTRO=rancher-desktop\n\nno_cr() {\n    tr -d '\\r'\n}\nctrctl() {\n    if using_docker; then\n        docker \"$@\"\n    else\n        nerdctl \"$@\"\n    fi\n}\ncurl() {\n    command \"curl$EXE\" \"$@\"\n}\ndocker() {\n    docker_exe --context $RD_DOCKER_CONTEXT \"$@\"\n}\ndocker_exe() {\n    # Add path to bundled credential helpers to the front of the PATH; also\n    # ensure that on Windows, it gets exported.\n    PATH=\"$PATH_RESOURCES/$PLATFORM/bin:$PATH\" WSLENV=\"PATH/l:${WSLENV:-}\" \\\n        \"$PATH_RESOURCES/$PLATFORM/bin/docker$EXE\" \"$@\" | no_cr\n}\nhelm() {\n    # Add path to bundled credential helpers to the front of the PATH; also\n    # ensure that on Windows, it gets exported.\n    PATH=\"$PATH_RESOURCES/$PLATFORM/bin:$PATH\" WSLENV=\"PATH/l:${WSLENV:-}\" \\\n        \"$PATH_RESOURCES/$PLATFORM/bin/helm$EXE\" --kube-context rancher-desktop \"$@\" | no_cr\n}\nkubectl() {\n    kubectl_exe --context rancher-desktop \"$@\"\n}\nkubectl_exe() {\n    \"$PATH_RESOURCES/$PLATFORM/bin/kubectl$EXE\" \"$@\" | no_cr\n}\nlimactl() {\n    # LIMA_HOME is set by paths.bash but not exported\n    LIMA_HOME=\"$LIMA_HOME\" \"$PATH_RESOURCES/$PLATFORM/lima/bin/limactl\" \"$@\"\n}\nnerdctl() {\n    # Add path to bundled credential helpers to the front of the PATH; also\n    # ensure that on Windows, it gets exported.\n    PATH=\"$PATH_RESOURCES/$PLATFORM/bin:$PATH\" WSLENV=\"PATH/l:${WSLENV:-}\" \\\n        \"$PATH_RESOURCES/$PLATFORM/bin/nerdctl$EXE\" --namespace \"$CONTAINERD_NAMESPACE\" \"$@\" | no_cr\n}\n# Run `rdctl`; if $RD_TIMEOUT is set, the value is used as the first argument to\n# the `timeout` command.\nrdctl() {\n    if is_windows; then\n        timeout \"${RD_TIMEOUT:-0}\" \"$PATH_RESOURCES/win32/bin/rdctl.exe\" \"$@\" | no_cr\n    else\n        timeout \"${RD_TIMEOUT:-0}\" \"$PATH_RESOURCES/$PLATFORM/bin/rdctl$EXE\" \"$@\"\n    fi\n}\nrdshell() {\n    rdctl shell \"$@\"\n}\nrdsudo() {\n    rdshell sudo \"$@\"\n}\nspin() {\n    # spin may call itself recursively, so make sure it calls the correct binary\n    PATH=\"$PATH_RESOURCES/$PLATFORM/bin:$PATH\" \"$PATH_RESOURCES/$PLATFORM/bin/spin$EXE\" \"$@\" | no_cr\n}\nwsl() {\n    wsl.exe -d \"$WSL_DISTRO\" \"$@\"\n}\n"
  },
  {
    "path": "bats/tests/helpers/defaults.bash",
    "content": "########################################################################\n: \"${RD_CONTAINER_ENGINE:=containerd}\"\n\nvalidate_enum RD_CONTAINER_ENGINE containerd moby\n\nusing_containerd() {\n    test \"$RD_CONTAINER_ENGINE\" = \"containerd\"\n}\n\nusing_docker() {\n    ! using_containerd\n}\n\n########################################################################\n: \"${RD_RANCHER_IMAGE_TAG:=}\"\n\nrancher_image_tag() {\n    echo \"${RANCHER_IMAGE_TAG:-v2.7.0}\"\n}\n\n########################################################################\n# Defaults to true, except in the helper unit tests, which default to false\n: \"${RD_INFO:=}\"\n\n########################################################################\n: \"${RD_CAPTURE_LOGS:=false}\"\n\ncapturing_logs() {\n    is_true \"$RD_CAPTURE_LOGS\"\n}\n\n########################################################################\n: \"${RD_NO_MODAL_DIALOGS:=true}\"\n\nsuppressing_modal_dialogs() {\n    is_true \"$RD_NO_MODAL_DIALOGS\"\n}\n\n########################################################################\n: \"${RD_TAKE_SCREENSHOTS:=false}\"\n\ntaking_screenshots() {\n    is_true \"$RD_TAKE_SCREENSHOTS\"\n}\n\n########################################################################\n: \"${RD_TRACE:=false}\"\n\n########################################################################\n# When RD_USE_GHCR_IMAGES is true, then all images will be pulled from\n# ghcr.io instead of docker.io, to avoid hitting the docker hub pull\n# rate limit.\n\n: \"${RD_USE_GHCR_IMAGES:=false}\"\n\nusing_ghcr_images() {\n    is_true \"$RD_USE_GHCR_IMAGES\"\n}\n\n########################################################################\n: \"${RD_DELETE_PROFILES:=true}\"\n\ndeleting_profiles() {\n    is_true \"$RD_DELETE_PROFILES\"\n}\n\n########################################################################\n: \"${RD_USE_IMAGE_ALLOW_LIST:=false}\"\n\nusing_image_allow_list() {\n    is_true \"$RD_USE_IMAGE_ALLOW_LIST\"\n}\n\n########################################################################\n# RD_USE_PROFILE is for internal use. It uses a profile instead of\n# settings.json to set initial values for WSL integrations and allowed\n# images list because when settings.json exists the default profile is\n# ignored.\n\n: \"${RD_USE_PROFILE:=false}\"\n\n########################################################################\n# RD_TIMEOUT is for internal use. It is used to configure timeouts for\n# the `rdctl` command, and should not be set outside of specific\n# commands.\n: \"${RD_TIMEOUT:=}\"\n\nif [[ -n $RD_TIMEOUT ]]; then\n    fatal \"RD_TIMEOUT should not be set\"\nfi\n\n########################################################################\n: \"${RD_USE_VZ_EMULATION:=$(bool is_macos)}\"\n\nusing_vz_emulation() {\n    is_true \"$RD_USE_VZ_EMULATION\"\n}\n\nif using_vz_emulation && ! supports_vz_emulation; then\n    fatal \"RD_USE_VZ_EMULATION is not supported on this OS or OS version\"\nfi\n\n########################################################################\n: \"${RD_USE_WINDOWS_EXE:=$(bool is_windows)}\"\n\nusing_windows_exe() {\n    is_true \"$RD_USE_WINDOWS_EXE\"\n}\n\nif using_windows_exe && ! is_windows; then\n    fatal \"RD_USE_WINDOWS_EXE only works on Windows\"\nfi\n\n########################################################################\nif is_unix; then\n    : \"${RD_MOUNT_TYPE:=reverse-sshfs}\"\n\n    validate_enum RD_MOUNT_TYPE reverse-sshfs 9p virtiofs\n\n    if [ \"$RD_MOUNT_TYPE\" = \"virtiofs\" ] && ! using_vz_emulation; then\n        fatal \"RD_MOUNT_TYPE=virtiofs only works with VZ emulation\"\n    fi\n    if [ \"$RD_MOUNT_TYPE\" = \"9p\" ] && using_vz_emulation; then\n        fatal \"RD_MOUNT_TYPE=9p only works with qemu emulation\"\n    fi\nelse\n    : \"${RD_MOUNT_TYPE:=}\"\n    if [ -n \"${RD_MOUNT_TYPE:-}\" ]; then\n        fatal \"RD_MOUNT_TYPE only works on Linux and macOS\"\n    fi\nfi\n\n########################################################################\n: \"${RD_9P_CACHE_MODE:=mmap}\"\n\nvalidate_enum RD_9P_CACHE_MODE none loose fscache mmap\n\n########################################################################\n: \"${RD_9P_MSIZE:=128}\"\n\n########################################################################\n: \"${RD_9P_PROTOCOL_VERSION:=9p2000.L}\"\n\nvalidate_enum RD_9P_PROTOCOL_VERSION 9p2000 9p2000.u 9p2000.L\n\n########################################################################\n: \"${RD_9P_SECURITY_MODEL:=none}\"\n\nvalidate_enum RD_9P_SECURITY_MODEL passthrough mapped-xattr mapped-file none\n\n########################################################################\n# When RD_USE_RAMDISK is true, we will try to set up a temporary ramdisk\n# for the application profile to make things run faster.  This is not\n# supported on all platforms, but is a no-op on unsupported platforms.\n# Some test files may override this due to interactions with factory reset.\n: \"${RD_USE_RAMDISK:=false}\"\n# Size of the ramdisk, in gigabytes.  If a test requires more space than given,\n# then ramdisk will be disabled for that test.\n: \"${RD_RAMDISK_SIZE:=12}\"\nusing_ramdisk() {\n    is_true \"${RD_USE_RAMDISK}\"\n}\n\n########################################################################\n# Use RD_PROTECTED_DOT in profile settings for WSL distro names.\n: \"${RD_PROTECTED_DOT:=·}\"\n\n########################################################################\n# RD_KUBELET_TIMEOUT specifies the number of minutes wait_for_kubelet()\n# waits before it times out.\n: \"${RD_KUBELET_TIMEOUT:=10}\"\n\n########################################################################\n# RD_LOCATION specifies the location where Rancher Desktop is installed\n#   system: default system-wide install location shared for all users\n#   user:   per-user install location\n#   dist:   use the result of `yarn package` in ../dist\n#   dev:    dev mode; start app with `cd ..; yarn dev`\n#   \"\":     use first location from the list above that contains the app\n\n: \"${RD_LOCATION:=}\"\n\nvalidate_enum RD_LOCATION system user dist dev \"\"\n\nusing_dev_mode() {\n    [ \"$RD_LOCATION\" = \"dev\" ]\n}\n\n########################################################################\n# Kubernetes versions\n\n# The main Kubernetes version to test.\n: \"${RD_KUBERNETES_VERSION:=1.32.7}\"\n\n# A secondary Kubernetes version; this is used for testing upgrades.\n: \"${RD_KUBERNETES_ALT_VERSION:=1.31.3}\"\n\n# RD_K3S_VERSIONS specifies a list of k3s versions. foreach_k3s_version()\n# can dynamically register a test to run once for each version in the\n# list. Only versions between RD_K3S_MIN and RD_K3S_MAX (inclusively)\n# will be used.\n#\n# Special values:\n# \"all\" will fetch the list of all k3s releases from GitHub\n# \"latest\" will fetch the list of latest versions from the release channel\n\n: \"${RD_K3S_MIN:=1.25.3}\"\n: \"${RD_K3S_MAX:=1.99.0}\"\n: \"${RD_K3S_VERSIONS:=$RD_KUBERNETES_VERSION}\"\n\nvalidate_semver RD_K3S_MIN\nvalidate_semver RD_K3S_MAX\n\n# Cache expansion of RD_K3S_VERSIONS special versions because they are slow to compute\nif ! load_var RD_K3S_VERSIONS; then\n    # Fetch \"all\" or \"latest\" versions\n    get_k3s_versions\n\n    for k3s_version in ${RD_K3S_VERSIONS}; do\n        validate_semver k3s_version\n    done\n\n    save_var RD_K3S_VERSIONS\nfi\n\n########################################################################\n# RD_VPN_TEST_IMAGE specifies the URL used by the split DNS test to access\n# the private registry. Defaults to empty. Can be set via environment\n# variable when running tests.\n\n: \"${RD_VPN_TEST_IMAGE:=}\"\n\nusing_vpn_test_image() {\n    [[ -n $RD_VPN_TEST_IMAGE ]]\n}\n"
  },
  {
    "path": "bats/tests/helpers/images.bash",
    "content": "# These images have been mirrored to ghcr.io (using bats/scripts/ghcr-mirror.sh)\n# to avoid hitting Docker Hub pull limits during testing.\n\n# TODO TODO TODO\n# The python image is huge (10GB across all platforms). We should either pin the\n# tag, or replace it with a different image for testing, so we don't have to mirror\n# the images to ghcr.io every time we run the mirror script.\n# TODO TODO TODO\n\n# Any time you add an image here you need to re-run the mirror script!\nIMAGES=(alpine busybox nginx python python:3.9-slim ruby tonistiigi/binfmt registry:2.8.1)\n\nGHCR_REPO=ghcr.io/rancher-sandbox/bats\n\n# Create IMAGE_FOO_BAR_TAG=foo/bar:tag variables\nfor IMAGE in \"${IMAGES[@]}\"; do\n    VAR=\"IMAGE_$(echo \"$IMAGE\" | tr '[:lower:]' '[:upper:]' | tr -C '[:alnum:][:space:]' _)\"\n    # file may be loaded outside BATS environment\n    if [ \"$(type -t using_ghcr_images)\" = \"function\" ] && using_ghcr_images; then\n        eval \"$VAR=$GHCR_REPO/$IMAGE\"\n    else\n        eval \"$VAR=$IMAGE\"\n    fi\ndone\n\n# shellcheck disable=2034 # The registry image doesn't really need the tag\nIMAGE_REGISTRY=$IMAGE_REGISTRY_2_8_1\n"
  },
  {
    "path": "bats/tests/helpers/info.bash",
    "content": "# shellcheck disable=SC2059\n# https://www.shellcheck.net/wiki/SC2059 -- Don't use variables in the printf format string. Use printf '..%s..' \"$foo\".\n# This file exists to print information about the configuration.\n\nshow_info() { # @test\n    # In case the file is loaded as a test: bats tests/helpers/info.bash\n    if [ -z \"$RD_HELPERS_LOADED\" ]; then\n        load load.bash\n    fi\n\n    if capturing_logs || taking_screenshots; then\n        rm -rf \"$PATH_BATS_LOGS\"\n    fi\n\n    if is_false \"${RD_INFO:-true}\"; then\n        return\n    fi\n\n    (\n        local format=\"# %s | %s\\n\"\n\n        printf \"$format\" \"Install location:\" \"$RD_LOCATION\"\n        printf \"$format\" \"Resources path:\" \"$PATH_RESOURCES\"\n        echo \"#\"\n        printf \"$format\" \"Container engine:\" \"$RD_CONTAINER_ENGINE\"\n        printf \"$format\" \"Kubernetes version:\" \"$RD_KUBERNETES_VERSION ($RD_K3S_VERSIONS)\"\n        printf \"$format\" \"Mount type:\" \"$RD_MOUNT_TYPE\"\n        if [ \"$RD_MOUNT_TYPE\" = \"9p\" ]; then\n            printf \"$format\" \"  9p cache mode:\" \"$RD_9P_CACHE_MODE\"\n            printf \"$format\" \"  9p msize:\" \"$RD_9P_MSIZE\"\n            printf \"$format\" \"  9p protocol version:\" \"$RD_9P_PROTOCOL_VERSION\"\n            printf \"$format\" \"  9p security model:\" \"$RD_9P_SECURITY_MODEL\"\n        fi\n        printf \"$format\" \"Using image allow list:\" \"$(bool using_image_allow_list)\"\n        if is_macos; then\n            printf \"$format\" \"Using VZ emulation:\" \"$(bool using_vz_emulation)\"\n            printf \"$format\" \"Using ramdisk:\" \"$(bool using_ramdisk)\"\n        fi\n        if is_windows; then\n            printf \"$format\" \"Using Windows executables:\" \"$(bool using_windows_exe)\"\n        fi\n        echo \"#\"\n        printf \"$format\" \"Capturing logs:\" \"$(bool capturing_logs)\"\n        printf \"$format\" \"Tracing execution:\" \"$(bool is_true \"$RD_TRACE\")\"\n        printf \"$format\" \"Taking screenshots:\" \"$(bool taking_screenshots)\"\n        printf \"$format\" \"Using ghcr.io images:\" \"$(bool using_ghcr_images)\"\n    ) | column -t -s '|' >&3\n}\n"
  },
  {
    "path": "bats/tests/helpers/kubernetes.bash",
    "content": "wait_for_kubelet() {\n    local desired_version=${1:-$RD_KUBERNETES_VERSION}\n    local timeout=$(($(date +%s) + RD_KUBELET_TIMEOUT * 60))\n    trace \"waiting for Kubernetes ${desired_version} to be available\"\n    while true; do\n        sleep 1\n        assert [ \"$(date +%s)\" -lt \"$timeout\" ]\n        if ! kubectl get --raw /readyz &>/dev/null; then\n            continue\n        fi\n\n        # Check that kubelet is Ready\n        run kubectl get node -o jsonpath=\"{.items[0].status.conditions[?(@.type=='Ready')].status}\"\n        if ((status != 0)) || [[ $output != \"True\" ]]; then\n            continue\n        fi\n\n        # Make sure the \"default\" serviceaccount exists\n        if ! kubectl get --namespace default serviceaccount default &>/dev/null; then\n            continue\n        fi\n\n        # Get kubelet version\n        run kubectl get node -o jsonpath=\"{.items[0].status.nodeInfo.kubeletVersion}\"\n        if ((status != 0)); then\n            continue\n        fi\n\n        # Turn \"v1.23.4+k3s1\" into \"1.23.4\"\n        local version=${output#v}\n        version=${version%+*}\n        if [ \"$version\" == \"$desired_version\" ]; then\n            return 0\n        fi\n    done\n}\n\n# unwrap_kube_list removes the \"List\" wrapper from the JSON in $output if .kind is \"List\".\n# Returns an error if the number of .items in the List isn't exactly 1.\nunwrap_kube_list() {\n    local json=$output\n\n    run jq_output '.kind'\n    assert_success\n    if [[ $output == \"List\" ]]; then\n        run jq --raw-output '.items | length' <<<\"$json\"\n        assert_success\n        assert_output \"1\"\n\n        run jq --raw-output '.items[0]' <<<\"$json\"\n        assert_success\n        json=$output\n    fi\n    echo \"$json\"\n}\n\nassert_kube_deployment_available() {\n    local jsonpath=\"jsonpath={.status.conditions[?(@.type=='Available')].status}\"\n    run --separate-stderr kubectl get deployment \"$@\" --output \"$jsonpath\"\n    assert_success\n    assert_output \"True\"\n}\n\nwait_for_kube_deployment_available() {\n    trace \"waiting for deployment $*\"\n    try assert_kube_deployment_available \"$@\"\n}\n\nassert_pod_containers_are_running() {\n    run kubectl get pod \"$@\" --output json\n    assert_success\n\n    # Make sure the query returned just a single pod\n    run unwrap_kube_list\n    assert_success\n\n    # Confirm that **all** containers of the pod are in \"running\" state\n    run jq_output '[.status.containerStatuses[].state | keys] | add | unique | .[]'\n    assert_success\n    assert_output \"running\"\n}\n\ntraefik_ip() {\n    local jsonpath='jsonpath={.status.loadBalancer.ingress[0].ip}'\n    run --separate-stderr kubectl get service traefik --namespace kube-system --output \"$jsonpath\"\n    assert_success\n    assert_output\n    echo \"$output\"\n}\n\ntraefik_hostname() {\n    if is_windows; then\n        # BUG BUG BUG\n        # Currently the service ip address is not routable from the host\n        # https://github.com/rancher-sandbox/rancher-desktop/issues/6934\n        # BUG BUG BUG\n\n        # local ip\n        # ip=$(traefik_ip)\n        # echo \"${ip}.sslip.io\"\n\n        # caller must have called `skip_unless_host_ip`\n        output=$HOST_IP assert_output\n        echo \"${HOST_IP}.sslip.io\"\n    else\n        echo \"localhost\"\n    fi\n}\n\nwait_for_traefik() {\n    try traefik_ip\n}\n\nget_k3s_versions() {\n    if [[ $RD_K3S_VERSIONS == \"all\" ]]; then\n        # filter out duplicates; RD only supports the latest of +k3s1, +k3s2, etc.\n        RD_K3S_VERSIONS=$(\n            gh api /repos/k3s-io/k3s/releases --paginate --jq '.[].tag_name' |\n                grep -E '^v1\\.[0-9]+\\.[0-9]+\\+k3s[0-9]+$' |\n                sed -E 's/v([^+]+)\\+.*/\\1/' |\n                sort --unique --version-sort\n        )\n    fi\n\n    if [[ $RD_K3S_VERSIONS == \"latest\" ]]; then\n        RD_K3S_VERSIONS=$(\n            curl --silent --fail https://update.k3s.io/v1-release/channels |\n                jq --raw-output '.data[] | select(.name | test(\"^v[0-9]+\\\\.[0-9]+$\")).latest' |\n                sed -E 's/v([^+]+)\\+.*/\\1/'\n        )\n    fi\n}\n"
  },
  {
    "path": "bats/tests/helpers/kubernetes.bats",
    "content": "load '../helpers/load'\n\n: \"${RD_INFO:=false}\"\n\n@test 'unwrap_kube_list: no list' {\n    run echo '{\"kind\": \"Pod\"}'\n    assert_success\n\n    run unwrap_kube_list\n    assert_success\n\n    run jq_output .kind\n    assert_success\n    assert_output Pod\n}\n\n@test 'unwrap_kube_list: no items' {\n    run echo '{\"kind\": \"List\"}'\n    assert_success\n\n    run unwrap_kube_list\n    assert_failure\n}\n\n@test 'unwrap_kube_list: one item' {\n    run echo '{\"kind\": \"List\", \"items\": [{\"kind\": \"Pod\"}]}'\n    assert_success\n\n    run unwrap_kube_list\n    assert_success\n\n    run jq_output .kind\n    assert_success\n    assert_output Pod\n}\n\n@test 'unwrap_kube_list: two items' {\n    run echo '{\"kind\": \"List\", \"items\": [{\"kind\": \"Pod\"},{\"kind\": \"Pod\"}]}'\n    assert_success\n\n    run unwrap_kube_list\n    assert_failure\n}\n\n@test 'unwrap_kube_list: not JSON' {\n    run echo 'Some random error message'\n    assert_success\n\n    run unwrap_kube_list\n    assert_failure\n}\n"
  },
  {
    "path": "bats/tests/helpers/load.bash",
    "content": "set -o errexit -o nounset -o pipefail\n\n# Make sure run() will execute all functions with errexit enabled\nexport BATS_RUN_ERREXIT=1\n\n# RD_HELPERS_LOADED is set when bats/helpers/load.bash has been loaded\nRD_HELPERS_LOADED=1\n\nabsolute_path() {\n    (\n        cd \"$1\"\n        pwd\n    )\n}\n\nPATH_BATS_HELPERS=$(absolute_path \"$(dirname \"${BASH_SOURCE[0]}\")\")\nPATH_BATS_ROOT=$(absolute_path \"$PATH_BATS_HELPERS/../..\")\nPATH_BATS_LOGS=$PATH_BATS_ROOT/logs\n\n# RD_TEST_FILENAME is relative to tests/ and strips the .bats extension,\n# e.g. \"registry/creds\" for \".../bats/tests/registry/creds.bats\"\nRD_TEST_FILENAME=${BATS_TEST_FILENAME#\"$PATH_BATS_ROOT/tests/\"}\nRD_TEST_FILENAME=${RD_TEST_FILENAME%.bats}\n\n# Use fatal() to abort loading helpers; don't run any tests\nfatal() {\n    local fd=2\n    # fd 3 might not be open if we're not fully under bats yet; detect that.\n    [[ -e /dev/fd/3 ]] && fd=3\n    echo \"   $1\" >&$fd\n\n    # Print (ugly) stack trace if we are outside any @test function\n    if [ -z \"${BATS_SUITE_TEST_NUMBER:-}\" ]; then\n        echo >&$fd\n        local frame=0\n        while caller $frame >&$fd; do\n            ((frame++))\n        done\n    fi\n    exit 1\n}\n\nsource \"$PATH_BATS_ROOT/bats-support/load.bash\"\nsource \"$PATH_BATS_ROOT/bats-assert/load.bash\"\nsource \"$PATH_BATS_ROOT/bats-file/load.bash\"\n\nsource \"$PATH_BATS_HELPERS/os.bash\"\nsource \"$PATH_BATS_HELPERS/utils.bash\"\nsource \"$PATH_BATS_HELPERS/snapshots.bash\"\n\n# kubernetes.bash has no load-time dependencies\nsource \"$PATH_BATS_HELPERS/kubernetes.bash\"\n\n# defaults.bash uses is_windows() from os.bash and\n# validate_enum() and is_true() from utils.bash.\n# get_k3s_versions from kubernetes.bash.\nsource \"$PATH_BATS_HELPERS/defaults.bash\"\n\n# images.bash uses using_ghcr_images() from defaults.bash\nsource \"$PATH_BATS_HELPERS/images.bash\"\n\n# paths.bash uses RD_LOCATION from defaults.bash\nsource \"$PATH_BATS_HELPERS/paths.bash\"\n\n# commands.bash uses is_containerd() from defaults.bash,\n# is_windows() etc from os.bash,\n# and PATH_* variables from paths.bash\nsource \"$PATH_BATS_HELPERS/commands.bash\"\n\n# profile.bash uses is_xxx() from os.bash\nsource \"$PATH_BATS_HELPERS/profile.bash\"\n\n# vm.bash uses various PATH_* variables from paths.bash,\n# rdctl from commands.bash, and jq_output from utils.bash\nsource \"$PATH_BATS_HELPERS/vm.bash\"\n\n# Add BATS helper executables to $PATH.  On Windows, we use the Linux version\n# from WSL.\nexport PATH=\"$PATH_BATS_ROOT/bin/${OS/windows/linux}:$PATH\"\n\n# If called from foo() this function will call local_foo() if it exist.\ncall_local_function() {\n    local func\n    func=\"local_$(calling_function)\"\n    if [ \"$(type -t \"$func\")\" = \"function\" ]; then\n        \"$func\"\n    fi\n}\n\nsetup_file() {\n    # We require bash 4; bash 3.2 (as shipped by macOS) seems to have\n    # compatibility issues.\n    if semver_gt 4.0.0 \"$(semver \"$BASH_VERSION\")\"; then\n        fail \"Bash 4.0.0 is required; you have $BASH_VERSION\"\n    fi\n    # We currently use a submodule that provides BATS 1.10; we do not test\n    # against any other copy of BATS (and therefore only support the version in\n    # that submodule).\n    bats_require_minimum_version 1.10.0\n    # Ideally this should be printed only when using the tap formatter,\n    # but I don't see a way to check for this.\n    echo \"# ===== $RD_TEST_FILENAME =====\" >&3\n\n    # local_setup_file may override RD_USE_RAMDISK\n    call_local_function\n\n    setup_ramdisk\n}\n\nteardown_file() {\n    capture_logs\n\n    local shutdown=false\n    if is_linux || is_windows; then\n        # On Linux & Windows if we don't shutdown Rancher Desktop bats tests don't terminate.\n        shutdown=true\n    elif using_dev_mode; then\n        # In dev mode, we also need to shut down.\n        shutdown=true\n    elif using_ramdisk; then\n        # When using a ramdisk, we need to shut down to clean up.\n        shutdown=true\n    fi\n    if is_true $shutdown; then\n        rdctl shutdown || :\n    fi\n\n    teardown_ramdisk\n\n    call_local_function\n}\n\nsetup() {\n    if [ \"${BATS_SUITE_TEST_NUMBER}\" -eq 1 ] && [ \"$RD_TEST_FILENAME\" != \"helpers/info.bash\" ]; then\n        source \"$PATH_BATS_HELPERS/info.bash\"\n        show_info\n        echo \"#\"\n    fi\n\n    call_local_function\n}\n\nteardown() {\n    if [ -z \"$BATS_TEST_SKIPPED\" ] && [ -z \"$BATS_TEST_COMPLETED\" ]; then\n        capture_logs\n        take_screenshot\n    fi\n\n    call_local_function\n}\n"
  },
  {
    "path": "bats/tests/helpers/os.bash",
    "content": "# https://www.shellcheck.net/wiki/SC2120 -- disabled due to complaining about not referencing arguments that are optional on functions is_platformName\n# shellcheck disable=SC2120\nUNAME=$(uname)\nARCH=$(uname -m)\nARCH=${ARCH/arm64/aarch64}\n\ncase $UNAME in\nDarwin)\n    # OS matches the directory name of the PATH_RESOURCES directory,\n    # so uses \"darwin\" and not \"macos\".\n    OS=darwin\n    ;;\nLinux)\n    if [[ $(uname -a) =~ microsoft ]]; then\n        OS=windows\n    else\n        OS=linux\n    fi\n    ;;\n*)\n    echo \"Unexpected uname: $UNAME\" >&2\n    exit 1\n    ;;\nesac\n\nis_linux() {\n    if [ -z \"${1:-}\" ]; then\n        test \"$OS\" = linux\n    else\n        test \"$OS\" = linux -a \"$ARCH\" = \"$1\"\n    fi\n}\n\nis_macos() {\n    if [ -z \"${1:-}\" ]; then\n        test \"$OS\" = darwin\n    else\n        test \"$OS\" = darwin -a \"$ARCH\" = \"$1\"\n    fi\n}\n\nis_windows() {\n    if [ -z \"${1:-}\" ]; then\n        test \"$OS\" = windows\n    else\n        test \"$OS\" = windows -a \"$ARCH\" = \"$1\"\n    fi\n}\n\nis_unix() {\n    ! is_windows \"$@\"\n}\n\nskip_on_windows() {\n    if is_windows; then\n        skip \"${1:-This test is not applicable on Windows.}\"\n    fi\n}\n\nskip_on_unix() {\n    if is_unix; then\n        skip \"${1:-This test is not applicable on macOS/Linux.}\"\n    fi\n}\n\nneeds_port() {\n    local port=$1\n    if is_linux; then\n        if [ \"$(cat /proc/sys/net/ipv4/ip_unprivileged_port_start)\" -gt \"$port\" ]; then\n            # Run sudo non-interactive, so don't prompt for password\n            run sudo -n sh -c \"echo $port > /proc/sys/net/ipv4/ip_unprivileged_port_start\"\n            if ((status > 0)); then\n                skip \"net.ipv4.ip_unprivileged_port_start must be $port or less\"\n            fi\n        fi\n    fi\n}\n\nsudo_needs_password() {\n    # Check if we can run /usr/bin/true (or /bin/true) without requiring a password\n    run sudo --non-interactive --reset-timestamp true\n    ((status != 0))\n}\n\nsupports_vz_emulation() {\n    if ! is_macos; then\n        return 1\n    fi\n    [[ -n ${_RD_SUPPORTS_VZ_EMULATION:-} ]] || load_var _RD_SUPPORTS_VZ_EMULATION || true\n    if [[ -z ${_RD_SUPPORTS_VZ_EMULATION:-} ]]; then\n        local version\n        version=$(semver \"$(/usr/bin/sw_vers -productVersion)\")\n        trace \"macOS version is $version\"\n        if semver_gte \"$version\" 13.3.0; then\n            _RD_SUPPORTS_VZ_EMULATION=true\n        elif [[ $ARCH == x86_64 ]] && semver_gte \"$version\" 13.0.0; then\n            # Versions 13.0.x .. 13.2.x work only on x86_64, not aarch64\n            _RD_SUPPORTS_VZ_EMULATION=true\n        else\n            _RD_SUPPORTS_VZ_EMULATION=false\n        fi\n        save_var _RD_SUPPORTS_VZ_EMULATION\n    fi\n    is_true \"${_RD_SUPPORTS_VZ_EMULATION}\"\n}\n"
  },
  {
    "path": "bats/tests/helpers/paths.bash",
    "content": "# PATH_BATS_ROOT, PATH_BATS_LOGS, and PATH_BATS_HELPERS are already set by load.bash\n\nPATH_REPO_ROOT=$(absolute_path \"$PATH_BATS_ROOT/..\")\n\ninside_repo_clone() {\n    [ -d \"$PATH_REPO_ROOT/pkg/rancher-desktop\" ]\n}\n\nset_path_resources() {\n    local system=$1\n    local user=$2\n    local dist=$3\n    local subdir=$4\n    local fd=3\n\n    if [[ ! -e /dev/fd/3 ]]; then\n        fd=2\n    fi\n\n    if [ -z \"${RD_LOCATION:-}\" ]; then\n        if [ -d \"$system\" ]; then\n            RD_LOCATION=system\n        elif [ -d \"$user\" ]; then\n            RD_LOCATION=user\n        elif [ -d \"$dist\" ]; then\n            RD_LOCATION=dist\n        elif inside_repo_clone; then\n            RD_LOCATION=dev\n        else\n            (\n                echo \"Couldn't locate Rancher Desktop in\"\n                echo \"- \\\"$system\\\"\"\n                echo \"- \\\"$user\\\"\"\n                echo \"- \\\"$dist\\\"\"\n                echo \"and 'yarn dev' is unavailable outside repo clone\"\n            ) >&$fd\n            exit 1\n        fi\n    fi\n    if using_dev_mode; then\n        if is_windows; then\n            fatal \"yarn operation not yet implemented for Windows\"\n        fi\n        PATH_RESOURCES=\"$PATH_REPO_ROOT/resources\"\n    else\n        PATH_RESOURCES=\"${!RD_LOCATION}/${subdir}\"\n    fi\n    if [ ! -d \"$PATH_RESOURCES\" ]; then\n        fatal \"App resource directory '$PATH_RESOURCES' does not exist\"\n    fi\n}\n\nif is_macos; then\n    PATH_APP_HOME=\"$HOME/Library/Application Support/rancher-desktop\"\n    PATH_CONFIG=\"$HOME/Library/Preferences/rancher-desktop\"\n    PATH_CACHE=\"$HOME/Library/Caches/rancher-desktop\"\n    PATH_LOGS=\"$HOME/Library/Logs/rancher-desktop\"\n    PATH_EXTENSIONS=\"$PATH_APP_HOME/extensions\"\n    LIMA_HOME=\"$PATH_APP_HOME/lima\"\n    PATH_SNAPSHOTS=\"$PATH_APP_HOME/snapshots\"\n    PATH_CONTAINERD_SHIMS=\"$PATH_APP_HOME/containerd-shims\"\n\n    ELECTRON_DIST_ARCH=\"mac\"\n    if is_macos aarch64; then\n        ELECTRON_DIST_ARCH=\"mac-arm64\"\n    fi\n    set_path_resources \\\n        \"/Applications/Rancher Desktop.app\" \\\n        \"$HOME/Applications/Rancher Desktop.app\" \\\n        \"$PATH_REPO_ROOT/dist/$ELECTRON_DIST_ARCH/Rancher Desktop.app\" \\\n        \"Contents/Resources/resources\"\nfi\n\nif is_linux; then\n    PATH_APP_HOME=\"$HOME/.local/share/rancher-desktop\"\n    PATH_CONFIG=\"$HOME/.config/rancher-desktop\"\n    PATH_CACHE=\"$HOME/.cache/rancher-desktop\"\n    PATH_LOGS=\"$PATH_APP_HOME/logs\"\n    PATH_EXTENSIONS=\"$PATH_APP_HOME/extensions\"\n    LIMA_HOME=\"$PATH_APP_HOME/lima\"\n    PATH_SNAPSHOTS=\"$PATH_APP_HOME/snapshots\"\n    PATH_CONTAINERD_SHIMS=\"$PATH_APP_HOME/containerd-shims\"\n\n    set_path_resources \\\n        \"/opt/rancher-desktop\" \\\n        \"$HOME/opt/rancher-desktop\" \\\n        \"$PATH_REPO_ROOT/dist/linux-unpacked\" \\\n        \"resources/resources\"\nfi\n\nwslpath_from_win32_env() {\n    # The cmd.exe _sometimes_ returns an empty string when invoked in a subshell\n    # wslpath \"$(cmd.exe /c \"echo %$1%\" 2>/dev/null)\" | tr -d \"\\r\"\n    # Let's see if powershell.exe avoids this issue\n    wslpath \"$(powershell.exe -Command \"Write-Output \\${Env:$1}\")\" | tr -d \"\\r\"\n}\n\nif is_windows; then\n    LOCALAPPDATA=\"$(wslpath_from_win32_env LOCALAPPDATA)\"\n    PROGRAMFILES=\"$(wslpath_from_win32_env ProgramFiles)\"\n    SYSTEMROOT=\"$(wslpath_from_win32_env SystemRoot)\"\n\n    PATH_APP_HOME=\"$LOCALAPPDATA/rancher-desktop\"\n    PATH_CONFIG=\"$LOCALAPPDATA/rancher-desktop\"\n    PATH_CACHE=\"$PATH_APP_HOME/cache\"\n    PATH_LOGS=\"$PATH_APP_HOME/logs\"\n    PATH_DISTRO=\"$PATH_APP_HOME/distro\"\n    PATH_DISTRO_DATA=\"$PATH_APP_HOME/distro-data\"\n    PATH_EXTENSIONS=\"$PATH_APP_HOME/extensions\"\n    PATH_SNAPSHOTS=\"$PATH_APP_HOME/snapshots\"\n    PATH_CONTAINERD_SHIMS=\"$PATH_APP_HOME/containerd-shims\"\n\n    set_path_resources \\\n        \"$PROGRAMFILES/Rancher Desktop\" \\\n        \"$LOCALAPPDATA/Programs/Rancher Desktop\" \\\n        \"$PATH_REPO_ROOT/dist/win-unpacked\" \\\n        \"resources/resources\"\nfi\n\nPATH_CONFIG_FILE=\"$PATH_CONFIG/settings.json\"\n\nUSERPROFILE=$HOME\nif using_windows_exe; then\n    USERPROFILE=\"$(wslpath_from_win32_env USERPROFILE)\"\nfi\n\nhost_path() {\n    local path=$1\n    if using_windows_exe; then\n        path=$(wslpath -w \"$path\")\n    fi\n    echo \"$path\"\n}\n"
  },
  {
    "path": "bats/tests/helpers/profile.bash",
    "content": "case $OS in\ndarwin)\n    PROFILE_SYSTEM_DEFAULTS=/Library/Preferences/io.rancherdesktop.profile.defaults.plist\n    PROFILE_SYSTEM_LOCKED=/Library/Preferences/io.rancherdesktop.profile.locked.plist\n    PROFILE_USER_DEFAULTS=\"${HOME}${PROFILE_SYSTEM_DEFAULTS}\"\n    PROFILE_USER_LOCKED=\"${HOME}${PROFILE_SYSTEM_LOCKED}\"\n    ;;\nlinux)\n    PROFILE_SYSTEM_DEFAULTS=/etc/rancher-desktop/defaults.json\n    PROFILE_SYSTEM_LOCKED=/etc/rancher-desktop/locked.json\n    PROFILE_USER_DEFAULTS=\"${HOME}/.config/rancher-desktop.defaults.json\"\n    PROFILE_USER_LOCKED=\"${HOME}/.config/rancher-desktop.locked.json\"\n    ;;\nwindows)\n    PROFILE='Software\\Policies\\Rancher Desktop'\n    PROFILE_SYSTEM_DEFAULTS=\"HKLM\\\\${PROFILE}\\\\Defaults\"\n    PROFILE_SYSTEM_LOCKED=\"HKLM\\\\${PROFILE}\\\\Locked\"\n    PROFILE_USER_DEFAULTS=\"HKCU\\\\${PROFILE}\\\\Defaults\"\n    PROFILE_USER_LOCKED=\"HKCU\\\\${PROFILE}\\\\Locked\"\n\n    # The legacy profiles (for both system and user) are supported for backward\n    # compatibility with Rancher Desktop 1.8.x. For BATS purposes the legacy\n    # user profiles have the advantage of being writable without admin rights.\n    PROFILE='Software\\Rancher Desktop\\Profile'\n    PROFILE_SYSTEM_LEGACY_DEFAULTS=\"HKLM\\\\${PROFILE}\\\\Defaults\"\n    PROFILE_SYSTEM_LEGACY_LOCKED=\"HKLM\\\\${PROFILE}\\\\Locked\"\n    PROFILE_USER_LEGACY_DEFAULTS=\"HKCU\\\\${PROFILE}\\\\Defaults\"\n    PROFILE_USER_LEGACY_LOCKED=\"HKCU\\\\${PROFILE}\\\\Locked\"\n    ;;\nesac\n\nPROFILE_SYSTEM=system\nPROFILE_SYSTEM_LEGACY=system-legacy\nPROFILE_USER=user\nPROFILE_USER_LEGACY=user-legacy\n\nPROFILE_DEFAULTS=defaults\nPROFILE_LOCKED=locked\n\n# Default location is a writable user location\nif is_windows; then\n    PROFILE_LOCATION=$PROFILE_USER_LEGACY\nelse\n    PROFILE_LOCATION=$PROFILE_USER\nfi\nPROFILE_TYPE=$PROFILE_DEFAULTS\n\n# profile_location is a registry key on Windows, or a filename on macOS and Linux.\nprofile_location() {\n    local profile\n    profile=$(to_upper \"profile_${PROFILE_LOCATION}_${PROFILE_TYPE}\" | tr - _)\n    echo \"${!profile}\"\n}\n\n# Execute command for each profile\nforeach_profile() {\n    local locations=(\"$PROFILE_SYSTEM\" \"$PROFILE_USER\")\n    if is_windows; then\n        locations+=(\"$PROFILE_SYSTEM_LEGACY\" \"$PROFILE_USER_LEGACY\")\n    fi\n\n    local PROFILE_LOCATION PROFILE_TYPE\n    for PROFILE_LOCATION in \"${locations[@]}\"; do\n        for PROFILE_TYPE in \"$PROFILE_DEFAULTS\" \"$PROFILE_LOCKED\"; do\n            \"$@\"\n        done\n    done\n}\n\n# Check if profile exists\nprofile_exists() {\n    case $OS in\n    darwin | linux)\n        [[ -f $(profile_location) ]]\n        ;;\n    windows)\n        profile_reg query &>/dev/null\n        ;;\n    esac\n}\n\n# Create empty profile\ncreate_profile() {\n    case $OS in\n    darwin)\n        profile_plutil -create xml1\n        ;;\n    linux)\n        local filename\n        filename=$(profile_location)\n        profile_sudo mkdir -p \"$(dirname \"$filename\")\"\n        echo \"{}\" | profile_cat \"$filename\"\n        ;;\n    windows)\n        # Make sure any old profile data at this location is removed\n        run profile_reg delete \".\"\n        assert_nothing\n        # Create subkey so that profile_exists returns true now\n        profile_reg add \".\"\n        ;;\n    esac\n}\n\n# Completely remove the profile. Ignores error if profile doesn't exist\ndelete_profile() {\n    if deleting_profiles; then\n        case $OS in\n        darwin | linux)\n            run profile_sudo rm -f \"$(profile_location)\"\n            assert_nothing\n            ;;\n        windows)\n            run profile_reg delete \".\"\n            assert_nothing\n            ;;\n        esac\n    fi\n}\n\n# Export/copy profile to a directory\nexport_profile() {\n    local dir=$1\n    if profile_exists; then\n        local export=\"${dir}/profile.${PROFILE_LOCATION}.${PROFILE_TYPE}\"\n        case $OS in\n        darwin | linux)\n            local filename\n            filename=$(profile_location)\n            # Keep .plist or .json file extension\n            cp \"$filename\" \"${export}.${filename##*.}\"\n            ;;\n        windows)\n            export=\"$(wslpath -w \"${export}.reg\")\"\n            profile_reg export \"${export}\" /y\n            ;;\n        esac\n    fi\n}\n\n# Set a profile setting to a boolean; value must be \"true\" or \"false\"\n# The profile must exist before calling this function.\nadd_profile_bool() {\n    local setting=$1\n    local value=$2\n\n    assert profile_exists\n    case $OS in\n    darwin)\n        profile_plutil -replace \"$setting\" -bool \"$value\"\n        ;;\n    linux)\n        profile_jq \".${setting} = ${value}\"\n        ;;\n    windows)\n        if [[ $value == true ]]; then\n            profile_reg add \"$setting\" /t REG_DWORD /d 1\n        else\n            profile_reg add \"$setting\" /t REG_DWORD /d 0\n        fi\n        ;;\n    esac\n}\n\n# Set a profile setting to an integer.\n# The profile must exist before calling this function.\nadd_profile_int() {\n    local setting=$1\n    local value=$2\n\n    assert profile_exists\n    case $OS in\n    darwin)\n        profile_plutil -replace \"$setting\" -integer \"$value\"\n        ;;\n    linux)\n        profile_jq \".${setting} = ${value}\"\n        ;;\n    windows)\n        profile_reg add \"$setting\" /t REG_DWORD /d \"$value\"\n        ;;\n    esac\n}\n\n# Set a profile setting to a string.\n# The profile must exist before calling this function.\nadd_profile_string() {\n    local setting=$1\n    local value=$2\n\n    assert profile_exists\n    case $OS in\n    darwin)\n        profile_plutil -replace \"$setting\" -string \"$value\"\n        ;;\n    linux)\n        profile_jq \".${setting} = $(json_string \"$value\")\"\n        ;;\n    windows)\n        profile_reg add \"$setting\" /t REG_SZ /d \"$value\"\n        ;;\n    esac\n}\n\n# Set a profile setting to a list of strings, replacing any existing elements.\n# The profile must exist before calling this function.\nadd_profile_list() {\n    local elem\n    local setting=$1\n    shift\n\n    assert profile_exists\n    case $OS in\n    darwin)\n        profile_plutil -replace \"$setting\" -array\n        for elem in \"$@\"; do\n            profile_plutil -insert \"$setting\" -string \"$elem\" -append\n        done\n        ;;\n    linux)\n        profile_jq \".${setting} = []\"\n        for elem in \"$@\"; do\n            profile_jq \".${setting} += [$(json_string \"$elem\")]\"\n        done\n        ;;\n    windows)\n        # TODO: what happens when the values contain whitespace or quote characters?\n        profile_reg add \"$setting\" /t REG_MULTI_SZ /d \"$(join_map '\\0' echo \"$@\")\"\n        ;;\n    esac\n}\n\n# Remove a key or named value from the profile.\n# Use a trailing dot to specify that the setting points to a key, e.g. \"foo.bar.\".\n# It only makes a difference on Windows but will work on all platforms.\nremove_profile_entry() {\n    local setting=$1\n\n    assert profile_exists\n    case $OS in\n    darwin)\n        profile_plutil -remove \"${setting%.}\"\n        ;;\n    linux)\n        # This relies on `null` not being a valid setting value.\n        profile_jq \"\n            if (try .${setting%.}) | type == \\\"null\\\" then\n                error(\\\"setting ${setting%.} not found\\\")\n            else\n                del(.${setting%.})\n            end\n        \"\n        ;;\n    windows)\n        profile_reg delete \"$setting\"\n        ;;\n    esac\n}\n\n################################################################################\n# functions defined below this line are implementation detail and should not\n# be called directly from any tests.\n################################################################################\n\n# Returns number of setting segments (separated by dots), e.g. foo.bar.baz returns 3\ncount_setting_segments() {\n    echo \"${1//./$'\\n'}\" | wc -l\n}\n\n# Usage: profile_jq $expr\n#\n# Applies $expr against the profile and update it in-place.\nprofile_jq() {\n    local expr=$1\n    local filename\n    filename=$(profile_location)\n\n    assert_file_exists \"$filename\"\n    # Need to use a temp file to avoid truncating the file before it has been read.\n    jq \"$expr\" \"$filename\" | profile_cat \"${filename}.tmp\"\n    profile_sudo mv \"${filename}.tmp\" \"$filename\"\n}\n\n# Usage: profile_plutil $action $options\n#\n# For -insert|-replace|-remove actions it will make sure all higher level\n# dictionaries are created first because plutil doesn't do it by itself.\nprofile_plutil() {\n    local action=$1\n\n    # Make sure all the dictionaries for the setting path exist\n    if [[ $action =~ ^-insert|-replace|-remove$ ]]; then\n        local setting=$2\n        local count\n        count=$(count_setting_segments \"$setting\")\n        if ((count > 1)); then\n            local index\n            for index in $(seq $((count - 1))); do\n                local keypath\n                keypath=$(echo \"$setting\" | cut -d . -f 1-\"$index\")\n                # Ignore error if dictionary already exists\n                profile_sudo plutil -insert \"$keypath\" -dictionary \"$(profile_location)\" || :\n            done\n        fi\n    fi\n\n    profile_sudo plutil \"$@\" \"$(profile_location)\"\n}\n\n# Usage: profile_reg $action $options\n#    or: profile_reg add|delete $setting $options\n#\n# Determines the $reg_key from both the profile_location() and the $setting.\n# Setting `foo.bar.baz` means `foo\\bar` is the reg_subkey, and `baz` is the value name.\n#\n# Special case `foo.bar.` is used only for \"delete\" action and specifies `foo\\bar`\n# as the subkey to be deleted (including all values under the key).\nprofile_reg() {\n    local action=$1\n    shift\n\n    local reg_key\n    reg_key=$(profile_location)\n    if [[ $action =~ ^add|delete$ ]]; then\n        local setting=$1\n        shift\n\n        local count\n        count=$(count_setting_segments \"$setting\")\n        if ((count > 1)); then\n            local reg_subkey\n            reg_subkey=$(echo \"$setting\" | cut -d . -f 1-\"$((count - 1))\")\n            # reg_key uses backslashes instead of dot separators\n            reg_key=\"${reg_key}\\\\${reg_subkey//./\\\\}\"\n        fi\n\n        local reg_value_name\n        reg_value_name=$(echo \"$setting\" | cut -d . -f \"$count\")\n        # reg_value_name may be empty when deleting a registry key instead of a named value\n        if [[ -n $reg_value_name ]]; then\n            # turn protected dots back into regular dots again\n            set - /v \"${reg_value_name//$RD_PROTECTED_DOT/.}\" \"$@\"\n        fi\n\n        # Delete entries (and overwrite existing ones) without prompt\n        set - \"$@\" /f\n    fi\n\n    reg.exe \"$action\" \"$reg_key\" \"$@\"\n}\n\nprofile_sudo() {\n    # TODO How can we make this work on Windows?\n    if [[ $PROFILE_LOCATION == system ]]; then\n        sudo -n \"$@\"\n    else\n        \"$@\"\n    fi\n}\n\nprofile_cat() {\n    profile_sudo tee \"$1\" >/dev/null\n}\n\nensure_profile_is_deleted() {\n    delete_profile\n    if profile_exists; then\n        fatal \"Cannot delete $(profile_location)\"\n    fi\n}\n\n# Only run this once per test file. It cannot be part of setup_file() because\n# we want to be able to call fatal() and skip the rest of the tests.\nif [[ -z ${BATS_SUITE_TEST_NUMBER:-} ]] && deleting_profiles; then\n    foreach_profile ensure_profile_is_deleted\nfi\n"
  },
  {
    "path": "bats/tests/helpers/snapshots.bash",
    "content": "delete_all_snapshots() {\n    run rdctl snapshot list --json\n    assert_success\n    # On Windows, executing native Windows executables consumes stdin.\n    # https://github.com/microsoft/WSL/issues/10429\n    # Work around the issue by using `run` to populate `${lines[@]}` ahead of\n    # time, so that we don't need the buffer during the loop.\n    run jq_output .name\n    assert_success\n    local name\n    for name in \"${lines[@]}\"; do\n        rdctl snapshot delete \"$name\"\n    done\n    run rdctl snapshot list\n    assert_success\n    assert_output --partial 'No snapshots'\n}\n"
  },
  {
    "path": "bats/tests/helpers/utils.bash",
    "content": "to_lower() {\n    echo \"$@\" | tr '[:upper:]' '[:lower:]'\n}\n\nto_upper() {\n    echo \"$@\" | tr '[:lower:]' '[:upper:]'\n}\n\nis_true() {\n    # case-insensitive check; false values: '', '0', 'no', and 'false'\n    local value\n    value=$(to_lower \"$1\")\n    [[ ! $value =~ ^(0|no|false)?$ ]]\n}\n\nis_false() {\n    ! is_true \"$1\"\n}\n\nbool() {\n    if \"$@\"; then\n        echo \"true\"\n    else\n        echo \"false\"\n    fi\n}\n\n# Ensure that the variable contains a valid value, e.g.\n# `validate_enum VAR value1 value2`\nvalidate_enum() {\n    local var=$1\n    shift\n    for value in \"$@\"; do\n        if [[ ${!var} == \"$value\" ]]; then\n            return\n        fi\n    done\n    fatal \"$var=${!var} is not a valid setting; select from [$*]\"\n}\n\n# Ensure that the variable contains a valid semver (major.minor.path) version, e.g.\n# `validate_semver RD_K3S_MAX`\nvalidate_semver() {\n    local var=$1\n    if ! semver_is_valid \"${!var}\"; then\n        fatal \"$var=${!var} is not a valid semver value (major.minor.patch)\"\n    fi\n}\n\nassert_nothing() {\n    # This is a no-op, used to show that run() has been used to continue the\n    # test even when the command failed, but the failure itself is ignored.\n    true\n}\n\n########################################################################\n\nassert=assert\nrefute=refute\n\nbefore() {\n    local assert=refute\n    local refute=assert\n    \"$@\"\n}\n\nrefute_success() {\n    assert_failure\n}\n\nrefute_failure() {\n    assert_success\n}\n\nrefute_not_exists() {\n    assert_exists \"$@\"\n}\n\nrefute_file_exists() {\n    assert_file_not_exists \"$@\"\n}\n\nrefute_file_contains() {\n    assert_file_not_contains \"$@\"\n}\n\n########################################################################\n\n# Convert raw string into properly quoted JSON string\njson_string() {\n    echo -n \"$1\" | jq --raw-input --raw-output @json\n}\n\n# Join list elements by separator after converting them via the mapping function\n# Examples:\n#   join_map \"/\" echo usr local bin            =>   usr/local/bin\n#   join_map \", \" json_string a b\\ c\\\"d\\\\e f   =>   \"a\", \"b c\\\"d\\\\e\", \"f\"\njoin_map() {\n    local sep=$1\n    local map=$2\n    shift 2\n\n    local elem\n    local result=\"\"\n    for elem in \"$@\"; do\n        elem=$(eval \"$map\" '\"$elem\"')\n        if [[ -z $result ]]; then\n            result=$elem\n        else\n            result=\"${result}${sep}${elem}\"\n        fi\n    done\n    echo \"$result\"\n}\n\n# Run jq on the current $output\n# Note that when capturing $output, you may need to use `run --separate-stderr`\n# to avoid also capturing stderr and ending up with invalid JSON.\njq_output() {\n    local json=$output\n    run jq --raw-output \"$@\" <<<\"${json}\"\n    if [[ -n $output ]]; then\n        echo \"$output\"\n        if [[ $output == null ]]; then\n            status=1\n        fi\n    elif ((status == 0)); then\n        # The command succeeded, so we should be able to run it again without error\n        # If the jq command emitted a newline, then we want to emit a newline too.\n        if [ \"$(jq --raw-output \"$@\" <<<\"${json}\" | wc -c)\" -gt 0 ]; then\n            echo \"\"\n        fi\n    fi\n    return \"$status\"\n}\n\n# semver returns the first semver version from its first argument (which may be multiple lines).\n# It does not include pre-release markers or build ids.\n# It will match major.minor, or even just major if it can't find major.minor.patch.\n# The returned version will always be a major.minor.patch string.\n# Each part will have leading zeros removed.\n# semver will fail when the input contains no number.\nsemver() {\n    local input=$1\n    local semver\n    semver=$(awk 'match($0, /([0-9]+\\.[0-9]+\\.[0-9]+)/) {print substr($0, RSTART, RLENGTH); exit}' <<<\"$input\")\n    if [[ -z $semver ]]; then\n        semver=$(awk 'match($0, /([0-9]+\\.[0-9]+)/) {print substr($0, RSTART, RLENGTH); exit}' <<<\"$input\")\n    fi\n    if [[ -z $semver ]]; then\n        semver=$(awk 'match($0, /([0-9]+)/) {print substr($0, RSTART, RLENGTH); exit}' <<<\"$input\")\n    fi\n    if [[ -z $semver ]]; then\n        return 1\n    fi\n    until [[ $semver =~ \\..+\\. ]]; do\n        semver=\"${semver}.0\"\n    done\n    sed -E 's/^0*([0-9])/\\1/; s/\\.0*([0-9])/.\\1/g' <<<\"$semver\"\n}\n\n# Check if the argument is a valid 3-tuple version number with no leading 0s and no newlines\nsemver_is_valid() {\n    [[ ! $1 =~ $'\\n' ]] && grep -q -E '^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$' <<<\"$1\"\n}\n\n# All semver comparison functions will return false when called without any argument\n# and return true when called with just a single argument.\n\n# semver_eq checks that all specified arguments are equal to each other.\n# (semver_eq and semver_neq don't really depend on the arguments being versions).\n# `A = B = C`\nsemver_eq() {\n    [[ $# -gt 0 ]] && [[ $(printf \"%s\\n\" \"$@\" | sort --unique | wc -l) -eq 1 ]]\n}\n\n# semver_neq checks that all arguments are unique. `semver_neq A B C` is not the same as\n# `A ≠ B ≠ C` because semver_neq will also return a failure if `A = C`.\n# `(A ≠ B) & (A ≠ C) & (B ≠ C)`\nsemver_neq() {\n    [[ $# -gt 0 ]] && printf \"%s\\n\" \"$@\" | sort | sort --check=silent --unique\n}\n\n# `A ≤ B ≤ C`\nsemver_lte() {\n    [[ $# -gt 0 ]] && printf \"%s\\n\" \"$@\" | sort --check=silent --version-sort\n}\n\n# `A < B < C`\nsemver_lt() {\n    [[ $# -gt 0 ]] && semver_lte \"$@\" && semver_neq \"$@\"\n}\n\n# `A ≥ B ≥ C`\nsemver_gte() {\n    [[ $# -gt 0 ]] && printf \"%s\\n\" \"$@\" | sort --check=silent --reverse --version-sort\n}\n\n# `A > B > C`\nsemver_gt() {\n    [[ $# -gt 0 ]] && semver_gte \"$@\" && semver_neq \"$@\"\n}\n\n########################################################################\n\nget_setting() {\n    run rdctl api /settings\n    assert_success\n    jq_output \"$@\"\n}\n\nthis_function() {\n    echo \"${FUNCNAME[1]}\"\n}\n\ncalling_function() {\n    echo \"${FUNCNAME[2]}\"\n}\n\n# Write a comment to the TAP stream.\n# Set CALLER to print a calling function higher up in the call stack.\ncomment() {\n    local prefix=\"\"\n    if is_true \"$RD_TRACE\"; then\n        local caller=\"${CALLER:-$(calling_function)}\"\n        prefix=\"($(date -u +\"%FT%TZ\"): ${caller}): \"\n    fi\n    local line\n    while IFS= read -r line; do\n        if [[ -e /dev/fd/3 ]]; then\n            printf \"# %s%s\\n\" \"$prefix\" \"$line\" >&3\n        else\n            printf \"# %s%s\\n\" \"$prefix\" \"$line\" >&2\n        fi\n    done <<<\"$*\"\n}\n\n# Write a comment to the TAP stream if RD_TRACE is set.\n# Set CALLER to print a calling function higher up in the call stack.\ntrace() {\n    if is_true \"$RD_TRACE\"; then\n        CALLER=${CALLER:-$(calling_function)} comment \"$@\"\n    fi\n}\n\n# try runs the specified command until it either succeeds, or --max attempts\n# have been made (with a --delay seconds sleep in between).\n#\n# Right now the command is **always** run with --separate-stderr, and stderr\n# is output after all of stdout. This is subject to change, if we can figure\n# out a way to detect if the caller used `run --separate-stderr try …` or not.\ntry() {\n    local max=24\n    local delay=5\n\n    while [[ $# -gt 0 ]] && [[ $1 == -* ]]; do\n        case \"$1\" in\n        --max)\n            max=$2\n            shift\n            ;;\n        --delay)\n            delay=$2\n            shift\n            ;;\n        --)\n            shift\n            break\n            ;;\n        *)\n            printf \"Usage error: unknown flag '%s'\" \"$1\" >&2\n            return 1\n            ;;\n        esac\n        shift\n    done\n\n    local count=0\n    while true; do\n        run --separate-stderr \"$@\"\n        if ((status == 0 || ++count >= max)); then\n            trace \"$count/$max tries: $*\"\n            break\n        fi\n        sleep \"$delay\"\n    done\n    echo \"$output\"\n    if [ -n \"${stderr:-}\" ]; then\n        echo \"$stderr\" >&2\n    fi\n    return \"$status\"\n}\n\nimage_without_tag_as_json_string() {\n    local image=$1\n    # If the tag looks like a port number and follows something that looks\n    # like a domain name, then don't strip the tag (e.g. foo.io:5000).\n    if [[ ${image##*:} =~ ^[0-9]+(/|$) && ${image%:*} =~ \\.[a-z]+$ ]]; then\n        json_string \"$image\"\n    else\n        json_string \"${image%:*}\"\n    fi\n}\n\nupdate_allowed_patterns() {\n    local enabled=$1\n    shift\n\n    local patterns\n    patterns=$(join_map \", \" image_without_tag_as_json_string \"$@\")\n\n    # If the enabled state changes, then the container engine will be restarted.\n    # Record PID of the current daemon process so we can wait for it to be ready again.\n    local pid\n    if [ \"$enabled\" != \"$(get_setting .containerEngine.allowedImages.enabled)\" ]; then\n        pid=$(get_service_pid \"$CONTAINER_ENGINE_SERVICE\")\n    fi\n\n    rdctl api settings -X PUT --input - <<EOF\n{\n  \"version\": 8,\n  \"containerEngine\": {\n    \"allowedImages\": {\n      \"enabled\": $enabled,\n      \"patterns\": [$patterns]\n    }\n  }\n}\nEOF\n    # Wait for container engine (and Kubernetes) to be ready again\n    if [[ -n ${pid:-} ]]; then\n        try --max 15 --delay 5 refute_service_pid \"$CONTAINER_ENGINE_SERVICE\" \"$pid\"\n        wait_for_container_engine\n        if [[ $(get_setting .kubernetes.enabled) == \"true\" ]]; then\n            wait_for_kubelet\n        fi\n    fi\n}\n\n# create_file path/to/file <<< \"contents\"\n# Create a new file with the provided path; the contents of standard input will\n# be written to that file.  Analogous to `cat >$1`.  Will create any parent\n# directories.\ncreate_file() {\n    local dest=$1\n    # On Windows, avoid creating files from within WSL; this leads to issues\n    # where the WSL view of the filesystem is desynchronized from the Windows\n    # view, so we end up having ghost files that can't be deleted from Windows.\n    if ! is_windows; then\n        mkdir -p \"$(dirname \"$dest\")\"\n        cat >\"$dest\"\n        return\n    fi\n\n    local contents # Base64 encoded file contents\n    contents=\"$(base64)\"\n\n    local winParent\n    local winDest\n    winParent=\"$(wslpath -w \"$(dirname \"$dest\")\")\"\n    winDest=\"$(wslpath -w \"$dest\")\"\n    PowerShell.exe -NoProfile -NoLogo -NonInteractive -Command \"New-Item -ItemType Directory -ErrorAction SilentlyContinue '$winParent'\" || true\n    local command=\"[IO.File]::WriteAllBytes('$winDest', \\$([System.Convert]::FromBase64String('$contents')))\"\n    PowerShell.exe -NoProfile -NoLogo -NonInteractive -Command \"$command\"\n}\n\n# unique_filename /tmp/image .png\n# will return /tmp/image.png, or /tmp/image_2.png, etc.\nunique_filename() {\n    local basename=$1\n    local extension=${2:-}\n    local index=1\n    local suffix=\"\"\n\n    while true; do\n        local filename=\"${basename}${suffix}${extension}\"\n        if [[ ! -e $filename ]]; then\n            echo \"$filename\"\n            return\n        fi\n        suffix=\"_$((++index))\"\n    done\n}\n\ncapture_logs() {\n    if capturing_logs && [[ -d $PATH_LOGS ]]; then\n        local logdir\n        logdir=$(unique_filename \"${PATH_BATS_LOGS}/${RD_TEST_FILENAME}\")\n        mkdir -p \"$logdir\"\n        # On Linux/macOS, the symlinks to the lima logs might be dangling.\n        # Remove any dangling ones before doing the copy.\n        find -L \"${PATH_LOGS}/\" -type l \\\n            -exec rm -f -- '{}' ';' \\\n            -exec touch -- '{}' ';' \\\n            -exec echo 'Replaced dangling symlink with empty file:' '{}' ';'\n        cp -LR \"${PATH_LOGS}/\" \"$logdir\"\n        echo \"${BATS_TEST_DESCRIPTION:-teardown}\" >\"${logdir}/test_description\"\n        # Capture settings.json\n        cp \"$PATH_CONFIG_FILE\" \"$logdir\"\n        foreach_profile export_profile \"$logdir\"\n    fi\n}\n\ntake_screenshot() {\n    if taking_screenshots; then\n        local image_path\n        image_path=\"$(unique_filename \"${PATH_BATS_LOGS}/${BATS_SUITE_TEST_NUMBER}-${BATS_TEST_DESCRIPTION}\" .png)\"\n        mkdir -p \"$PATH_BATS_LOGS\"\n        if is_macos; then\n            # The terminal app must have \"Screen Recording\" permission;\n            # otherwise only the desktop background is captured.\n            # -x option means \"do not play sound\"\n            screencapture -x \"$image_path\"\n        elif is_linux; then\n            if import -help </dev/null 2>&1 | grep --quiet -E 'Version:.*Magick'; then\n                # `import` from ImageMagick is available.\n                import -window root \"$image_path\"\n            elif gm import -help </dev/null 2>&1 | grep --quiet -E 'Version:.*Magick'; then\n                # GraphicsMagick is installed (its command is `gm`).\n                gm import -window root \"$image_path\"\n            fi\n        fi\n    fi\n}\n\nskip_unless_host_ip() {\n    if using_windows_exe; then\n        # Make sure the exit code is 0 even when netsh.exe or grep fails, in case errexit is in effect\n        HOST_IP=$(netsh.exe interface ip show addresses 'vEthernet (WSL)' | grep -Po 'IP Address:\\s+\\K[\\d.]+' || :)\n        # The veth interface name changed at some time on Windows 11, so try the new name if the old one doesn't exist\n        if [[ -z $HOST_IP ]]; then\n            HOST_IP=$(netsh.exe interface ip show addresses 'vEthernet (WSL (Hyper-V firewall))' | grep -Po 'IP Address:\\s+\\K[\\d.]+' || :)\n        fi\n    else\n        # TODO determine if the Lima VM has its own IP address\n        HOST_IP=\"\"\n    fi\n    if [[ -z $HOST_IP ]]; then\n        skip \"Test requires a routable host ip address\"\n    fi\n}\n\n########################################################################\n\n# Register one or more test commands for each k3s version in RD_K3S_VERSIONS.\n# Versions can be filtered by RD_K3S_MIN and RD_K3S_MAX.\nforeach_k3s_version() {\n    local k3s_version\n    for k3s_version in $RD_K3S_VERSIONS; do\n        if semver_lte \"$RD_K3S_MIN\" \"$k3s_version\" \"$RD_K3S_MAX\"; then\n            local cmd\n            for cmd in \"$@\"; do\n                bats_test_function --description \"$cmd $k3s_version\" -- _foreach_k3s_version \"$k3s_version\" \"$cmd\"\n            done\n        fi\n    done\n}\n\n_foreach_k3s_version() {\n    local RD_KUBERNETES_VERSION=$1\n    local skip_kubernetes_version\n    skip_kubernetes_version=$(cat \"${BATS_FILE_TMPDIR}/skip-kubernetes-version\" 2>/dev/null || echo none)\n    if [[ $skip_kubernetes_version == \"$RD_KUBERNETES_VERSION\" ]]; then\n        skip \"All remaining tests for Kubernetes $RD_KUBERNETES_VERSION are skipped\"\n    fi\n    \"$2\"\n}\n\n# Tests can call mark_k3s_version_skipped to skip the rest of the tests within\n# this iteration of foreach_k3s_version.\nmark_k3s_version_skipped() {\n    echo \"$RD_KUBERNETES_VERSION\" >\"${BATS_FILE_TMPDIR}/skip-kubernetes-version\"\n}\n\n########################################################################\n\n_var_filename() {\n    # Can't use BATS_SUITE_TMPDIR because it is unset outside of @test functions\n    echo \"${BATS_RUN_TMPDIR}/var_$1\"\n}\n\n# Save env variables on disk, so they can be reloaded in different tests.\n# This is mostly useful if calculating the setting takes a long time.\n# Returns false if any variable was unbound, but will continue saving remaining variables.\n# `save_var VAR1 VAR2`\nsave_var() {\n    local res=0\n    local var\n    for var in \"$@\"; do\n        # Using [[ -v $var ]] requires bash 4.2 but macOS only ships with 3.2\n        if [ -n \"${!var+exists}\" ]; then\n            printf \"%s=%q\\n\" \"$var\" \"${!var}\" >\"$(_var_filename \"$var\")\"\n        else\n            res=1\n        fi\n    done\n    return $res\n}\n\n# Load env variables saved by `save_var`. Returns an error if any of the variables\n# had not been saved, but will continue to try to load the remaining variables.\n# `load_var VAR1 VAR2`\nload_var() {\n    local res=0\n    local var\n    for var in \"$@\"; do\n        local file\n        file=$(_var_filename \"$var\")\n        if [[ -r $file ]]; then\n            # shellcheck disable=SC1090 # Can't follow non-constant source\n            source \"$file\"\n        else\n            res=1\n        fi\n    done\n    return $res\n}\n"
  },
  {
    "path": "bats/tests/helpers/utils.bats",
    "content": "# bats file_tags=opensuse\n\nload '../helpers/load'\n\n: \"${RD_INFO:=false}\"\n\n########################################################################\n\nlocal_setup() {\n    COUNTER=\"${BATS_FILE_TMPDIR}/counter\"\n    reset_counter\n}\n\nreset_counter() {\n    echo 0 >\"$COUNTER\"\n    SECONDS=0\n}\n\n# Increment counter file. Return success when counter >= max.\ninc_counter() {\n    local max=${1-9999}\n    local counter=$(($(cat \"$COUNTER\") + 1))\n    echo $counter >\"$COUNTER\"\n    ((counter >= max))\n}\n\nassert_counter_is() {\n    run cat \"${COUNTER}\"\n    assert_output \"$1\"\n}\n\nis() {\n    local expect=$1\n    # shellcheck disable=SC2086 # we want to split on whitespace\n    run ${BATS_TEST_DESCRIPTION}\n    assert_success\n    assert_output \"$expect\"\n}\n\nis_quoted() {\n    is \"\\\"$1\\\"\"\n}\n\nsucceeds() {\n    # shellcheck disable=SC2086 # we want to split on whitespace\n    run ${BATS_TEST_DESCRIPTION}\n    assert_success\n}\n\nfails() {\n    # shellcheck disable=SC2086 # we want to split on whitespace\n    run ${BATS_TEST_DESCRIPTION}\n    assert_failure\n}\n\n########################################################################\n\nerrexit() {\n    false\n    true\n}\n\n@test 'run() calls functions with errexit enabled' {\n    run errexit\n    assert_failure\n}\n\n########################################################################\n\n@test 'to_lower Upper and Lower' {\n    is \"upper and lower\"\n}\n\n@test 'to_lower' {\n    is \"\"\n}\n\n@test 'to_upper 123+abc' {\n    is \"123+ABC\"\n}\n\n@test 'to_upper' {\n    is \"\"\n}\n\n########################################################################\n\ncheck_truthiness() {\n    local predicate=$1\n    local value\n\n    # test true values\n    for value in 1 true True TRUE yes Yes YES any; do\n        run \"$predicate\" \"$value\"\n        \"${assert}_success\"\n    done\n\n    # test false values\n    for value in 0 false False FALSE no No NO ''; do\n        run \"$predicate\" \"$value\"\n        \"${assert}_failure\"\n    done\n}\n\n@test 'is_true' {\n    check_truthiness is_true\n}\n\n@test 'is_false' {\n    assert=refute\n    check_truthiness is_false\n}\n\n@test 'bool [ 0 -eq 0 ]' {\n    is true\n}\n\n@test 'bool [ 0 -eq 1 ]' {\n    is false\n}\n\n########################################################################\n\n@test 'validate_enum OS should pass' {\n    run validate_enum OS darwin linux windows\n    assert_success\n}\n\n@test 'validate_enum FRUIT should fail' {\n    FRUIT=apple\n    run validate_enum FRUIT banana cherry pear\n    assert_failure\n    # Can't check output; it is written using \"fatal\":\n    # FRUIT=apple is not a valid setting; select from [banana cherry pear]\n}\n\n########################################################################\n\n@test 'is_xxx' {\n    # Exactly one of the is_xxx functions should return true\n    count=0\n    for os in linux macos windows; do\n        if \"is_$os\"; then\n            ((++count))\n        fi\n    done\n    ((count == 1))\n}\n\n########################################################################\n\nget_json_test_data() {\n    # The run/assert silliness is because shellcheck gets confused by direct assignment to $output\n    run echo '{\"String\":\"string\", \"False\":false, \"Null\":null}'\n    assert_success\n}\n\n@test 'jq_output extracts string value' {\n    get_json_test_data\n    run jq_output .String\n    assert_success\n    assert_output string\n}\n\n@test 'jq_output extracts \"false\" value' {\n    get_json_test_data\n    run jq_output .False\n    assert_success\n    assert_output false\n}\n\n@test 'jq_output cannot extract \"null\" value' {\n    get_json_test_data\n    run jq_output .Null\n    assert_failure\n    assert_output null\n}\n\n@test 'jq_output fails when key is not found' {\n    get_json_test_data\n    run jq_output .DoesNotExist\n    assert_failure\n    assert_output null\n}\n\n@test 'jq_output fails on null' {\n    output=null\n    run jq_output .Anything\n    assert_failure\n    assert_output null\n}\n\n@test 'jq_output fails on undefined' {\n    output=undefined\n    run jq_output .Anything\n    assert_failure\n    assert_output --partial \"parse error\"\n}\n\n@test 'jq_output fails on non-JSON data' {\n    output=\"This is not JSON\"\n    run jq_output .Anything\n    assert_failure\n    assert_output --partial \"parse error\"\n}\n\n@test 'jq_output does not return a newline when the output is \"nothing\"' {\n    output=\"\"\n    output=$(\n        jq_output .Anything\n        echo \".\"\n    )\n    assert_output \".\"\n}\n\n@test 'jq_output does return a newline when the output is the empty string' {\n    output='{\"Empty\": \"\"}'\n    output=$(\n        jq_output .Empty\n        echo \".\"\n    )\n    assert_output $'\\n.'\n}\n\n@test 'jq must be version 1.7.1 or newer' {\n    run semver \"$(jq --version)\"\n    assert_success\n    semver_gte \"$output\" 1.7.1\n}\n\n########################################################################\n\n@test 'semver a1b2.3c4.5.6d7.8.9.0' {\n    is 4.5.6\n}\n\n@test 'semver a1b2.3c4.5' {\n    is 2.3.0\n}\n\n@test 'semver a1b2c3' {\n    is 1.0.0\n}\n\n@test 'semver 1.2.3.4' {\n    is 1.2.3\n}\n\n@test 'semver 00.00.00' {\n    is 0.0.0\n}\n\n@test 'semver 000000' {\n    is 0.0.0\n}\n\n@test 'semver 0.001' {\n    is 0.1.0\n}\n\n@test 'semver 00100.00200.00300' {\n    is 100.200.300\n}\n\n@test 'semver ignores dates/times' {\n    run semver \"1/1/70 12:00:00 version 7.8\"\n    assert_success\n    assert_output 7.8.0\n}\n\n@test 'semver looks at all lines of the input' {\n    run semver $'Version1: 1.2\\nVersion2: 3.4.5'\n    assert_success\n    assert_output 3.4.5\n}\n\n@test 'semver looks only at the first argument' {\n    run semver 'Version1: 1.2' 'Version2: 3.4.5'\n    assert_success\n    assert_output 1.2.0\n}\n\n@test 'semver fails when input has no number' {\n    run semver \"Hello world\"\n    assert_failure\n}\n\n########################################################################\n\n@test 'semver_is_valid 1.2.3' {\n    succeeds\n}\n@test 'semver_is_valid 1.2.3-pre' {\n    fails\n}\n@test 'semver_is_valid v1.2.3' {\n    fails\n}\n@test 'semver_is_valid 1.2.' {\n    fails\n}\n@test 'semver_is_valid 1' {\n    fails\n}\n@test 'semver_is_valid 0.0.0' {\n    succeeds\n}\n@test 'semver_is_valid 01.2.3' {\n    fails\n}\n@test 'semver_is_valid 1.02.3' {\n    fails\n}\n@test 'semver_is_valid fails on trailing newline' {\n    run semver_is_valid $'1.2.3\\n'\n    assert_failure\n}\n\n########################################################################\n\n@test 'semver_eq' {\n    fails\n}\n@test 'semver_eq 1.2.3' {\n    succeeds\n}\n@test 'semver_eq 1.2.3 1.2.3' {\n    succeeds\n}\n@test 'semver_eq 1.2.3 4.5.6' {\n    fails\n}\n@test 'semver_eq 1.2.3 1.2.3 1.2.3' {\n    succeeds\n}\n@test 'semver_eq 1.2.3 1.2.3 4.5.6' {\n    fails\n}\n\n########################################################################\n\n@test 'semver_neq' {\n    fails\n}\n@test 'semver_neq 1.2.3' {\n    succeeds\n}\n@test 'semver_neq 1.2.3 1.2.3' {\n    fails\n}\n@test 'semver_neq 1.2.3 4.5.6' {\n    succeeds\n}\n@test 'semver_neq 4.5.6 1.2.3' {\n    succeeds\n}\n@test 'semver_neq 1.2.3 4.5.6 1.2.3' {\n    fails\n}\n@test 'semver_neq 1.2.3 4.5.6 7.8.9' {\n    succeeds\n}\n@test 'semver_neq 4.5.6 7.8.9 1.2.3' {\n    succeeds\n}\n\n########################################################################\n\n@test 'semver_lt' {\n    fails\n}\n@test 'semver_lt 1.2.3' {\n    succeeds\n}\n@test 'semver_lt 1.2.3 1.2.3' {\n    fails\n}\n@test 'semver_lt 1.2.3 4.5.6' {\n    succeeds\n}\n@test 'semver_lt 4.5.6 1.2.3' {\n    fails\n}\n@test 'semver_lt 1.2.3 4.5.6 7.8.9' {\n    succeeds\n}\n@test 'semver_lt 1.2.3 4.5.6 4.5.6' {\n    fails\n}\n\n########################################################################\n\n@test 'semver_lte' {\n    fails\n}\n@test 'semver_lte 1.2.3' {\n    succeeds\n}\n@test 'semver_lte 1.2.3 1.2.3' {\n    succeeds\n}\n@test 'semver_lte 1.2.3 4.5.6' {\n    succeeds\n}\n@test 'semver_lte 4.5.6 1.2.3' {\n    fails\n}\n@test 'semver_lte 1.2.3 4.5.6 4.5.6' {\n    succeeds\n}\n@test 'semver_lte 1.2.3 4.5.6 1.2.3' {\n    fails\n}\n\n########################################################################\n\n@test 'semver_gt' {\n    fails\n}\n@test 'semver_gt 1.2.3' {\n    succeeds\n}\n@test 'semver_gt 1.2.3 1.2.3' {\n    fails\n}\n@test 'semver_gt 1.2.3 4.5.6' {\n    fails\n}\n@test 'semver_gt 4.5.6 1.2.3' {\n    succeeds\n}\n@test 'semver_gt 7.8.9 4.5.6 1.2.3' {\n    succeeds\n}\n@test 'semver_gt 7.8.9 4.5.6 4.5.6' {\n    fails\n}\n\n########################################################################\n\n@test 'semver_gte' {\n    fails\n}\n@test 'semver_gte 1.2.3' {\n    succeeds\n}\n@test 'semver_gte 1.2.3 1.2.3' {\n    succeeds\n}\n@test 'semver_gte 1.2.3 4.5.6' {\n    fails\n}\n@test 'semver_gte 4.5.6 1.2.3' {\n    succeeds\n}\n@test 'semver_gte 7.8.9 4.5.6 4.5.6' {\n    succeeds\n}\n@test 'semver_gte 7.8.9 4.5.6 7.8.9' {\n    fails\n}\n\n########################################################################\n\n@test 'this_function' {\n    foo() {\n        this_function\n    }\n    run foo\n    assert_success\n    assert_output foo\n}\n\n@test 'calling_function' {\n    bar() {\n        baz\n    }\n    baz() {\n        calling_function\n    }\n    run bar\n    assert_success\n    assert_output bar\n}\n\n########################################################################\n\n@test 'call_local_function' {\n    local_func() {\n        echo local_func\n    }\n    func() {\n        call_local_function\n    }\n    run func\n    assert_success\n    assert_output local_func\n}\n\n########################################################################\n\n@test 'try returns stdout and stderr together' {\n    run try --max 1 sh -c 'echo foo; echo bar >&2; echo baz'\n    trace \"output=$output\"\n    trace \"stderr=${stderr:-}\"\n    assert_success\n    # output is currently re-ordered that all stderr follows all stdout\n    # this is subject to change\n    assert_line -n 0 foo\n    assert_line -n 2 bar\n    assert_line -n 1 baz\n    output=${stderr:-} assert_output ''\n}\n\n@test 'try supports --separate-stderr' {\n    run --separate-stderr try --max 1 sh -c 'echo foo; echo bar >&2; echo baz'\n    trace \"output=$output\"\n    trace \"stderr=${stderr:-}\"\n    assert_success\n    assert_output $'foo\\nbaz'\n    output=$stderr assert_output bar\n}\n\n@test 'try will run command at least once' {\n    run try --max 0 --delay 15 inc_counter\n    assert_failure\n    assert_counter_is 1\n    # \"try\" should not have called \"sleep 15\" at all\n    ((SECONDS < 15))\n}\n\n@test 'try will stop as soon as the command succeeds' {\n    run try --max 3 --delay 3 inc_counter 2\n    assert_success\n    assert_counter_is 2\n    # \"try\" should have called \"sleep 3\" exactly once\n    ((SECONDS >= 3))\n    if ((SECONDS >= 6)); then\n        # maybe slow machine; try again with longer sleep\n        reset_counter\n        run try --max 3 --delay 15 inc_counter 2\n        assert_success\n        assert_counter_is 2\n        # \"try\" should have called \"sleep 15\" exactly once\n        ((SECONDS >= 15))\n        ((SECONDS < 30))\n    fi\n}\n\n@test 'try will return after max retries' {\n    run try --max 3 --delay 3 inc_counter\n    assert_failure\n    assert_counter_is 3\n    # \"try\" should have called \"sleep 3\" exactly twice\n    ((SECONDS >= 6))\n    if ((SECONDS >= 9)); then\n        # maybe slow machine; try again with longer sleep\n        reset_counter\n        run try --max 3 --delay 15 inc_counter\n        assert_failure\n        assert_counter_is 3\n        # \"try\" should have called \"sleep 15\" exactly twice\n        ((SECONDS >= 30))\n        ((SECONDS < 45))\n    fi\n}\n\n########################################################################\n\n@test 'json_string' {\n    run json_string foo\\ bar\\\"baz\\'\n    assert_success\n    assert_output \"\\\"foo bar\\\\\\\"baz'\\\"\"\n}\n\n########################################################################\n\n@test 'join_map echo' {\n    run join_map / echo usr local bin\n    assert_success\n    assert_output usr/local/bin\n}\n\n@test 'join_map false' {\n    run join_map / false usr local bin\n    assert_failure\n}\n\n@test 'join_map json_string' {\n    run join_map \", \" json_string true \"foo bar\" baz:80\n    assert_success\n    assert_output '\"true\", \"foo bar\", \"baz:80\"'\n}\n\n@test 'join_map empty list' {\n    run join_map / echo\n    assert_success\n    assert_output ''\n}\n\n########################################################################\n\n@test 'image_without_tag_as_json_string busybox' {\n    is_quoted busybox\n}\n\n@test 'image_without_tag_as_json_string busybox:latest' {\n    is_quoted busybox\n}\n\n@test 'image_without_tag_as_json_string busybox:5000' {\n    is_quoted busybox\n}\n\n@test 'image_without_tag_as_json_string registry.io:5000' {\n    is_quoted registry.io:5000\n}\n\n@test 'image_without_tag_as_json_string registry.io:5000/busybox' {\n    is_quoted registry.io:5000/busybox\n}\n\n@test 'image_without_tag_as_json_string registry.io:5000/busybox:8080' {\n    is_quoted registry.io:5000/busybox\n}\n\n########################################################################\n\n@test 'unique_filename without extension' {\n    run unique_filename \"$COUNTER\"\n    assert_success\n    assert_output \"${COUNTER}_2\"\n    touch \"$output\"\n\n    run unique_filename \"$COUNTER\"\n    assert_success\n    assert_output \"${COUNTER}_3\"\n}\n\n@test 'unique_filename with extension' {\n    run unique_filename \"$COUNTER\" .png\n    assert_success\n    assert_output \"${COUNTER}.png\"\n    touch \"$output\"\n\n    run unique_filename \"$COUNTER\" .png\n    assert_success\n    assert_output \"${COUNTER}_2.png\"\n    touch \"$output\"\n\n    run unique_filename \"$COUNTER\" .png\n    assert_success\n    assert_output \"${COUNTER}_3.png\"\n}\n\n########################################################################\n\n@test 'save_var existing variables' {\n    FOO=baz BAR=foo\n    save_var FOO BAR\n}\n\n@test 'load_var existing variables' {\n    # shellcheck disable=SC2030\n    FOO=bar BAR=bar\n    load_var FOO BAR\n    [[ $FOO == baz ]]\n    [[ $BAR == foo ]]\n}\n\n@test 'save_var mix of existing and non-existing variables' {\n    ONE=one TWO=two\n    FAILED=false\n    # Don't use run because it may mask errexit failures\n    save_var ONE DOES_NOT_EXIST TWO || FAILED=true\n    [[ $FAILED == true ]]\n    [[ $ONE == one ]]\n    [[ $TWO == two ]]\n}\n\n@test 'load_var mix of existing and non-existing variables' {\n    DOES_NOT_EXIST=false\n    # Can't use `run` because variable would be sourced in a subshell\n    load_var FOO DOES_NOT_EXIST BAR || DOES_NOT_EXIST=true\n    [[ $DOES_NOT_EXIST == true ]]\n    # shellcheck disable=SC2031\n    [[ $FOO == baz ]]\n    # shellcheck disable=SC2031\n    [[ $BAR == foo ]]\n}\n"
  },
  {
    "path": "bats/tests/helpers/vm.bash",
    "content": "wait_for_shell() {\n    if is_windows; then\n        try --max 48 --delay 5 rdctl shell grep ID= /etc/os-release\n        if using_systemd; then\n            try --max 24 --delay 5 rdctl shell test -f /var/run/lima-boot-done\n            try --max 24 --delay 5 rdctl shell systemctl is-active rancher-desktop.target\n            try --max 48 --delay 5 rdctl shell sudo systemctl is-system-running --wait\n        fi\n    else\n        # Be at the root directory to avoid issues with limactl automatic\n        # changing to the current directory, which might not exist.\n        pushd /\n        try --max 24 --delay 5 rdctl shell test -f /var/run/lima-boot-done\n        # wait until sshfs mounts are done\n        try --max 12 --delay 5 rdctl shell test -d \"$HOME/.rd\"\n        popd || :\n    fi\n}\n\npkill_by_path() {\n    local arg\n    arg=$(readlink -f \"$1\")\n    if [[ -n $arg ]]; then\n        pkill -f \"$arg\"\n    fi\n}\n\nclear_iptables_chain() {\n    local chain=$1\n    local rule\n    wsl sudo iptables -L | awk \"/^Chain ${chain}/ {print \\$2}\" | while IFS= read -r rule; do\n        wsl sudo iptables -X \"$rule\"\n    done\n}\n\nflush_iptables() {\n    # reset default policies\n    wsl sudo iptables -P INPUT ACCEPT\n    wsl sudo iptables -P FORWARD ACCEPT\n    wsl sudo iptables -P OUTPUT ACCEPT\n    wsl sudo iptables -t nat -F\n    wsl sudo iptables -t mangle -F\n    wsl sudo iptables -F\n    wsl sudo iptables -X\n}\n\n# Helper to eject all existing ramdisk instances on macOS\nmacos_eject_ramdisk() {\n    local mount=\"$1\"\n    run hdiutil info -plist\n    assert_success\n    # shellcheck disable=2154 # $output set by `run`\n    run plutil -convert json -o - - <<<\"$output\"\n    assert_success\n    # shellcheck disable=2016 # $mount is interpreted by jq, not shell.\n    local expr='.images[].\"system-entities\"[] | select(.\"mount-point\" == $mount) | .\"dev-entry\"'\n    run jq_output --arg mount \"$mount\" \"$expr\"\n    assert_success\n    if [[ -z $output ]]; then\n        return\n    fi\n    # We don't need to worry about splitting here, it's all /dev/disk*\n    # However, we do need to ensure $output isn't clobbered.\n    # shellcheck disable=2206\n    local disks=($output)\n    local disk\n    for disk in \"${disks[@]}\"; do\n        CALLER=\"$(calling_function):umount\" trace \"$(umount \"$disk\" 2>&1 || :)\"\n    done\n    for disk in \"${disks[@]}\"; do\n        CALLER=\"$(calling_function):hdiutil\" trace \"$(hdiutil eject \"$disk\" 2>&1 || :)\"\n    done\n}\n\n# Set up the use of a ramdisk for application data, to make things faster.\nsetup_ramdisk() {\n    if ! using_ramdisk; then\n        return\n    fi\n\n    # Force eject any existing disks.\n    if is_macos; then\n        # Try to eject the disk, if it already exists.\n        macos_eject_ramdisk \"$LIMA_HOME\"\n    fi\n\n    local ramdisk_size=\"${RD_RAMDISK_SIZE}\"\n    if ((ramdisk_size < ${RD_FILE_RAMDISK_SIZE:-0})); then\n        local fmt='%s requires %dGB of ramdisk; disabling ramdisk for this file'\n        # shellcheck disable=SC2059 # The string is set the line above.\n        printf -v fmt \"$fmt\" \"$BATS_TEST_FILENAME\" \"$RD_FILE_RAMDISK_SIZE\"\n        printf \"RD:   %s\\n\" \"$fmt\" >>\"$BATS_WARNING_FILE\"\n        printf \"# WARN: %s\\n\" \"$fmt\" >&3\n        return\n    fi\n\n    if is_macos; then\n        local sectors=$((ramdisk_size * 1024 * 1024 * 1024 / 512)) # Size, in sectors.\n        # hdiutil space-pads the output; strip it.\n        disk=\"$(hdiutil attach -nomount \"ram://$sectors\" | xargs echo)\"\n        newfs_hfs -v 'Rancher Desktop BATS' \"$disk\"\n        mkdir -p \"$LIMA_HOME\"\n        mount -t hfs \"$disk\" \"$LIMA_HOME\"\n        CALLER=\"$(this_function):hdiutil\" trace \"$(hdiutil info)\"\n        CALLER=\"$(this_function):df\" trace \"$(df -h)\"\n    fi\n}\n\n# Remove any ramdisks\nteardown_ramdisk() {\n    # We run this even if ramdisk is not in use, in case a previous run had\n    # used ramdisk.\n    if is_macos; then\n        CALLER=\"$(this_function):hdiutil\" trace \"$(hdiutil info)\"\n        CALLER=\"$(this_function):df\" trace \"$(df -h)\"\n        macos_eject_ramdisk \"$LIMA_HOME\"\n    fi\n}\n\nfactory_reset() {\n    if [ \"$BATS_TEST_NUMBER\" -gt 1 ]; then\n        capture_logs\n    fi\n\n    if using_dev_mode; then\n        if is_unix; then\n            rdctl shutdown || :\n            pkill_by_path \"$PATH_REPO_ROOT/node_modules\" || :\n            pkill_by_path \"$PATH_RESOURCES\" || :\n            pkill_by_path \"$LIMA_HOME\" || :\n        else\n            # TODO: kill `yarn dev` instance on Windows\n            true\n        fi\n    fi\n    if is_windows && wsl true >/dev/null; then\n        wsl sudo ip link delete docker0 || :\n        wsl sudo ip link delete nerdctl0 || :\n        # reset iptables to original state\n        flush_iptables\n        clear_iptables_chain \"CNI\"\n        clear_iptables_chain \"KUBE\"\n    fi\n    rdctl reset --factory \"$@\"\n    setup_ramdisk\n}\n\n# Turn `rdctl start` arguments into `yarn dev` arguments\napify_arg() {\n    # TODO this should be done via autogenerated code from command-api.yaml\n    perl -w - \"$1\" <<'EOF'\n# don't modify the value part after the first '=' sign\n($_, my $value) = split /=/, shift, 2;\nif (/^--/) {\n    # turn \"--virtual-machine.memory-in-gb\" into \"--virtualMachine.memoryInGb\"\n    s/(\\w)-(\\w)/$1\\U$2/g;\n    # fixup acronyms\n    s/memoryInGb/memoryInGB/;\n    s/numberCpus/numberCPUs/;\n    s/--wsl/--WSL/;\n}\nprint;\nprint \"=$value\" if $value;\nEOF\n}\n\nstart_container_engine() {\n    local args=(\n        --application.debug\n        --application.updater.enabled=false\n        --kubernetes.enabled=false\n    )\n    local admin_access=false\n\n    if [ -n \"$RD_CONTAINER_ENGINE\" ]; then\n        args+=(--container-engine.name=\"$RD_CONTAINER_ENGINE\")\n    fi\n    if is_unix; then\n        args+=(\n            --application.admin-access=\"$admin_access\"\n            --application.path-management-strategy rcfiles\n            --virtual-machine.memory-in-gb 6\n            --virtual-machine.mount.type=\"$RD_MOUNT_TYPE\"\n        )\n    fi\n    if [ \"$RD_MOUNT_TYPE\" = \"9p\" ]; then\n        args+=(\n            --experimental.virtual-machine.mount.9p.cache-mode=\"$RD_9P_CACHE_MODE\"\n            --experimental.virtual-machine.mount.9p.msize-in-kib=\"$RD_9P_MSIZE\"\n            --experimental.virtual-machine.mount.9p.protocol-version=\"$RD_9P_PROTOCOL_VERSION\"\n            --experimental.virtual-machine.mount.9p.security-model=\"$RD_9P_SECURITY_MODEL\"\n        )\n    fi\n    if is_macos; then\n        if using_vz_emulation; then\n            args+=(--virtual-machine.type vz)\n            if is_macos aarch64; then\n                args+=(--virtual-machine.use-rosetta)\n            fi\n        else\n            args+=(--virtual-machine.type qemu)\n        fi\n    fi\n\n    # TODO containerEngine.allowedImages.patterns and WSL.integrations\n    # TODO cannot be set from the commandline yet\n    image_allow_list=\"$(bool using_image_allow_list)\"\n    registry=\"docker.io\"\n    if using_ghcr_images; then\n        registry=\"ghcr.io\"\n    fi\n    if is_true \"${RD_USE_PROFILE:-}\"; then\n        if ! profile_exists; then\n            create_profile\n        fi\n        add_profile_int \"version\" 7\n        if is_windows; then\n            # Translate any dots in the distro name into $RD_PROTECTED_DOT (e.g. \"Ubuntu-22.04\")\n            # so that they are not treated as setting separator characters.\n            add_profile_bool \"WSL.integrations.${WSL_DISTRO_NAME//./$RD_PROTECTED_DOT}\" true\n        fi\n        # TODO Figure out the interaction between RD_USE_PROFILE and RD_USE_IMAGE_ALLOW_LIST!\n        # TODO For now we need to avoid overwriting settings that may already exist in the profile.\n        # add_profile_bool containerEngine.allowedImages.enabled \"$image_allow_list\"\n        # add_profile_list containerEngine.allowedImages.patterns \"$registry\"\n    else\n        local wsl_integrations=\"{}\"\n        if is_windows; then\n            wsl_integrations=\"{\\\"$WSL_DISTRO_NAME\\\":true}\"\n        fi\n        create_file \"$PATH_CONFIG_FILE\" <<EOF\n{\n  \"version\": 7,\n  \"WSL\": { \"integrations\": $wsl_integrations },\n  \"containerEngine\": {\n    \"allowedImages\": {\n      \"enabled\": $image_allow_list,\n      \"patterns\": [\"$registry\"]\n    }\n  }\n}\nEOF\n    fi\n    args+=(\"$@\")\n    launch_the_application \"${args[@]}\"\n}\n\n# shellcheck disable=SC2120\nstart_kubernetes() {\n    start_container_engine \\\n        --kubernetes.enabled \\\n        --kubernetes.version \"$RD_KUBERNETES_VERSION\" \\\n        \"$@\"\n}\n\nstart_application() {\n    start_kubernetes\n    wait_for_kubelet\n\n    # the docker context \"rancher-desktop\" may not have been written\n    # even though the apiserver is already running\n    if using_docker; then\n        wait_for_container_engine\n    fi\n}\n\nlaunch_the_application() {\n    local args=(\"$@\")\n    trace \"$*\"\n\n    if using_dev_mode; then\n        # translate args back into the internal API format\n        local api_args=()\n        for arg in \"${args[@]}\"; do\n            api_args+=(\"$(apify_arg \"$arg\")\")\n        done\n        if suppressing_modal_dialogs; then\n            # Don't apify this option\n            api_args+=(--no-modal-dialogs)\n        fi\n\n        yarn dev \"${api_args[@]}\" &\n    else\n        # Detach `rdctl start` because on Windows the process may not exit until\n        # Rancher Desktop itself quits.\n        if suppressing_modal_dialogs; then\n            args+=(--no-modal-dialogs)\n        fi\n        RD_TEST=bats rdctl start \"${args[@]}\" &\n    fi\n}\n\n# Write a provisioning script that will be executed during VM startup.\n# Only a single script can be defined, and scripts are deleted by factory-reset.\n# The script must be provided via STDIN and not as a parameter.\nprovisioning_script() {\n    if is_windows; then\n        mkdir -p \"$PATH_APP_HOME/provisioning\"\n        cat >\"$PATH_APP_HOME/provisioning/bats.start\"\n    else\n        mkdir -p \"$LIMA_HOME/_config\"\n        cat <<EOF >\"$LIMA_HOME/_config/override.yaml\"\nprovision:\n- mode: system\n  script: |\n$(sed 's/^/    /')\nEOF\n    fi\n}\n\nget_container_engine_info() {\n    run ctrctl info\n    echo \"$output\"\n    assert_success\n    assert_output --partial \"Server Version:\"\n}\n\ndocker_context_exists() {\n    # We don't use docker contexts on Windows\n    if is_windows; then\n        return\n    fi\n    run docker_exe context ls -q\n    assert_success\n    assert_line \"$RD_DOCKER_CONTEXT\"\n    # Ensure that the context actually exists by reading from the file.\n    run docker_exe context inspect \"$RD_DOCKER_CONTEXT\" --format '{{ .Name }}'\n    assert_success\n    assert_output \"$RD_DOCKER_CONTEXT\"\n}\n\n# Check if the VM is using systemd (instead of OpenRC).\nusing_systemd() {\n    [[ -n ${_RD_USING_SYSTEMD:-} ]] || load_var _RD_USING_SYSTEMD || true\n    if [[ -z ${_RD_USING_SYSTEMD:-} ]]; then\n        # `systemctl whoami` contacts the systemd init to check things, so if\n        # it succeeds we're using systemd.  On alpine-based systems, the\n        # `systemctl` command would be missing so this still applies.\n        if rdctl shell /usr/bin/systemctl whoami &>/dev/null; then\n            _RD_USING_SYSTEMD=true\n        else\n            _RD_USING_SYSTEMD=false\n        fi\n        save_var _RD_USING_SYSTEMD\n    fi\n    is_true \"${_RD_USING_SYSTEMD}\"\n}\n\n# Manage a service in the vm.\n# service_control [--ifstarted] $SERVICE start|stop|restart\nservice_control() {\n    local if_started\n    if [[ ${1:-} == \"--ifstarted\" ]]; then\n        if_started=$1\n        shift\n    fi\n    local service=$1 action=$2\n    if using_systemd; then\n        if [[ -n $ifstarted && $action == restart ]]; then\n            rdsudo systemctl try-restart \"$service\"\n        else\n            rdsudo systemctl \"$action\" \"$service\"\n        fi\n    else\n        # shellcheck disable=2086 # the argument may expand to nothing.\n        rdsudo rc-service ${if_started:-} \"$service\" \"$action\"\n    fi\n}\n\nget_service_pid() {\n    local service_name=$1\n    if using_systemd; then\n        RD_TIMEOUT=10s run rdshell systemctl show --property MainPID --value \"$service_name.service\"\n        assert_success\n        echo \"$output\"\n    else\n        RD_TIMEOUT=10s run rdshell sh -c \"RC_SVCNAME=$service_name /usr/libexec/rc/bin/service_get_value pidfile\"\n        assert_success\n        RD_TIMEOUT=10s rdshell cat \"$output\"\n    fi\n}\n\nassert_service_pid() {\n    local service_name=$1\n    local expected_pid=$2\n    run get_service_pid \"$service_name\"\n    assert_success\n    assert_output \"$expected_pid\"\n}\n\n# Check that the given service does not have the given PID.  It is acceptable\n# for the service to not be running.\nrefute_service_pid() {\n    local service_name=$1\n    local unexpected_pid=$2\n    run get_service_pid \"$service_name\"\n    if [ \"$status\" -eq 0 ]; then\n        refute_output \"$unexpected_pid\"\n    fi\n}\n\nassert_service_status() {\n    local service_name=$1\n    local expect=$2\n\n    if using_systemd; then\n        local mapped_status\n        case $expect in\n        started) mapped_status=active ;;\n        stopped) mapped_status=inactive ;;\n        *) fail \"Status $expect is unsupported\" ;;\n        esac\n        RD_TIMEOUT=10s run rdsudo systemctl is-active \"$service_name\"\n        # `systemctl is-active` returns 0 on active, and non-0 on non-active.\n        if [[ $expect == started ]]; then\n            assert_success\n        fi\n        assert_line \"$mapped_status\"\n    else\n        RD_TIMEOUT=10s run rdsudo rc-service \"$service_name\" status\n        # rc-service report non-zero status (3) when the service is stopped\n        if [[ $expect == started ]]; then\n            assert_success\n        fi\n        assert_output --partial \"status: ${expect}\"\n    fi\n}\n\nwait_for_service_status() {\n    local service_name=$1\n    local expect=$2\n\n    trace \"waiting for VM to be available\"\n    wait_for_shell\n\n    trace \"waiting for ${service_name} to be ${expect}\"\n    try --max 30 --delay 5 assert_service_status \"$service_name\" \"$expect\"\n}\n\nwait_for_container_engine() {\n    local CALLER\n    CALLER=$(this_function)\n\n    trace \"waiting for api /settings to be callable\"\n    RD_TIMEOUT=10s try --max 30 --delay 5 rdctl api /settings\n\n    if using_docker; then\n        wait_for_service_status docker started\n        trace \"waiting for docker context to exist\"\n        try --max 30 --delay 10 docker_context_exists\n    else\n        wait_for_service_status buildkitd started\n    fi\n\n    trace \"waiting for container engine info to be available\"\n    try --max 12 --delay 10 get_container_engine_info\n}\n\n# Wait fot the extension manager to be initialized.\nwait_for_extension_manager() {\n    trace \"waiting for extension manager to be ready\"\n    # We want to match specific error strings, so we can't use try() directly.\n    local count=0 max=30 message\n    while true; do\n        run --separate-stderr rdctl api /extensions\n        if ((status == 0 || ++count >= max)); then\n            break\n        fi\n        message=$(jq_output .message)\n        output=\"$message\" assert_output \"503 Service Unavailable\"\n        sleep 10\n    done\n    trace \"$count/$max tries: wait_for_extension_manager\"\n}\n\n# See definition of `State` in\n# pkg/rancher-desktop/backend/backend.ts for an explanation of each state.\nassert_backend_available() {\n    RD_TIMEOUT=10s run rdctl api /v1/backend_state\n    if ((status == 0)); then\n        run jq_output .vmState\n        case \"$output\" in\n        ERROR) return 0 ;;\n        STARTED) return 0 ;;\n        DISABLED) return 0 ;;\n        esac\n    fi\n    return 1\n}\n\nwait_for_backend() {\n    trace \"waiting for backend to be available\"\n    try --max 60 --delay 10 assert_backend_available\n}\n"
  },
  {
    "path": "bats/tests/k8s/enable-disable-k8s.bats",
    "content": "# Test case 8, 13, 22\n\nload '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\nverify_k8s_is_running() {\n    wait_for_container_engine\n    wait_for_service_status k3s started\n}\n\n@test 'start rancher desktop with kubernetes enabled' {\n    start_kubernetes\n    wait_for_kubelet\n    verify_k8s_is_running\n}\n\n@test 'disable kubernetes' {\n    rdctl set --kubernetes.enabled=false\n    wait_for_container_engine\n    wait_for_service_status k3s stopped\n}\n\n@test 're-enable kubernetes' {\n    rdctl set --kubernetes.enabled=true\n    wait_for_kubelet\n    verify_k8s_is_running\n}\n"
  },
  {
    "path": "bats/tests/k8s/foreach-k3s-version.bats",
    "content": "load '../helpers/load'\n\nwait_for_dns() {\n    try assert_pod_containers_are_running \\\n        --namespace kube-system \\\n        --selector k8s-app=kube-dns\n}\n\nforeach_k3s_version \\\n    factory_reset \\\n    start_kubernetes \\\n    wait_for_kubelet \\\n    wait_for_dns\n"
  },
  {
    "path": "bats/tests/k8s/helm-install-rancher.bats",
    "content": "# Test case 11 & 12\n# bats file_tags=opensuse\n\nload '../helpers/load'\nRD_FILE_RAMDISK_SIZE=12 # We need more disk to run the Rancher image.\n\nlocal_setup() {\n    needs_port 443\n}\n\n# Check that the rancher-latest/rancher helm chart at the given version is\n# supported on the current Kubernetes version (as determined by\n# $RD_KUBERNETES_VERSION)\nis_rancher_chart_compatible() {\n    local chart_version=$1\n\n    run helm show chart rancher-latest/rancher --version \"$chart_version\"\n    assert_success\n\n    run awk '/^kubeVersion:/ { $1 = \"\"; print }' <<<\"$output\"\n    assert_success\n    # We only support kubeVersion of form \"< x.y.z\"\n    assert_output --regexp '^[[:space:]]*<[[:space:]]*[^[:space:]]+$'\n\n    run awk '{ print $2 }' <<<\"$output\"\n    assert_success\n\n    local unsupported_version=$output\n    semver_gt \"$unsupported_version\" \"$RD_KUBERNETES_VERSION\"\n}\n\n# Set (and save) $rancher_chart_version to $RD_RANCHER_IMAGE_TAG if it is set\n# (and compatible), or otherwise the oldest chart version that supports\n# $RD_KUBERNETES_VERSION.\n# If no compatible chart version could be found, calls mark_k3s_version_skipped\n# and fails the test.\ndetermine_chart_version() {\n    local rancher_chart_version\n    if [[ -n $RD_RANCHER_IMAGE_TAG ]]; then\n        # If a version is given, check that it's compatible.\n        rancher_chart_version=${RD_RANCHER_IMAGE_TAG#v}\n        if ! is_rancher_chart_compatible \"$rancher_chart_version\"; then\n            mark_k3s_version_skipped\n            printf \"Rancher %s is not compatible with Kubernetes %s\" \\\n                \"$rancher_chart_version\" \"$RD_KUBERNETES_VERSION\" |\n                fail\n            return\n        fi\n        save_var rancher_chart_version\n        return\n    fi\n    local default_version\n    default_version=$(rancher_image_tag)\n    default_version=${default_version#v}\n\n    run --separate-stderr helm search repo --versions rancher-latest/rancher --output json\n    assert_success\n\n    run jq_output 'map(.version).[]'\n    assert_success\n\n    run sort --version-sort <<<\"$output\"\n    assert_success\n    local versions=$output\n\n    for rancher_chart_version in $versions; do\n        if ! semver_is_valid \"$rancher_chart_version\"; then\n            continue # Skip invalid / RC versions.\n        fi\n        if semver_lt \"$rancher_chart_version\" \"$default_version\"; then\n            continue # Skip any versions older than the default version\n        fi\n        if is_rancher_chart_compatible \"$rancher_chart_version\"; then\n            # Once we find a compatible version, use it (and don't look at the\n            # rest of the chart versions).\n            trace \"$(printf \"Selected rancher chart version %s for Kubernetes %s\" \\\n                \"$rancher_chart_version\" \"$RD_KUBERNETES_VERSION\")\"\n            save_var rancher_chart_version\n            return\n        fi\n    done\n    mark_k3s_version_skipped\n    printf \"Could not find a version of rancher-latest/rancher compatible with Kubernetes %s\\n\" \\\n        \"$RD_KUBERNETES_VERSION\" |\n        fail\n}\n\ndeploy_rancher() {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local rancher_chart_version\n    if ! load_var rancher_chart_version; then\n        fail \"Could not restore Rancher chart version\"\n    fi\n\n    helm upgrade \\\n        --install cert-manager oci://quay.io/jetstack/charts/cert-manager \\\n        --namespace cert-manager \\\n        --set installCRDs=true \\\n        --set \"extraArgs[0]=--enable-certificate-owner-ref=true\" \\\n        --create-namespace\n\n    local host\n    host=$(traefik_hostname)\n\n    comment \"Installing rancher $rancher_chart_version\"\n    helm upgrade \\\n        --install rancher rancher-latest/rancher \\\n        --version \"$rancher_chart_version\" \\\n        --namespace cattle-system \\\n        --set hostname=\"$host\" \\\n        --wait \\\n        --timeout=10m \\\n        --create-namespace\n}\n\nverify_rancher() {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    run try --max 9 --delay 10 curl --insecure --silent --show-error \"https://${host}/dashboard/auth/login\"\n    assert_success\n    assert_output --partial 'src=\"/dashboard/'\n    run kubectl get secret --namespace cattle-system bootstrap-secret -o json\n    assert_success\n    assert_output --partial \"bootstrapPassword\"\n}\n\nuninstall_rancher() {\n    run helm uninstall rancher --namespace cattle-system --wait\n    assert_nothing\n    run helm uninstall cert-manager --namespace cert-manager --wait\n    assert_nothing\n}\n\n@test 'add helm repo' {\n    helm repo add jetstack https://charts.jetstack.io\n    helm repo add rancher-latest https://releases.rancher.com/server-charts/latest\n    helm repo update\n}\n\nforeach_k3s_version \\\n    determine_chart_version \\\n    factory_reset \\\n    start_kubernetes \\\n    wait_for_kubelet \\\n    wait_for_traefik \\\n    deploy_rancher \\\n    verify_rancher \\\n    uninstall_rancher\n"
  },
  {
    "path": "bats/tests/k8s/port-forwarding.bats",
    "content": "load '../helpers/load'\n\n@test 'start k8s' {\n    factory_reset\n    start_kubernetes\n    wait_for_kubelet\n}\n\n@test 'deploy sample app' {\n    kubectl apply --filename - <<EOF\napiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: webapp-configmap\ndata:\n  index: \"Hello World!\"\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: webapp\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: webapp\n  template:\n    metadata:\n      labels:\n        app: webapp\n    spec:\n      volumes:\n      - name: webapp-config-volume\n        configMap:\n          name: webapp-configmap\n          items:\n          - key: index\n            path: index.html\n      containers:\n      - name: webapp\n        image: $IMAGE_NGINX\n        volumeMounts:\n        - name: webapp-config-volume\n          mountPath: /usr/share/nginx/html\nEOF\n}\n\n@test 'deploy ingress' {\n    kubectl apply --filename - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n  name: webapp\nspec:\n  type: ClusterIP\n  selector:\n    app: webapp\n  ports:\n  - port: 80\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: webapp\n  annotations:\n    traefik.ingress.kubernetes.io/router.entrypoints: web\nspec:\n  rules:\n  - host: localhost\n    http:\n      paths:\n        - path: /\n          pathType: Prefix\n          backend:\n            service:\n              name: webapp\n              port:\n                number: 80\nEOF\n}\n\n@test 'fail to connect to the service on localhost without port forwarding' {\n    run try --max 5 curl --silent --fail \"http://localhost:8080\"\n    assert_failure\n}\n\n@test 'connect to the service on localhost with port forwarding' {\n    rdctl api -X POST -b '{ \"namespace\": \"default\", \"service\": \"webapp\", \"k8sPort\": 80, \"hostPort\": 8080 }' port_forwarding\n    run try curl --silent --fail \"http://localhost:8080\"\n    assert_success\n    assert_output \"Hello World!\"\n}\n\n@test 'fail to connect to the service on localhost after removing port forwarding' {\n    rdctl api -X DELETE \"port_forwarding?namespace=default&service=webapp&k8sPort=80\"\n    run try --max 5 curl --silent --fail \"http://localhost:8080\"\n    assert_failure\n}\n"
  },
  {
    "path": "bats/tests/k8s/specify-invalid-k8s-version.bats",
    "content": "load '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'invalid k8s version' {\n    start_kubernetes --kubernetes.version=moose\n    wait_for_container_engine\n    # Can't use wait_for_api_server because it hard-wires a valid k8s version and we're specifying an invalid one here.\n    # and we're specifying an invalid one here\n    local timeout=\"$(($(date +%s) + 10 * 60))\"\n    until kubectl get --raw /readyz &>/dev/null; do\n        assert [ \"$(date +%s)\" -lt \"$timeout\" ]\n        sleep 1\n    done\n    # No way there's a race-condition here.\n    # The version was checked and written to the log file before starting k8s,\n    # and we have to wait a few minutes before k8s is ready and we're at the next line.\n    assert_file_contains \"$PATH_LOGS/kube.log\" \"Requested kubernetes version 'moose' is not a supported version. Falling back to\"\n}\n\n# on macOS it still hangs without this\n@test 'shutdown' {\n    if is_macos; then\n        rdctl shutdown\n    fi\n}\n"
  },
  {
    "path": "bats/tests/k8s/spinkube-npm.bats",
    "content": "load '../helpers/load'\n\nlocal_setup_file() {\n    echo \"$RANDOM\" >\"${BATS_FILE_TMPDIR}/random\"\n}\n\nlocal_setup() {\n    if using_docker; then\n        skip \"this test only works on containerd right now\"\n    fi\n    if ! command -v \"npm${EXE}\" >/dev/null; then\n        skip \"this test requires npm${EXE} to be installed and on the PATH\"\n    fi\n    needs_port 80\n\n    MY_APP=my-app\n    MY_APP_NAME=\"${MY_APP}-$(cat \"${BATS_FILE_TMPDIR}/random\")\"\n    MY_APP_IMAGE=\"ttl.sh/${MY_APP_NAME}:15m\"\n}\n\n@test 'start k8s with spinkube' {\n    factory_reset\n    start_kubernetes \\\n        --experimental.container-engine.web-assembly.enabled \\\n        --experimental.kubernetes.options.spinkube\n    wait_for_kubelet\n    wait_for_traefik\n}\n\n@test 'create sample application' {\n    cd \"$BATS_FILE_TMPDIR\"\n    spin new --accept-defaults --template http-js \"$MY_APP\"\n    cd \"$MY_APP\"\n    \"npm${EXE}\" install\n    spin build\n    spin registry push \"$MY_APP_IMAGE\"\n}\n\n@test 'wait for spinkube operator' {\n    wait_for_kube_deployment_available --namespace spin-operator spin-operator-controller-manager\n}\n\n@test 'deploy app to kubernetes' {\n    spin kube deploy --context rancher-desktop --from \"$MY_APP_IMAGE\"\n}\n\n# TODO replace ingress with port-forwarding\n@test 'deploy ingress' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    kubectl apply --filename - <<EOF\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: \"${MY_APP_NAME}\"\n  annotations:\n    traefik.ingress.kubernetes.io/router.entrypoints: web\nspec:\n  rules:\n  - host: \"${host}\"\n    http:\n      paths:\n        - path: /\n          pathType: Prefix\n          backend:\n            service:\n              name: \"${MY_APP_NAME}\"\n              port:\n                number: 80\nEOF\n}\n\n@test 'connect to app on localhost' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    run --separate-stderr try curl --connect-timeout 5 --fail \"http://${host}\"\n    assert_success\n    assert_output --regexp '^(Hello|hello)'\n}\n"
  },
  {
    "path": "bats/tests/k8s/spinkube.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    if using_docker; then\n        skip \"this test only works on containerd right now\"\n    fi\n    needs_port 80\n}\n\n@test 'start k8s with spinkube' {\n    factory_reset\n    start_kubernetes \\\n        --experimental.container-engine.web-assembly.enabled \\\n        --experimental.kubernetes.options.spinkube\n    wait_for_kubelet\n    wait_for_traefik\n}\n\n@test 'wait for spinkube operator' {\n    wait_for_kube_deployment_available --namespace spin-operator spin-operator-controller-manager\n}\n\n@test 'wait for spinkube executor' {\n    try kubectl get SpinAppExecutors.core.spinkube.dev/containerd-shim-spin\n}\n\n@test 'deploy app to kubernetes' {\n    # Newer versions of the sample app have moved from \"deislabs\" to \"spinkube\":\n    # ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0\n    spin kube deploy --context rancher-desktop --from ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0\n}\n\n# TODO replace ingress with port-forwarding\n@test 'deploy ingress' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    kubectl apply --filename - <<EOF\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: spin-rust-hello\n  annotations:\n    traefik.ingress.kubernetes.io/router.entrypoints: web\nspec:\n  rules:\n  - host: \"${host}\"\n    http:\n      paths:\n        - path: /\n          pathType: Prefix\n          backend:\n            service:\n              name: spin-rust-hello\n              port:\n                number: 80\nEOF\n}\n\n@test 'connect to app on localhost' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    run --separate-stderr try curl --connect-timeout 5 --fail \"http://${host}/hello\"\n    assert_success\n    assert_output \"Hello world from Spin!\"\n}\n\n@test 'disable spinkube and traefik' {\n    local k3s_pid\n    k3s_pid=$(get_service_pid k3s)\n\n    trace \"Disable spinkube operator and traefik\"\n    rdctl set \\\n        --experimental.kubernetes.options.spinkube=false \\\n        --kubernetes.options.traefik=false\n\n    trace \"Wait until k3s has restarted\"\n    try --max 30 --delay 5 refute_service_pid k3s \"${k3s_pid}\"\n    wait_for_kubelet\n}\n\nassert_helm_charts_are_deleted() {\n    run --separate-stderr kubectl get helmcharts --namespace kube-system\n    assert_success\n    refute_line traefik\n    refute_line spin-operator\n    refute_line cert-manager\n}\n\n@test 'verify that spinkube and traefik have been uninstalled' {\n    try assert_helm_charts_are_deleted\n}\n"
  },
  {
    "path": "bats/tests/k8s/traefik.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    if is_windows && ! using_windows_exe; then\n        # BUG BUG BUG not yet implemented\n        skip \"Test does not yet work from inside a WSL distro, since it requires WSL integration\"\n    fi\n    needs_port 80\n}\n\nassert_traefik_pods_are_down() {\n    local traefik_pods pods count\n    run --separate-stderr kubectl get --all-namespaces --output 'jsonpath={.items}' pods\n    assert_success\n\n    # There should be at least one pod (e.g. coredns, metrics server, ...)\n    if [[ \"$(jq_output length)\" -eq 0 ]]; then\n        trace \"No pods found\"\n        return 1\n    fi\n\n    # Filter for traefik related pods\n    traefik_pods=$(jq_output 'map(select(.metadata.name | contains(\"traefik\")))')\n\n    # Exclude pods that are completed (i.e. jobs)\n    pods=$(output=$traefik_pods jq_output 'map(select(.status.conditions | all(.reason != \"PodCompleted\")))')\n\n    count=\"$(output=$pods jq_output length)\"\n    if [[ $count -gt 0 ]]; then\n        trace \"Found $count active traefik pods\"\n        return 1\n    fi\n\n    trace \"No active traefik pods\"\n    return 0\n}\n\nassert_traefik_pods_are_up() {\n    ip_regex=\"^([0-9]{1,3}\\.){3}[0-9]{1,3}$\"\n    run kubectl -n kube-system get service traefik -o jsonpath=\"{.status.loadBalancer.ingress[0].ip}\"\n    [[ $output =~ $ip_regex ]]\n}\n\nassert_curl() {\n    try --max 30 --delay 10 curl --silent --head \"$@\"\n    assert_success\n    assert_output --regexp 'HTTP/[0-9.]* 404'\n}\n\nrefute_curl() {\n    run curl --head \"$@\"\n    assert_output --partial \"curl: (7) Failed to connect\"\n}\n\nassert_traefik() {\n    assert_curl \"http://$1:80\"\n    assert_curl --insecure \"https://$1:443\"\n}\n\nrefute_traefik() {\n    refute_curl \"http://$1:80\"\n    refute_curl --insecure \"https://$1:443\"\n}\n\nassert_traefik_on_localhost() {\n    if is_windows && ! using_windows_exe; then\n        # BUG BUG BUG not yet implemented\n        skip \"Test does not yet work from inside a WSL distro\"\n    fi\n    try --max 10 assert_traefik localhost\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start k8s' {\n    start_kubernetes --kubernetes.options.traefik=true\n    wait_for_kubelet\n}\n\n@test 'disable traefik' {\n    # First check whether the traefik pods are up from the first launch\n    try --max 30 --delay 10 assert_traefik_pods_are_up\n\n    local k3s_pid\n    k3s_pid=$(get_service_pid k3s)\n\n    trace \"Disable traefik\"\n    rdctl set --kubernetes.options.traefik=false\n\n    trace \"Wait until k3s has restarted\"\n    try --max 30 --delay 5 refute_service_pid k3s \"${k3s_pid}\"\n    wait_for_kubelet\n\n    trace \"Check if the traefik pods go down\"\n    try --max 30 --delay 10 assert_traefik_pods_are_down\n}\n\n@test 'no connection on localhost' {\n    try --max 10 refute_traefik localhost\n}\n\n@test 'no connection on host-ip' {\n    skip_unless_host_ip\n    try --max 10 refute_traefik \"$HOST_IP\"\n}\n\n@test 'enable traefik' {\n    local k3s_pid\n    k3s_pid=$(get_service_pid k3s)\n\n    trace \"Enable traefik\"\n    rdctl set --kubernetes.options.traefik\n\n    trace \"Wait until k3s has restarted\"\n    try --max 30 --delay 5 refute_service_pid k3s \"${k3s_pid}\"\n    wait_for_kubelet\n\n    trace \"Check if the traefik pods come up\"\n    try --max 30 --delay 10 assert_traefik_pods_are_up\n}\n\n@test 'curl traefik via localhost' {\n    assert_traefik_on_localhost\n}\n\n@test 'curl traefik via host-ip while kubernetes.ingress.localhost-only is false' {\n    skip_unless_host_ip\n    try --max 10 assert_traefik \"$HOST_IP\"\n}\n\n@test 'set kubernetes.ingress.localhost-only to true' {\n    skip_unless_host_ip\n    if ! is_windows; then\n        skip \"kubernetes.ingress.localhost-only is a Windows-only setting\"\n    fi\n    rdctl set --kubernetes.options.traefik --kubernetes.ingress.localhost-only\n    wait_for_kubelet\n    # Check if the traefik pods come up\n    try --max 30 --delay 10 assert_traefik_pods_are_up\n}\n\n@test 'curl traefik via localhost while kubernetes.ingress.localhost-only is true' {\n    if ! is_windows; then\n        skip \"Test requires kubernetes.ingress.localhost-only to be true\"\n    fi\n    assert_traefik_on_localhost\n}\n\n@test 'curl traefik via host-ip while kubernetes.ingress.localhost-only is true' {\n    if ! is_windows; then\n        skip \"Test requires kubernetes.ingress.localhost-only to be true\"\n    fi\n    skip_unless_host_ip\n\n    # traefik should not be accessible on other interface\n    try --max 10 refute_traefik \"$HOST_IP\"\n}\n"
  },
  {
    "path": "bats/tests/k8s/up-downgrade-k8s.bats",
    "content": "# Test cases 8, 13, 19\n\nload '../helpers/load'\n\nlocal_setup_file() {\n    if semver_eq \"$RD_KUBERNETES_VERSION\" \"$RD_KUBERNETES_ALT_VERSION\"; then\n        printf \"Cannot upgrade from %s to %s\\n\" \\\n            \"$RD_KUBERNETES_VERSION\" \"$RD_KUBERNETES_ALT_VERSION\" |\n            fail\n    fi\n    # It is undefined whether RD_KUBERNETES_VERSION is greater or less than\n    # RD_KUBERNETES_ALT_VERSION (and it's expected to flip in CI); actually\n    # compare them so we can expect to wipe data on the downgrade.\n    if semver_gt \"$RD_KUBERNETES_VERSION\" \"$RD_KUBERNETES_ALT_VERSION\"; then\n        export RD_KUBERNETES_VERSION_LOW=$RD_KUBERNETES_ALT_VERSION\n        export RD_KUBERNETES_VERSION_HIGH=$RD_KUBERNETES_VERSION\n    else\n        export RD_KUBERNETES_VERSION_LOW=$RD_KUBERNETES_VERSION\n        export RD_KUBERNETES_VERSION_HIGH=$RD_KUBERNETES_ALT_VERSION\n    fi\n    case \"$(uname -m)\" in\n    amd64 | x86_64 | i*86) export ARCH_FOR_KUBERLR=amd64 ;;\n    arm*) export ARCH_FOR_KUBERLR=arm64 ;;\n    *) printf \"Unsupported architecture %s\\n\" \"$(uname -m)\" | fail ;;\n    esac\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start rancher desktop' {\n    # Force use the pre-upgrade version\n    RD_KUBERNETES_VERSION=$RD_KUBERNETES_VERSION_LOW start_kubernetes\n    wait_for_kubelet \"$RD_KUBERNETES_VERSION_LOW\"\n    # the docker context \"rancher-desktop\" may not have been written\n    # even though the apiserver is already running\n    wait_for_container_engine\n}\n\n@test 'deploy nginx - always restart' {\n    ctrctl pull --quiet \"$IMAGE_NGINX\"\n    run ctrctl run -d -p 8585:80 --restart=always --name nginx-restart \"$IMAGE_NGINX\"\n    assert_success\n}\n\n@test 'deploy nginx - no restart' {\n    run ctrctl run -d -p 8686:80 --restart=no --name nginx-no-restart \"$IMAGE_NGINX\"\n    assert_success\n}\n\n@test 'deploy busybox' {\n    run kubectl create deploy busybox --image=\"$IMAGE_BUSYBOX\" --replicas=2 -- /bin/sh -c \"sleep inf\"\n    assert_success\n}\n\nverify_nginx() {\n    for port in 8585 8686; do\n        run curl \"http://localhost:$port\"\n        assert_success\n        assert_output --partial \"Welcome to nginx!\"\n    done\n}\n\n@test 'verify nginx before upgrade' {\n    try verify_nginx\n}\n\nverify_busybox() {\n    run kubectl get pods --selector=\"app=busybox\" -o jsonpath='{.items[*].status.phase}'\n    assert_output --partial \"Running Running\"\n}\n\n@test 'verify busybox before upgrade' {\n    try verify_busybox\n}\n\nverify_images() {\n    if using_docker; then\n        run docker images --format '{{.Repository}}'\n        assert_line \"$IMAGE_NGINX\"\n        assert_line \"$IMAGE_BUSYBOX\"\n    else\n        run nerdctl images --format '{{.Repository}}'\n        assert_line \"$IMAGE_NGINX\"\n        run nerdctl --namespace k8s.io images --format '{{.Repository}}'\n        assert_line \"$IMAGE_BUSYBOX\"\n    fi\n}\n@test 'verify images before upgrade' {\n    verify_images\n}\n\n# Remove all the kubectl clients from the .kuberlr directory.\n# Then run `kubectl`, and it should pull in the `kubectl` for\n# the current k8s version in that directory.\n\nverify_kuberlr_for_version() {\n    local K8S_VERSION=$1\n    local KUBERLR_DIR=\"${USERPROFILE}/.kuberlr/${OS}-${ARCH_FOR_KUBERLR}\"\n\n    rm -f \"${KUBERLR_DIR}/kubectl\"*\n    run kubectl version\n    assert_output --regexp \"Client Version.*:.v${K8S_VERSION}\"\n    assert_exists \"${KUBERLR_DIR}/kubectl${K8S_VERSION}$EXE\"\n}\n\n@test 'upgrade kubernetes' {\n    rdctl set --kubernetes.version \"$RD_KUBERNETES_VERSION_HIGH\"\n    wait_for_kubelet \"$RD_KUBERNETES_VERSION_HIGH\"\n    wait_for_container_engine\n}\n\n@test 'kuberlr pulls in kubectl for new k8s version' {\n    verify_kuberlr_for_version \"$RD_KUBERNETES_VERSION_HIGH\"\n}\n\nverify_nginx_after_change_k8s() {\n    run curl http://localhost:8686\n    assert_failure\n    assert_output --partial \"Failed to connect to localhost port 8686\"\n\n    run curl http://localhost:8585\n    assert_success\n    assert_output --partial \"Welcome to nginx!\"\n}\n\n@test 'verify nginx after upgrade' {\n    try verify_nginx_after_change_k8s\n}\n\n@test 'verify busybox after upgrade' {\n    try verify_busybox\n}\n\n@test 'verify images after upgrade' {\n    verify_images\n}\n\n@test 'restart nginx-no-restart before downgrade' {\n    if using_docker; then\n        run docker start nginx-no-restart\n        assert_success\n    else\n        # BUG BUG BUG\n        # After restarting the VM nerdctl fails to restart stopped containers.\n        # It will eventually succeed after retrying multiple times (typically twice).\n        # See https://github.com/containerd/nerdctl/issues/665#issuecomment-1372862742\n        # BUG BUG BUG\n        try nerdctl start nginx-no-restart\n    fi\n    try verify_nginx\n}\n\n@test 'downgrade kubernetes' {\n    rdctl set --kubernetes.version \"$RD_KUBERNETES_VERSION_LOW\"\n    wait_for_kubelet \"$RD_KUBERNETES_VERSION_LOW\"\n    wait_for_container_engine\n}\n\n@test 'kuberlr pulls in kubectl for previous k8s version' {\n    verify_kuberlr_for_version \"$RD_KUBERNETES_VERSION_LOW\"\n}\n\n@test 'verify nginx after downgrade' {\n    # nginx should still be running because it is not managed by kubernetes\n    try verify_nginx_after_change_k8s\n}\n\n@test 'verify busybox is gone after downgrade' {\n    run kubectl get pods --selector=\"app=busybox\"\n    assert_output --partial \"No resources found\"\n}\n\n@test 'verify images after downgrade' {\n    verify_images\n}\n\nlocal_teardown_file() {\n    run ctrctl rm -f nginx-restart nginx-no-restart\n    assert_nothing\n    run kubectl delete --selector=\"app=busybox\"\n    assert_nothing\n}\n"
  },
  {
    "path": "bats/tests/k8s/wasm.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    if using_docker; then\n        skip \"this test only works on containerd right now\"\n    fi\n}\n\n# Get Kubernetes RuntimeClasses; sets $output to the JSON list.\nget_runtime_classes() {\n    # kubectl may emit warnings here; ensure that we don't fall over.\n    run --separate-stderr kubectl get RuntimeClasses --output json\n    assert_success\n\n    if [[ -n $stderr ]]; then\n        # Check that we got a deprecation warning:\n        # Warning: node.k8s.io/v1beta1 RuntimeClass is deprecated in v1.22+, unavailable in v1.25+\n        output=$stderr assert_output --partial deprecated\n    fi\n\n    local rtc=$output\n    run jq '.items | length' <<<\"$rtc\"\n    assert_success\n    ((output > 0))\n    echo \"$rtc\"\n}\n\ncreate_bats_runtimeclass() {\n    provisioning_script <<EOF\nmkdir -p /var/lib/rancher/k3s/server/manifests\ncat <<YAML >/var/lib/rancher/k3s/server/manifests/zzzz-bats.yaml\napiVersion: node.k8s.io/v1\nkind: RuntimeClass\nmetadata:\n  name: bats\nhandler: bats\nYAML\nEOF\n}\n\n@test 'start k8s without wasm support' {\n    factory_reset\n    create_bats_runtimeclass\n    start_kubernetes\n    wait_for_kubelet\n}\n\n@test 'verify no runtimeclasses have been defined' {\n    run try get_runtime_classes\n    assert_success\n\n    run jq_output --raw-output '.items[0].metadata.name'\n    assert_success\n    assert_output 'bats'\n}\n\n@test 'start k8s with wasm support' {\n    # TODO We should enable the wasm feature on a running app to make sure the\n    # TODO runtime class is defined even after k3s is initially installed.\n    factory_reset\n    create_bats_runtimeclass\n    start_kubernetes --experimental.container-engine.web-assembly.enabled\n    wait_for_kubelet\n    wait_for_traefik\n}\n\n@test 'verify spin runtime class has been defined (and no others)' {\n    run try get_runtime_classes\n    assert_success\n\n    rtc=$output\n    run jq '.items | length' <<<\"$rtc\"\n    assert_success\n    assert_output 2\n\n    run jq --raw-output '.items[].metadata.name' <<<\"$rtc\"\n    assert_success\n    assert_line 'bats'\n    assert_line 'spin'\n}\n\n@test 'deploy sample app' {\n    kubectl apply --filename - <<EOF\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: hello-spin\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: hello-spin\n  template:\n    metadata:\n      labels:\n        app: hello-spin\n    spec:\n      runtimeClassName: spin\n      containers:\n      - name: hello-spin\n        # Newer versions of the sample app have moved from \"deislabs\" to \"spinkube\":\n        # ghcr.io/spinkube/containerd-shim-spin/examples/spin-rust-hello:v0.13.0\n        image: ghcr.io/deislabs/containerd-wasm-shims/examples/spin-rust-hello:v0.10.0\n        command: [\"/\"]\nEOF\n}\n\n@test 'deploy ingress' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    kubectl apply --filename - <<EOF\napiVersion: v1\nkind: Service\nmetadata:\n  name: hello-spin\nspec:\n  type: ClusterIP\n  selector:\n    app: hello-spin\n  ports:\n  - port: 80\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: hello-spin\n  annotations:\n    traefik.ingress.kubernetes.io/router.entrypoints: web\nspec:\n  rules:\n  - host: \"$host\"\n    http:\n      paths:\n        - path: /\n          pathType: Prefix\n          backend:\n            service:\n              name: hello-spin\n              port:\n                number: 80\nEOF\n}\n\n@test 'connect to the service' {\n    # TODO remove `skip_unless_host_ip` once `traefik_hostname` no longer needs it\n    if is_windows; then\n        skip_unless_host_ip\n    fi\n\n    local host\n    host=$(traefik_hostname)\n\n    # This can take 100s with old versions of traefik, and 15s with newer ones.\n    run try curl --silent --fail \"http://${host}/hello\"\n    assert_success\n    assert_output \"Hello world from Spin!\"\n}\n"
  },
  {
    "path": "bats/tests/preferences/move-from-roaming-to-local.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    skip_on_unix 'roaming appdata => local appdata migration is windows-only'\n    ROAMING_HOME=\"$(wslpath_from_win32_env APPDATA)/rancher-desktop\"\n}\n\n@test 'factory reset' {\n    factory_reset\n    # WSL sometimes ends up not seeing deletes from Windows; force it here.\n    rm -rf \"$PATH_CONFIG\" \"$ROAMING_HOME\"\n}\n\n@test 'start app, create a setting, and move settings to roaming' {\n    start_container_engine\n    wait_for_container_engine\n\n    rdctl api -X PUT /settings --body '{ \"version\": 9, \"WSL\": {\"integrations\": { \"beaker\" : true }}}'\n    rdctl shutdown\n    create_file \"$ROAMING_HOME/settings.json\" <\"$PATH_CONFIG_FILE\"\n    rm -f \"$PATH_CONFIG_FILE\"\n}\n\n@test 'restart app, verify settings has been migrated' {\n    launch_the_application\n    wait_for_container_engine\n\n    run rdctl api /settings\n    assert_success\n    run jq_output .WSL.integrations.beaker\n    assert_success\n    assert_output true\n}\n\n@test 'verify the settings file exists in both Local/ and Roaming/' {\n    # Migration doesn't delete it from Roaming/ in case the user decides to roll back to an earlier version.\n    test -f \"$PATH_CONFIG_FILE\"\n    test -f \"$ROAMING_HOME/settings.json\"\n}\n\n@test 'verify factory-reset deletes all of Roaming/rancher-desktop' {\n    rdctl factory-reset\n    assert_not_exists \"$ROAMING_HOME\"\n}\n"
  },
  {
    "path": "bats/tests/preferences/surface-invalid-args.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    skip_on_windows\n}\n\n@test 'initial factory reset' {\n    factory_reset\n}\n\n@test 'mac-specific failure for unacceptable start setting' {\n    if ! is_macos; then\n        skip 'need a mac for the --virtual-machine.type setting'\n    elif supports_vz_emulation; then\n        skip 'no error setting virtualMachine.type to \"vz\" on this platform'\n    fi\n    RD_NO_MODAL_DIALOGS=1 launch_the_application --virtual-machine.type vz\n    try --max 36 --delay 5 assert_file_contains \\\n        \"$PATH_LOGS/background.log\" \\\n        'Setting virtualMachine.type to \"vz\" on Intel requires macOS 13.0 (Ventura) or later.'\n    rdctl shutdown\n}\n\n@test 'report unrecognized options in the log file' {\n    if ! using_dev_mode; then\n        skip 'hard to get unrecognized options past rdctl-start; run this test in dev-mode'\n    fi\n    yarn dev --his-face-rings-a-bell --no-modal-dialogs &\n    try --max 36 --delay 5 assert_file_contains \"$PATH_LOGS/settings.log\" \"Unrecognized command-line argument --his-face-rings-a-bell\"\n    rdctl shutdown\n}\n"
  },
  {
    "path": "bats/tests/preferences/verify-paths.bats",
    "content": "# Test case 30\n\nload '../helpers/load'\n# Ensure subshells don't inherit a path that includes ~/.rd/bin\nexport PATH\nPATH=$(echo \"$PATH\" | tr ':' '\\n' | grep -v /.rd/bin | tr '\\n' ':')\n\nlocal_setup() {\n    if is_windows; then\n        skip \"test not applicable on Windows\"\n    fi\n}\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start app' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n# Running `bash -l -c` can cause bats to hang, so close the output file descriptor with '3>&-'\n@test 'bash managed' {\n    if command -v bash >/dev/null; then\n        run bash -l -c \"which rdctl\" 3>&-\n        assert_success\n        assert_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'bash not found'\n    fi\n}\n\n@test 'zsh managed' {\n    if command -v zsh >/dev/null; then\n        run zsh -i -c \"which rdctl\"\n        assert_success\n        assert_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'zsh not found'\n    fi\n}\n\n@test 'fish managed' {\n    if command -v fish >/dev/null; then\n        run fish -c \"which rdctl\"\n        assert_success\n        assert_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'fish not found'\n    fi\n}\n\n# This bashrc test assumes that this test will succeed, but it frees us\n# from sleeping after changing application.path-management-strategy\nno_bashrc_path_manager() {\n    ! grep --silent 'MANAGED BY RANCHER DESKTOP START' \"$HOME/.bashrc\"\n}\n\n@test 'move to manual path-management' {\n    rdctl set --application.path-management-strategy=manual\n    try --max 5 --delay 2 no_bashrc_path_manager\n}\n\n@test 'bash unmanaged' {\n    if command -v bash >/dev/null; then\n        run bash -l -c \"which rdctl\" 3>&-\n        # Can't assert success or failure because rdctl might be in a directory other than ~/.rd/bin\n        refute_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'bash not found'\n    fi\n}\n\n@test 'zsh unmanaged' {\n    if command -v zsh >/dev/null; then\n        run zsh -i -c \"which rdctl\"\n        refute_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'zsh not found'\n    fi\n}\n\n@test 'fish unmanaged' {\n    if command -v fish >/dev/null; then\n        run fish -c \"which rdctl\"\n        refute_output --partial \"$HOME/.rd/bin/rdctl\"\n    else\n        skip 'fish not found'\n    fi\n}\n"
  },
  {
    "path": "bats/tests/preferences/verify-settings.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    if is_windows; then\n        skip \"test not applicable on Windows\"\n    fi\n}\n\n@test 'initial factory reset' {\n    factory_reset\n}\n\n@test 'start the app' {\n    start_container_engine\n    wait_for_container_engine\n}\n\nproxy_set() {\n    local field=$1\n    local value=$2\n\n    printf -v payload '{ \"version\": 10, \"experimental\": { \"virtualMachine\": { \"proxy\": { \"%s\": %s }}}}' \"$field\" \"$value\"\n    run rdctl api settings -X PUT --body \"$payload\"\n    assert_failure\n    assert_output --partial \"Changing field \\\"experimental.virtualMachine.proxy.${field}\\\" via the API isn't supported\"\n}\n\n@test 'complain about windows-specific vm settings' {\n    run rdctl api /settings\n    assert_success\n    run jq_output .experimental.virtualMachine.proxy.enabled\n    assert_success\n    assert_output false\n\n    proxy_set enabled \"true\"\n\n    for field in address password username; do\n        # Need to include the quotes for a string-value\n        proxy_set $field '\"smorgasbord\"'\n    done\n\n    proxy_set port -1\n    proxy_set noproxy '[\"buffalo\"]'\n}\n\n@test 'ignores echoing current vm settings' {\n    run rdctl api /settings\n    assert_success\n    run jq_output .experimental.virtualMachine.proxy\n    assert_success\n    printf -v payload '{ \"version\": 10, \"experimental\": { \"virtualMachine\": { \"proxy\": %s }}}' \"$output\"\n    run rdctl api settings -X PUT --body \"$payload\"\n    assert_success\n}\n"
  },
  {
    "path": "bats/tests/profile/create-profile-output.bats",
    "content": "load '../helpers/load'\n\n@test 'factory reset' {\n    factory_reset\n}\n\nBOGUS_CONTAINERD_NAMESPACE=change-to-k8s.io\n\n@test 'start app' {\n    start_container_engine\n    wait_for_container_engine\n    rdctl set --images.namespace=\"$BOGUS_CONTAINERD_NAMESPACE\"\n}\n\n@test 'complains when no output type is specified' {\n    run rdctl create-profile --from-settings\n    assert_failure\n    assert_output --partial 'an \"--output FORMAT\" option of either \"plist\" or \"reg\" must be specified'\n}\n\n@test 'complains when an invalid output type is specified' {\n    run rdctl create-profile --from-settings --output=cabbage\n    assert_failure\n    assert_output --partial 'received unrecognized \"--output FORMAT\" option of \"cabbage\"; \"plist\" or \"reg\" must be specified'\n}\n\n@test 'complains when no input source is specified' {\n    for type in reg plist; do\n        run rdctl create-profile --output $type\n        assert_failure\n        assert_output --partial 'no input format specified: must specify exactly one input format of \"--input FILE|-\", \"--body|-b STRING\", or \"--from-settings\"'\n    done\n}\n\n@test 'complains when no --input or --body arg is specified' {\n    for type in reg plist; do\n        for input in input body; do\n            run rdctl create-profile --output \"$type\" --\"$input\"\n            assert_failure\n            assert_output --partial $\"Error: flag needs an argument: --$input\"\n        done\n    done\n}\n\ntoo_many_input_formats() {\n    run rdctl create-profile \"$@\"\n    assert_failure\n    assert_output --partial 'too many input formats specified: must specify exactly one input format of \"--input FILE|-\", \"--body|-b STRING\", or \"--from-settings\"'\n}\n\n@test 'complains when multiple input sources are specified' {\n    for type in reg plist; do\n        too_many_input_formats --output $type --input some-file.txt -b moose\n        too_many_input_formats --output $type --input some-file.txt --from-settings\n        too_many_input_formats --output $type --input some-file.txt -b moose --from-settings\n        too_many_input_formats --output $type -b moose --from-settings\n    done\n}\n\n@test \"complains when input file doesn't exist\" {\n    run rdctl create-profile --output reg --input /no/such/file/here\n    assert_failure\n    assert_output --partial 'Error: open /no/such/file/here:'\n}\n\n@test 'report invalid parameters for plist' {\n    run rdctl create-profile --output=plist --from-settings --hive=fish\n    assert_failure\n    assert_output --partial $\"registry hive and type can't be specified with \\\"plist\\\"\"\n\n    run rdctl create-profile --output plist --from-settings --type=writer\n    assert_failure\n    assert_output --partial $\"registry hive and type can't be specified with \\\"plist\\\"\"\n}\n\n@test 'report unrecognized output-options' {\n    run rdctl create-profile --output=pickle\n    assert_failure\n    assert_output --partial 'received unrecognized \"--output FORMAT\" option of \"pickle\"; \"plist\" or \"reg\" must be specified'\n}\n\n@test 'report unrecognized registry type sub-option' {\n    run rdctl create-profile --output=reg --hive=hklm --type=ruff --from-settings\n    assert_failure\n    assert_output --partial 'invalid registry type of \"ruff\" specified'\n}\n\n@test 'report unrecognized registry hive sub-option' {\n    run rdctl create-profile --output=reg --hive=stuff --type=locked --from-settings\n    assert_failure\n    assert_output --partial 'invalid registry hive of \"stuff\" specified'\n}\n\n@test 'report unrecognized registry hive and type sub-options reports only the bad hive' {\n    run rdctl create-profile --output=reg --hive=shelves --type=cows --from-settings\n    assert_failure\n    assert_output --partial 'invalid registry hive of \"shelves\" specified'\n}\n\n# Happy tests follow\n\n# Sample input-generating functions\n\njson_maps_and_lists() {\n    cat <<'EOF'\n{\n  \"kubernetes\": {\n    \"enabled\": false\n  },\n  \"containerEngine\": {\n    \"allowedImages\": {\"patterns\": [\"abc\", \"ghi\", \"def\"] }\n  },\n  \"WSL\": {\n    \"integrations\": { \"second\": false, \"first\": true }\n  },\n  \"application\":  {\n    \"extensions\":  {\n      \"allowed\": {\n        \"enabled\": true,\n        \"list\":    []\n      },\n      \"installed\": { }\n    }\n  }\n}\nEOF\n}\nexport -f json_maps_and_lists\n\nsimple_json_data() {\n    echo '{ \"kubernetes\": {\"version\": \"moose-head\" }}'\n}\nexport -f simple_json_data\n\n# Verify that fields in this structure appear alphabetically in reg output,\n# and in the same order as the settings struct in plutil output.\njson_with_special_chars() {\n    cat <<'EOF'\n{\n  \"containerEngine\": {\n    \"name\": \"small-less-<-than\"\n  },\n  \"application\": {\n    \"extensions\": {\n        \"allowed\": {\n          \"enabled\": false,\n          \"list\": [\"less-than:<\", \"greater:>\", \"and:&\", \"d-quote:\\\"\", \"emoji:😀\"]\n        },\n        \"installed\": {\n            \"key-with-less-than: <\": true,\n            \"key-with-ampersand: &\": true,\n            \"key-with-greater-than: >\": true,\n            \"key-with-emoji: 🐤\": false\n        }\n    }\n  }\n}\nEOF\n}\nexport -f json_with_special_chars\n\nassert_full_settings_registry_output() {\n    local hive=$1\n    local type=$2\n    assert_success\n    assert_output --partial \"Windows Registry Editor Version 5.00\"\n    assert_output --partial \"[$hive\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\$type\\\\application]\"\n    assert_output --partial '\"debug\"=dword:1'\n    assert_output --partial \"[$hive\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\$type\\\\images]\"\n    assert_output --partial '\"namespace\"=\"'${BOGUS_CONTAINERD_NAMESPACE}'\"'\n}\n\nassert_full_settings_plist_output() {\n    assert_success\n    assert_output --partial '<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">'\n    assert_output --partial '<plist version=\"1.0\">'\n    # this next line makes sense only after <key>namespace</key>\n    assert_output --partial \"<string>${BOGUS_CONTAINERD_NAMESPACE}</string>\"\n}\n\n@test 'generates registry output for hklm/defaults' {\n    run rdctl create-profile --output reg --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE defaults\n\n    run rdctl create-profile --output reg --hive=hklm --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE defaults\n\n    run rdctl create-profile --output reg --hive=HKLM --type=Defaults --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE defaults\n\n    run rdctl create-profile --output reg --type=DEFAULTS --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE defaults\n}\n\n@test 'generates registry output for hklm/locked' {\n    run rdctl create-profile --output reg --hive=Hklm --type=Locked --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE locked\n\n    run rdctl create-profile --output reg --type=LOCKED --from-settings\n    assert_full_settings_registry_output HKEY_LOCAL_MACHINE locked\n}\n\n@test 'generates registry output for hkcu/defaults' {\n    run rdctl create-profile --output reg --hive=Hkcu --from-settings\n    assert_full_settings_registry_output HKEY_CURRENT_USER defaults\n\n    run rdctl create-profile --output reg --hive=hkcu --type=Defaults --from-settings\n    assert_full_settings_registry_output HKEY_CURRENT_USER defaults\n}\n\n# This next directive makes no sense.\n# shellcheck disable=SC2030\n@test 'generates registry output for hkcu/locked' {\n    run rdctl create-profile --output reg --hive=HKCU --type=locked --from-settings\n    assert_full_settings_registry_output HKEY_CURRENT_USER locked\n}\n\nassert_check_registry_output() {\n    local bashSideTemp\n    local winSideTemp\n    local testFile\n    local salt\n    local safePolicyName\n\n    assert_success\n\n    # We need to formulate /tmp as a directory both sides can see.\n    winSideTemp=$(wslpath -a -m /tmp)            # //wsl$/distro/tmp\n    bashSideTemp=$(wslpath -a -u \"$winSideTemp\") # /tmp\n    testFile=test.reg\n    salt=$$\n    # Can't write into ...\\Policies\\ as non-administrator, so populate a different directory.\n    safePolicyName=\"fakeProfile${salt}\"\n    # shellcheck disable=SC2001,SC2031\n    sed \"s/Policies/${safePolicyName}/\" <<<\"$output\" >\"${bashSideTemp}/${testFile}\"\n    reg.exe import \"${winSideTemp}\\\\${testFile}\"\n    reg.exe delete \"HKCU\\\\Software\\\\${safePolicyName}\\\\Rancher Desktop\" /f /va\n    rm \"${bashSideTemp}/${testFile}\"\n}\n\n@test 'validate full-setting registry output on Windows' {\n    if ! is_windows; then\n        skip \"Test requires the reg utility and only works on Windows\"\n    fi\n    run rdctl create-profile --output reg --hive=HKCU --type=defaults --from-settings\n    assert_check_registry_output\n}\n\n@test 'validate special-characters' {\n    if ! is_windows; then\n        skip \"Test requires the reg utility and only works on Windows\"\n    fi\n    run rdctl create-profile --output reg --hive=HKCU --type=defaults --input - <<<\"$(json_with_special_chars)\"\n    assert_check_registry_output\n}\n\n@test 'generates registry output from inline json' {\n    run rdctl create-profile --output reg --body '{\"application\": { \"window\": { \"quitOnClose\": true }}}'\n    assert_success\n    SETTINGS_VERSION=$(get_setting .version)\n    printf -v HEX_SETTINGS_VERSION \"%x\" \"$SETTINGS_VERSION\"\n    assert_output - <<EOF\nWindows Registry Editor Version 5.00\n[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies]\n[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Rancher Desktop]\n[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Rancher Desktop\\defaults]\n\"version\"=dword:$HEX_SETTINGS_VERSION\n[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application]\n[HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\window]\n\"quitOnClose\"=dword:1\nEOF\n}\n\n@test 'generates plist output from settings' {\n    run rdctl create-profile --output plist --from-settings\n    assert_full_settings_plist_output\n}\n\n@test 'verify plutil is ok with the generated plist output' {\n    if ! is_macos; then\n        skip \"Test requires the plist utility and only works on macOS\"\n    fi\n    run rdctl create-profile --output plist --from-settings\n    assert_success\n    plutil -s - <<<\"$output\"\n}\n\n# Verify that the fields given in `json_maps_and_lists` are resorted alphabetically, ignoring case\nassert_registry_output_for_maps_and_lists() {\n    assert_success\n    SETTINGS_VERSION=$(get_setting .version)\n    printf -v HEX_SETTINGS_VERSION \"%x\" \"$SETTINGS_VERSION\"\n    assert_output - <<EOF\nWindows Registry Editor Version 5.00\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults]\n\"version\"=dword:$HEX_SETTINGS_VERSION\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\extensions]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\extensions\\allowed]\n\"enabled\"=dword:1\n\"list\"=hex(7):00,00\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\extensions\\installed]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\containerEngine]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\containerEngine\\allowedImages]\n\"patterns\"=hex(7):61,00,62,00,63,00,00,00,67,00,68,00,69,00,00,00,64,00,65,00,66,00,00,00,00,00\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\kubernetes]\n\"enabled\"=dword:0\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\WSL]\n[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\WSL\\integrations]\n\"first\"=dword:1\n\"second\"=dword:0\nEOF\n}\n\n@test 'encodes multi-string values and maps from a json string' {\n    run rdctl create-profile --output reg --hive hkcu --body \"$(json_maps_and_lists)\"\n    assert_registry_output_for_maps_and_lists\n}\n\nassert_moose_head_plist_output() {\n    SETTINGS_VERSION=$(get_setting .version)\n    assert_success\n    assert_output - <<EOF\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>$SETTINGS_VERSION</integer>\n    <key>kubernetes</key>\n    <dict>\n      <key>version</key>\n      <string>moose-head</string>\n    </dict>\n  </dict>\n</plist>\nEOF\n}\n\n@test 'generates plist output from a command-line argument' {\n    run rdctl create-profile --output plist --body \"$(simple_json_data)\"\n    assert_moose_head_plist_output\n}\n\n@test 'generates plist output from a file' {\n    run rdctl create-profile --output plist --body \"$(simple_json_data)\"\n    assert_moose_head_plist_output\n}\n\n@test 'verify plutil is ok with the generated plist output from input file' {\n    if ! is_macos; then\n        skip \"Test requires the plist utility and only works on macOS\"\n    fi\n    # This input form is ok here because it won't run in WSL/Windows\n    run rdctl create-profile --output plist --input <(simple_json_data)\n    assert_success\n    plutil -s - <<<\"$output\"\n}\n\nassert_complex_plist_output() {\n    assert_success\n    SETTINGS_VERSION=$(get_setting .version)\n    assert_output - <<EOF\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>$SETTINGS_VERSION</integer>\n    <key>application</key>\n    <dict>\n      <key>extensions</key>\n      <dict>\n        <key>allowed</key>\n        <dict>\n          <key>enabled</key>\n          <true/>\n          <key>list</key>\n          <array>\n          </array>\n        </dict>\n        <key>installed</key>\n        <dict>\n        </dict>\n      </dict>\n    </dict>\n    <key>containerEngine</key>\n    <dict>\n      <key>allowedImages</key>\n      <dict>\n        <key>patterns</key>\n        <array>\n          <string>abc</string>\n          <string>ghi</string>\n          <string>def</string>\n        </array>\n      </dict>\n    </dict>\n    <key>kubernetes</key>\n    <dict>\n      <key>enabled</key>\n      <false/>\n    </dict>\n    <key>WSL</key>\n    <dict>\n      <key>integrations</key>\n      <dict>\n        <key>first</key>\n        <true/>\n        <key>second</key>\n        <false/>\n      </dict>\n    </dict>\n  </dict>\n</plist>\n\nEOF\n}\n\n@test 'plist-encodes multi-string values and maps from a file' {\n    run rdctl create-profile --output plist --body \"$(json_maps_and_lists)\"\n    assert_complex_plist_output\n}\n\n@test 'plist-encodes multi-string values and maps from a json string' {\n    run rdctl create-profile --output plist --body \"$(json_maps_and_lists)\"\n    assert_complex_plist_output\n}\n\n# Actual output-testing of this input is done in `plist_test.go` -- the purpose of this test is to just\n# make sure that we're generating compliant data.\n\n@test 'verify converted special-char input is escaped and satisfies plutil' {\n    if ! is_macos; then\n        skip \"Test requires the plist utility and only works on macOS\"\n    fi\n    # This input form is ok here because it won't run in WSL/Windows\n    run rdctl create-profile --output plist --input <(json_with_special_chars)\n    assert_success\n    plutil -s - <<<\"$output\"\n}\n\n@test 'verify converted special-char output' {\n    SETTINGS_VERSION=$(get_setting .version)\n    run rdctl create-profile --output plist --body \"$(json_with_special_chars)\"\n    assert_success\n    assert_output - <<END\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>$SETTINGS_VERSION</integer>\n    <key>application</key>\n    <dict>\n      <key>extensions</key>\n      <dict>\n        <key>allowed</key>\n        <dict>\n          <key>enabled</key>\n          <false/>\n          <key>list</key>\n          <array>\n            <string>less-than:&lt;</string>\n            <string>greater:&gt;</string>\n            <string>and:&amp;</string>\n            <string>d-quote:&#34;</string>\n            <string>emoji:😀</string>\n          </array>\n        </dict>\n        <key>installed</key>\n        <dict>\n          <key>key-with-ampersand: &amp;</key>\n          <true/>\n          <key>key-with-emoji: 🐤</key>\n          <false/>\n          <key>key-with-greater-than: &gt;</key>\n          <true/>\n          <key>key-with-less-than: &lt;</key>\n          <true/>\n        </dict>\n      </dict>\n    </dict>\n    <key>containerEngine</key>\n    <dict>\n      <key>name</key>\n      <string>small-less-&lt;-than</string>\n    </dict>\n  </dict>\n</plist>\nEND\n}\n\n@test \"and shutdown\" {\n    if is_macos; then\n        rdctl shutdown\n    fi\n}\n"
  },
  {
    "path": "bats/tests/profile/deployment.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    # Tell start_container_engine to store additional settings in the current\n    # profile and not in settings.json.\n    RD_USE_PROFILE=true\n\n    RD_USE_IMAGE_ALLOW_LIST=true\n\n    ALLOWED_EXTENSION_NAME=\"joycelin79/newman-extension\"\n    ALLOWED_EXTENSION_TAG=\"0.0.7\"\n    FORBIDDEN_EXTENSION_TAG=\"0.0.5\"\n    FORBIDDEN_EXTENSION=\"ignatandrei/blockly-automation\" # spellcheck-ignore-line\n    KUBERNETES_RANDOM_VERSION=\"1.29.5\"\n\n    # profile settings should be the opposite of the default config\n    if using_docker; then\n        DEFAULTS_CONTAINER_ENGINE_NAME=containerd\n    else\n        DEFAULTS_CONTAINER_ENGINE_NAME=moby\n    fi\n    DEFAULTS_START_IN_BACKGROUND=true\n    DEFAULTS_KUBERNETES_VERSION=\"$RD_KUBERNETES_VERSION\"\n\n    LOCKED_KUBERNETES_VERSION=\"1.27.3\"\n    LOCKED_ALLOWED_IMAGES_ENABLED=true\n    LOCKED_ALLOWED_IMAGES_PATTERNS=(\"$ALLOWED_EXTENSION_NAME\" \"$IMAGE_NGINX\")\n    LOCKED_EXTENSIONS_ALLOWED_ENABLED=true\n    LOCKED_EXTENSIONS_ALLOWED_LIST=(\"$ALLOWED_EXTENSION_NAME:$ALLOWED_EXTENSION_TAG\")\n}\n\nlocal_teardown_file() {\n    foreach_profile delete_profile\n}\n\nstart_app() {\n    # Store WSL integration and allowed images list in locked profile instead of settings.json\n    PROFILE_TYPE=$PROFILE_LOCKED\n    start_container_engine\n    try --max 40 --delay 5 rdctl api /settings\n\n    RD_CONTAINER_ENGINE=$(jq_output .containerEngine.name)\n    wait_for_container_engine\n}\n\nverify_profiles() {\n    local PROFILE_TYPE\n    for PROFILE_TYPE in \"$PROFILE_LOCKED\" \"$PROFILE_DEFAULTS\"; do\n        run profile_exists\n        \"${assert}_success\"\n    done\n}\n\nverify_settings() {\n    # settings from defaults profile\n    run get_setting .containerEngine.name\n    \"${assert}_output\" \"$DEFAULTS_CONTAINER_ENGINE_NAME\"\n\n    run get_setting .application.startInBackground\n    \"${assert}_output\" \"$DEFAULTS_START_IN_BACKGROUND\"\n\n    # settings from locked profile\n    run get_setting .containerEngine.allowedImages.enabled\n    \"${assert}_output\" \"$LOCKED_ALLOWED_IMAGES_ENABLED\"\n\n    run get_setting .containerEngine.allowedImages.patterns\n    \"${assert}_output\" --partial \"${LOCKED_ALLOWED_IMAGES_PATTERNS[@]}\"\n\n    run get_setting .application.extensions.allowed.enabled\n    \"${assert}_output\" \"$LOCKED_EXTENSIONS_ALLOWED_ENABLED\"\n\n    run get_setting .application.extensions.allowed.list\n    \"${assert}_output\" --partial \"${LOCKED_EXTENSIONS_ALLOWED_LIST[@]}\"\n\n    run get_setting .kubernetes.version\n    \"${assert}_output\" \"$LOCKED_KUBERNETES_VERSION\"\n    \"${refute}_output\" \"$DEFAULTS_KUBERNETES_VERSION\"\n}\n\ninstall_extensions() {\n    # Extension install doesn't work until startup is fully complete.\n    wait_for_backend\n\n    RD_TIMEOUT=120s run rdctl extension install \"$FORBIDDEN_EXTENSION\"\n    \"${refute}_success\"\n\n    RD_TIMEOUT=120s run rdctl extension install \"$ALLOWED_EXTENSION_NAME:$FORBIDDEN_EXTENSION_TAG\"\n    \"${refute}_success\"\n\n    RD_TIMEOUT=120s run rdctl extension install \"${LOCKED_EXTENSIONS_ALLOWED_LIST[0]}\"\n    assert_success\n}\n\n@test 'initial factory reset' {\n    factory_reset\n}\n\n@test 'start up with NO profiles' {\n    assert_not_equal \"$DEFAULTS_KUBERNETES_VERSION\" \"$LOCKED_KUBERNETES_VERSION\"\n    assert_not_equal \"$KUBERNETES_RANDOM_VERSION\" \"$LOCKED_KUBERNETES_VERSION\"\n    RD_USE_PROFILE=false\n    RD_USE_IMAGE_ALLOW_LIST=false\n    start_application\n}\n\n@test 'verify there were NO profiles created' {\n    before verify_profiles\n}\n\n@test 'verify default settings were applied' {\n    before verify_settings\n}\n\n@test 'verify all extensions can be installed' {\n    wait_for_kubelet\n    before install_extensions\n}\n\n@test 'factory reset before creating profiles' {\n    factory_reset\n}\n\n@test 'create profiles' {\n    PROFILE_TYPE=$PROFILE_LOCKED\n    create_profile\n    add_profile_int version 10\n\n    PROFILE_TYPE=$PROFILE_DEFAULTS\n    create_profile\n    add_profile_int version 10\n    add_profile_bool application.startInBackground \"$DEFAULTS_START_IN_BACKGROUND\"\n    verify_profiles\n}\n\n@test 'create defaults profile' {\n    PROFILE_TYPE=$PROFILE_DEFAULTS\n    add_profile_bool application.startInBackground \"$DEFAULTS_START_IN_BACKGROUND\"\n    add_profile_string containerEngine.name \"$DEFAULTS_CONTAINER_ENGINE_NAME\"\n    add_profile_string kubernetes.version \"$DEFAULTS_KUBERNETES_VERSION\"\n}\n\n@test 'create locked profile' {\n    PROFILE_TYPE=\"$PROFILE_LOCKED\"\n    add_profile_bool containerEngine.allowedImages.enabled \"$LOCKED_ALLOWED_IMAGES_ENABLED\"\n    add_profile_list containerEngine.allowedImages.patterns \"${LOCKED_ALLOWED_IMAGES_PATTERNS[@]}\"\n    add_profile_bool application.extensions.allowed.enabled \"$LOCKED_EXTENSIONS_ALLOWED_ENABLED\"\n    add_profile_list application.extensions.allowed.list \"${LOCKED_EXTENSIONS_ALLOWED_LIST[@]}\"\n    add_profile_string kubernetes.version \"$LOCKED_KUBERNETES_VERSION\"\n}\n\n@test 'start app with new profiles' {\n    RD_CONTAINER_ENGINE=\"\"\n    start_app\n}\n\n@test 'verify profile settings were applied' {\n    verify_settings\n}\n\n@test 'install only allowed extensions' {\n    install_extensions\n}\n\n@test 'try to change locked fields via rdctl set' {\n    run rdctl set --container-engine.allowed-images.enabled=false\n    assert_failure\n    assert_output --partial 'field \"containerEngine.allowedImages.enabled\" is locked'\n\n    run rdctl set --kubernetes.version=\"$KUBERNETES_RANDOM_VERSION\"\n    assert_failure\n    assert_output --partial 'field \"kubernetes.version\" is locked'\n}\n\napi_set() {\n    local body version\n    body=$(jq \".version=10\" <<<\"{$1}\")\n    rdctl api /v1/settings -X PUT --body \"$body\"\n}\n\n@test 'try to change locked fields via API' {\n    run api_set '\"containerEngine\": {\"allowedImages\": {\"patterns\": [\"pattern1\"]}}'\n    assert_failure\n    assert_output --partial 'field \"containerEngine.allowedImages.patterns\" is locked'\n\n    run api_set '\"containerEngine\": {\"allowedImages\": {\"enabled\": false}}'\n    assert_failure\n    assert_output --partial 'field \"containerEngine.allowedImages.enabled\" is locked'\n\n    run api_set '\"application\": {\"extensions\": {\"allowed\": {\"enabled\": false}}}'\n    assert_failure\n    assert_output --partial 'field \"application.extensions.allowed.enabled\" is locked'\n\n    run api_set '\"application\": {\"extensions\": {\"allowed\": {\"list\": [\"pattern1\"]}}}'\n    assert_failure\n    assert_output --partial 'field \"application.extensions.allowed.list\" is locked'\n\n    run api_set '\"kubernetes\": {\"version\": \"'\"$KUBERNETES_RANDOM_VERSION\"'\"}'\n    assert_failure\n    assert_output --partial 'field \"kubernetes.version\" is locked'\n}\n\n@test 'ensure locked settings are preserved' {\n    verify_settings\n}\n\n@test 'change defaults profile setting' {\n    run rdctl set --application.start-in-background=false\n    assert_success\n\n    run rdctl set --application.auto-start=true\n    assert_success\n\n    run rdctl set --kubernetes.version=\"$KUBERNETES_RANDOM_VERSION\"\n    assert_failure\n}\n\n@test 'verify that the new defaults settings are applied' {\n    DEFAULTS_START_IN_BACKGROUND=false\n    verify_settings\n    run get_setting .application.autoStart\n    assert_output true\n}\n\n@test 'shutdown app' {\n    rdctl shutdown\n}\n\n@test 'restart app' {\n    RD_CONTAINER_ENGINE=\"\"\n    start_app\n}\n\n@test 'verify that default profile is not applied again' {\n    DEFAULTS_START_IN_BACKGROUND=false\n    verify_settings\n    run get_setting .application.autoStart\n    assert_output true\n}\n\n@test 'shutdown Rancher Desktop' {\n    rdctl shutdown\n}\n\n@test 'try to change locked fields via rdctl start and watch it fail' {\n    launch_the_application --container-engine.allowed-images.enabled=false\n    if using_dev_mode; then\n        numTries=36\n    else\n        numTries=12\n    fi\n    try --max $numTries --delay 5 assert_file_contains \"$PATH_LOGS/background.log\" 'field \"containerEngine.allowedImages.enabled\" is locked'\n\n    # The app-launch commands are expected to fail. We wait until we see the failure\n    # message in the log file, but at that time the process may still be running.\n    # Make sure that Rancher Desktop has really stopped; otherwise `rdctl start/yarn dev` may not launch a new instance\n    rdctl shutdown\n\n    launch_the_application --kubernetes.version=\"1.16.15\"\n    try --max $numTries --delay 5 assert_file_contains \"$PATH_LOGS/background.log\" 'field \"kubernetes.version\" is locked'\n    # And again verify that the app is no longer running\n    rdctl shutdown\n}\n\n@test 'restart application' {\n    RD_CONTAINER_ENGINE=\"\"\n    start_app\n}\n\n@test 'ensure profile settings are preserved' {\n    DEFAULTS_START_IN_BACKGROUND=false\n    verify_settings\n}\n"
  },
  {
    "path": "bats/tests/profile/invalid-locked-k8s-version.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    RD_USE_PROFILE=true\n    PROFILE_TYPE=$PROFILE_LOCKED\n}\n\nlocal_teardown_file() {\n    foreach_profile delete_profile\n}\n\n@test 'initial factory reset' {\n    factory_reset\n}\n\n@test 'create profile' {\n    create_profile\n    add_profile_int version 8\n    add_profile_string kubernetes.version NattyBo\n}\n\n@test 'fails to start app with an invalid locked k8s version' {\n    # Have to set the version field or RD will think we're trying to change a locked field.\n    RD_KUBERNETES_VERSION=NattyBo start_kubernetes\n    # Don't do wait_for_container_engine because RD will shut down in the middle\n    # and the function will take a long time to time out making futile queries.\n    # The app should exit gracefully; after that we can check for contents.\n    try --max 60 --delay 5 assert_file_contains \"$PATH_LOGS/background.log\" \"Child exited\"\n    assert_file_contains \"$PATH_LOGS/background.log\" \"Error Starting Rancher Desktop\"\n    assert_file_contains \"$PATH_LOGS/background.log\" \"Locked kubernetes version 'NattyBo' isn't a valid version\"\n}\n\n@test 'recreate profile with a valid k8s version' {\n    add_profile_string kubernetes.version v1.27.1\n}\n\n@test 'fails to start app with a specified k8s version != locked k8s version' {\n    factory_reset\n    # Have to set the version field or RD will think we're trying to change a locked field.\n    RD_KUBERNETES_VERSION=v1.27.2 start_kubernetes\n    # The app should exit gracefully; after that we can check for contents.\n    try --max 60 --delay 5 assert_file_contains \"$PATH_LOGS/background.log\" \"Child exited\"\n    assert_file_contains \"$PATH_LOGS/background.log\" \"Error Starting Rancher Desktop\"\n    assert_file_contains \"$PATH_LOGS/background.log\" 'field \"kubernetes.version\" is locked'\n}\n"
  },
  {
    "path": "bats/tests/profile/wasm.bats",
    "content": "load '../helpers/load'\n\nlocal_teardown_file() {\n    foreach_profile delete_profile\n}\n\n@test 'create version 10 locked profile' {\n    PROFILE_TYPE=$PROFILE_LOCKED\n    create_profile\n    add_profile_int version 10\n}\n\n@test 'start application' {\n    factory_reset\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'WASM mode should be locked down' {\n    run rdctl set --experimental.container-engine.web-assembly.enabled\n    assert_failure\n    assert_output --partial 'field \"experimental.containerEngine.webAssembly.enabled\" is locked'\n}\n\n@test 'update locked profile to version 11' {\n    PROFILE_TYPE=$PROFILE_LOCKED\n    add_profile_int version 11\n}\n\n@test 'restart application with version 11 locked profile' {\n    factory_reset\n    start_container_engine\n    wait_for_backend\n}\n\n@test 'WASM mode is now unlocked' {\n    run rdctl set --experimental.container-engine.web-assembly.enabled\n    assert_success\n    assert_output --partial 'reconfiguring Rancher Desktop to apply changes'\n}\n"
  },
  {
    "path": "bats/tests/registry/creds.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    REGISTRY_PORT=\"5050\"\n    if is_windows && ! using_windows_exe; then\n        # TODO TODO TODO\n        # RD will only modify the Windows version of .docker/config.json;\n        # there is no WSL integration support for it. Therefore this test\n        # always needs to modify the Windows version and not touch the\n        # Linux one. This may change depending on:\n        # https://github.com/rancher-sandbox/rancher-desktop/issues/5523\n        # TODO TODO TODO\n        USERPROFILE=\"$(wslpath_from_win32_env USERPROFILE)\"\n    fi\n    DOCKER_CONFIG_FILE=\"$USERPROFILE/.docker/config.json\"\n\n    TEMP=/tmp\n    if is_windows; then\n        # We need to use a directory that exists on the Win32 filesystem\n        # so the ctrctl clients can correctly map the bind mounts.\n        # We can use host_path() on these paths because they will exist\n        # both here and in the rancher-desktop distro.\n        TEMP=\"$(wslpath_from_win32_env TEMP)\"\n    fi\n\n    AUTH_DIR=\"$TEMP/auth\"\n    CAROOT=\"$TEMP/caroot\"\n    CERTS_DIR=\"$TEMP/certs\"\n\n    if is_windows && using_docker; then\n        # BUG BUG BUG\n        # docker service on Windows cannot be restarted, so we can't register\n        # a new CA. `localhost` is an insecure registry, not requiring certs.\n        # https://github.com/rancher-sandbox/rancher-desktop/issues/3878\n        # BUG BUG BUG\n        REGISTRY_HOST=\"localhost\"\n    else\n        # Determine IP address of the VM that is routable inside the VM itself.\n        # Essentially localhost, but needs to be a routable IP that also works\n        # from inside a container. Will be turned into a DNS name using sslip.io.\n        if is_windows; then\n            ipaddr=\"192.168.143.1\"\n        else\n            # Lima uses a fixed hard-coded IP address\n            ipaddr=\"192.168.5.15\"\n        fi\n        REGISTRY_HOST=\"registry.$ipaddr.sslip.io\"\n    fi\n    REGISTRY=\"$REGISTRY_HOST:$REGISTRY_PORT\"\n}\n\ncreate_registry() {\n    run ctrctl rm -f registry\n    assert_nothing\n    rdshell mkdir -p \"$CERTS_DIR\"\n    ctrctl run \\\n        --detach \\\n        --name registry \\\n        --restart always \\\n        -p \"$REGISTRY_PORT:$REGISTRY_PORT\" \\\n        -e \"REGISTRY_HTTP_ADDR=0.0.0.0:$REGISTRY_PORT\" \\\n        -v \"$(host_path \"$CERTS_DIR\"):/certs\" \\\n        -e \"REGISTRY_HTTP_TLS_CERTIFICATE=/certs/$REGISTRY_HOST.pem\" \\\n        -e \"REGISTRY_HTTP_TLS_KEY=/certs/$REGISTRY_HOST-key.pem\" \\\n        \"$@\" \\\n        \"$IMAGE_REGISTRY\"\n    wait_for_registry\n}\n\nwait_for_registry() {\n    trace \"$(ctrctl ps -a)\"\n    # registry port is forwarded to host\n    try --max 20 --delay 5 curl -k --silent --show-error \"https://localhost:$REGISTRY_PORT/v2/_catalog\"\n}\n\nusing_insecure_registry() {\n    [ \"$REGISTRY_HOST\" = \"localhost\" ]\n}\n\nskip_for_insecure_registry() {\n    if using_insecure_registry; then\n        skip \"BUG: docker on Windows can only use insecure registry\"\n    fi\n}\n\n@test 'factory reset' {\n    factory_reset\n    rm -f \"$DOCKER_CONFIG_FILE\"\n}\n\n@test 'start container engine' {\n    start_container_engine\n\n    wait_for_shell\n    for dir in \"$AUTH_DIR\" \"$CAROOT\" \"$CERTS_DIR\"; do\n        rdshell rm -rf \"$dir\"\n    done\n\n    if using_image_allow_list; then\n        update_allowed_patterns true \"$IMAGE_REGISTRY\" \"$REGISTRY\"\n    fi\n}\n\n@test 'wait for container engine' {\n    wait_for_container_engine\n}\n\n@test 'verify credential is set correctly' {\n    verify_default_credStore\n}\n\nverify_default_credStore() {\n    local CREDHELPER_NAME\n    CREDHELPER_NAME=\"$(basename \"$CRED_HELPER\" .exe | sed s/^docker-credential-//)\"\n    run jq --raw-output .credsStore \"$DOCKER_CONFIG_FILE\"\n    assert_success\n    assert_output \"$CREDHELPER_NAME\"\n}\n\n@test 'verify allowed-images config' {\n    run ctrctl pull --quiet \"$IMAGE_BUSYBOX\"\n    if using_image_allow_list; then\n        assert_failure\n        assert_output --regexp \"(UNAUTHORIZED|Forbidden)\"\n    else\n        assert_success\n    fi\n}\n\n@test 'create server certs for registry' {\n    rdsudo apk add mkcert --force-broken-world --repository https://dl-cdn.alpinelinux.org/alpine/edge/testing\n    rdshell mkdir -p \"$CAROOT\" \"$CERTS_DIR\"\n    rdshell sh -c \"CAROOT=\\\"$CAROOT\\\" TRUST_STORES=none mkcert -install\"\n    rdshell sh -c \"cd \\\"$CERTS_DIR\\\"; CAROOT=\\\"$CAROOT\\\" mkcert \\\"$REGISTRY_HOST\\\"\"\n}\n\n@test 'pull registry image' {\n    ctrctl pull --quiet \"$IMAGE_REGISTRY\"\n}\n\n@test 'create plain registry' {\n    create_registry\n}\n\n@test 'tag image with registry' {\n    ctrctl tag \"$IMAGE_REGISTRY\" \"$REGISTRY/registry\"\n}\n\n@test 'expect push image to registry to fail because CA cert has not been installed' {\n    skip_for_insecure_registry\n\n    run ctrctl push \"$REGISTRY/registry\"\n    assert_failure\n    # we don't get cert errors when going through the proxy; they turn into 502's\n    assert_output --regexp \"(certificate signed by unknown authority|502 Bad Gateway)\"\n}\n\n@test 'install CA cert' {\n    skip_for_insecure_registry\n\n    rdsudo cp \"$CAROOT/rootCA.pem\" /usr/local/share/ca-certificates/\n    rdsudo update-ca-certificates\n}\n\nrestart_container_engine() {\n    # BUG BUG BUG\n    # When using containerd, sometimes the container would get wedged on a\n    # restart; however, restarting containerd again seems to fix this.\n    # So we need to keep trying until the registry container is not `created`.\n    # BUG BUG BUG\n    service_control \"$CONTAINER_ENGINE_SERVICE\" restart\n\n    service_control --ifstarted rd-openresty restart\n\n    wait_for_container_engine\n\n    trace \"$(ctrctl ps -a)\"\n    if using_containerd; then\n        run ctrctl ps --filter status=created,name=registry --format '{{.Names}}'\n        assert_success\n        refute_output registry\n    fi\n}\n\n@test 'restart container engine to refresh certs' {\n    skip_for_insecure_registry\n\n    try restart_container_engine\n\n    wait_for_registry\n}\n\n@test 'expect push image to registry to succeed now' {\n    ctrctl push \"$REGISTRY/registry\"\n}\n\n@test 'create registry with basic auth' {\n    # note: docker htpasswd **must** use bcrypt algorithm, i.e. `htpasswd -nbB user password`\n    # We intentionally use single-quotes; the '$' characters are literals\n    # shellcheck disable=SC2016\n    HTPASSWD='user:$2y$05$pd/kWjYSW9x48yaPQgrl.eLn02DdMPyoYPUy/yac601k6w.okKgmG'\n    rdshell mkdir -p \"$AUTH_DIR\"\n    echo \"$HTPASSWD\" | rdshell tee \"$AUTH_DIR/htpasswd\" >/dev/null\n    create_registry \\\n        -v \"$(host_path \"$AUTH_DIR\"):/auth\" \\\n        -e REGISTRY_AUTH=htpasswd \\\n        -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \\\n        -e REGISTRY_AUTH_HTPASSWD_REALM=\"Registry Realm\"\n}\n\n@test 'verify that registry requires basic auth' {\n    local curl_options=(--silent --show-error)\n    if using_insecure_registry; then\n        curl_options+=(--insecure)\n    fi\n\n    local registry_url=\"https://$REGISTRY/v2/_catalog\"\n    run rdshell curl \"${curl_options[@]}\" \"$registry_url\"\n    assert_success\n    assert_output --partial '\"message\":\"authentication required\"'\n\n    run rdshell curl \"${curl_options[@]}\" --user user:password \"$registry_url\"\n    assert_success\n    assert_output '{\"repositories\":[]}'\n}\n\n@test 'verify that pushing fails when not logged in' {\n    run bash -c \"echo \\\"$REGISTRY\\\" | \\\"$CRED_HELPER\\\" erase\"\n    assert_nothing\n    run ctrctl push \"$REGISTRY/registry\"\n    assert_failure\n    assert_output --regexp \"(401 Unauthorized|no basic auth credentials)\"\n}\n\n@test 'verify that pushing succeeds after logging in' {\n    run ctrctl login -u user -p password \"$REGISTRY\"\n    assert_success\n    assert_output --partial \"Login Succeeded\"\n\n    ctrctl push \"$REGISTRY/registry\"\n}\n\n@test 'verify credentials in host cred store' {\n    run bash -c \"echo \\\"$REGISTRY\\\" | \\\"$CRED_HELPER\\\" get\"\n    assert_success\n    assert_output --partial '\"Secret\":\"password\"'\n\n    ctrctl logout \"$REGISTRY\"\n    run bash -c \"echo \\\"$REGISTRY\\\" | \\\"$CRED_HELPER\\\" get\"\n    refute_output --partial '\"Secret\":\"password\"'\n}\n\n@test 'verify the docker-desktop credential helper is replaced with the rancher-desktop default' {\n    factory_reset\n    create_file \"$DOCKER_CONFIG_FILE\" <<<'{ \"credsStore\": \"desktop\" }'\n    start_container_engine\n    wait_for_container_engine\n    verify_default_credStore\n}\n"
  },
  {
    "path": "bats/tests/snapshots/create-use-snapshot.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    SNAPSHOT=the-ubiquitous-flounder\n}\n\n@test 'factory reset and delete all the snapshots' {\n    delete_all_snapshots\n    factory_reset\n}\n\n@test 'start up in moby' {\n    RD_CONTAINER_ENGINE=moby\n    start_kubernetes\n    wait_for_container_engine\n    wait_for_kubelet\n    wait_for_backend\n}\n\n@test 'push an nginx pod and verify' {\n    kubectl run nginx --image=\"$IMAGE_NGINX\" --port=8080\n    try --max 48 --delay 5 running_nginx\n}\n\n@test 'shutdown, make a snapshot, and run factory-reset' {\n    rdctl shutdown\n\n    snapshot_description=\"first snapshot\"\n    rdctl snapshot create \"$SNAPSHOT\" --description \"$snapshot_description\"\n    run rdctl snapshot list\n    assert_success\n    assert_output --partial \"$SNAPSHOT\"\n    assert_output --partial \"$snapshot_description\"\n\n    rdctl factory-reset\n}\n\n@test 'startup, verify using new settings' {\n    RD_CONTAINER_ENGINE=containerd\n    start_kubernetes\n    wait_for_container_engine\n    wait_for_kubelet\n    run rdctl api /settings\n    assert_success\n    run jq_output .containerEngine.name\n    assert_success\n    assert_output --partial containerd\n    run kubectl get pods -A\n    assert_success\n    refute_output --regexp 'default.*nginx.*Running'\n}\n\n# This should be one long test because if `snapshot restore` fails there's no point starting up\n@test 'shutdown, restore, restart and verify snapshot state' {\n    rdctl shutdown\n    run rdctl snapshot restore \"$SNAPSHOT\"\n    assert_success\n    refute_output --partial fail\n\n    launch_the_application\n\n    # Keep this variable in sync with the current setting so the wait_for commands work\n    RD_CONTAINER_ENGINE=moby\n    wait_for_container_engine\n    wait_for_kubelet\n\n    run rdctl api /settings\n    assert_success\n    run jq_output .containerEngine.name\n    assert_success\n    assert_output moby\n    try --max 48 --delay 5 running_nginx\n}\n\n@test 'verify identification errors' {\n    for action in restore delete; do\n        run rdctl snapshot \"$action\" 'the-nomadic-pond'\n        assert_failure\n        assert_output --partial \"Error: failed to $action snapshot \\\"the-nomadic-pond\\\": can't find snapshot \\\"the-nomadic-pond\\\"\"\n\n        run rdctl snapshot \"$action\" 'the-nomadic-pond' --json\n        assert_failure\n        run jq_output '.error'\n        assert_success\n        assert_output \"failed to $action snapshot \\\"the-nomadic-pond\\\": can't find snapshot \\\"the-nomadic-pond\\\"\"\n    done\n}\n\n@test \"attempt to create a snapshot with an existing name is flagged and doesn't do factory-reset\" {\n    # Shutdown RD for faster snapshot creation\n    # Also verify that the failed creation doesn't trigger a factory-reset and remove settings.json\n    rdctl shutdown\n    assert_exists \"$PATH_CONFIG_FILE\"\n    run rdctl snapshot create \"$SNAPSHOT\" --json\n    assert_failure\n    run jq_output '.error'\n    assert_success\n    assert_output \"name \\\"$SNAPSHOT\\\" already exists\"\n    assert_exists \"$PATH_CONFIG_FILE\"\n}\n\n@test 'rejects attempts to create a snapshot with different description sources' {\n    run rdctl snapshot create --description abc --description-from my-sad-file my-happy-snapshot-2\n    assert_failure\n    assert_output --partial \"Error: can't specify more than one option from \\\"--description\\\" and \\\"--description-from\\\"\"\n}\n\n@test 'can create a snapshot where proposed name is a current ID' {\n    run ls -1 \"$PATH_SNAPSHOTS\"\n    assert_success\n    refute_output \"\"\n    snapshot_id=\"${lines[0]}\"\n    test -n \"$snapshot_id\"\n    snapshot_description=\"second snapshot made with the --description option with \\\\ and \\\" and '.\"\n\n    rdctl snapshot create \"$snapshot_id\" --description \"$snapshot_description\"\n    run rdctl snapshot list --json\n    assert_success\n    run jq_output \"select(.name == \\\"$snapshot_id\\\").description\"\n    assert_success\n    assert_output \"$snapshot_description\"\n\n    # And we can delete that snapshot\n    run rdctl snapshot delete \"$snapshot_id\" --json\n    assert_success\n    assert_output \"\"\n}\n\n@test 'very long descriptions are truncated in the table view' {\n    snapshot_name=armadillo_farm\n    description_part=\"very long description names are truncated in the table view\"\n    long_description=\"$description_part, repeat: $description_part\"\n\n    rdctl snapshot create \"$snapshot_name\" --description \"$long_description\"\n\n    run rdctl snapshot list --json\n    assert_success\n    run jq_output \"select(.name == \\\"$snapshot_name\\\").description\"\n    assert_success\n    assert_output \"$long_description\"\n\n    run rdctl snapshot list\n    assert_success\n    run grep \"$snapshot_name\" <<<\"$output\"\n    assert_success\n    # Shouldn't have the whole description, but part of it\n    refute_output --partial \"$long_description\"\n    assert_output --partial \"$description_part\"\n}\n\n@test 'table view truncates descriptions at an internal newline' {\n    snapshot_name=retinal_asparagus\n    newline=$'\\n'\n    part1=\"there's a new\"\n    description=\"${part1}${newline}line somewhere in this description\"\n\n    rdctl snapshot create \"$snapshot_name\" --description \"$description\"\n\n    run rdctl snapshot list --json\n    assert_success\n    run jq_output \"select(.name == \\\"$snapshot_name\\\").description\"\n    assert_success\n    assert_output \"$description\"\n\n    run rdctl snapshot list\n    assert_success\n    run grep \"$snapshot_name\" <<<\"$output\"\n    assert_success\n    # Shouldn't have the whole description, but part of it\n    refute_output --partial \"$description\"\n    assert_output --partial \"${part1}…\"\n}\n\n@test \"factory-reset doesn't delete a non-empty snapshots directory\" {\n    rdctl factory-reset\n    assert_exists \"$PATH_SNAPSHOTS\"\n}\n\n@test 'factory-reset does delete an empty snapshots directory' {\n    delete_all_snapshots\n    rdctl factory-reset\n    assert_not_exists \"$PATH_SNAPSHOTS\"\n}\n\nrunning_nginx() {\n    run kubectl get pods -A\n    assert_success\n    assert_output --regexp 'default.*nginx.*Running'\n}\n"
  },
  {
    "path": "bats/tests/snapshots/restore-snapshot-after-factory-reset.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    SNAPSHOT=some-test-snapshot-name\n}\n\n@test 'factory reset and delete all the snapshots' {\n    delete_all_snapshots\n    factory_reset\n}\n\n@test 'start up using containerd' {\n    # TODO TODO TODO\n    # Using the container engine name to check if a snapshot has been\n    # restored is one of the worst possible choices, as the engine choice\n    # is built into so much logic in the helper functions.\n    # Maybe pull an image and verify the image is still there after the\n    # restore.\n    # TODO TODO TODO\n    RD_CONTAINER_ENGINE=containerd\n    start_kubernetes\n    wait_for_container_engine\n    wait_for_kubelet\n    wait_for_backend\n}\n\n@test 'shut down and make a snapshot' {\n    rdctl shutdown\n    rdctl snapshot create \"$SNAPSHOT\"\n    run rdctl snapshot list\n    assert_success\n    assert_output --partial \"$SNAPSHOT\"\n}\n\n@test 'do a factory reset' {\n    rdctl factory-reset\n}\n\n@test 'restore the snapshot without starting up first' {\n    run rdctl snapshot restore \"$SNAPSHOT\"\n    assert_success\n}\n\n@test 'start back up' {\n    # don't provide a --container-engine.name argument to `rdctl start`\n    RD_CONTAINER_ENGINE=\"\"\n    # don't create settings.json file\n    RD_USE_PROFILE=true\n    start_kubernetes\n    unset RD_USE_PROFILE\n    # make sure we are not waiting for the docker context to be created\n    RD_CONTAINER_ENGINE=\"containerd\"\n    wait_for_container_engine\n    wait_for_kubelet\n    wait_for_backend\n}\n\n@test 'verify that we are running containerd' {\n    run rdctl api /settings\n    assert_success\n    run jq_output .containerEngine.name\n    assert_success\n    assert_output --partial containerd\n}\n\n@test 'delete the snapshot and verify there are no others' {\n    rdctl snapshot delete \"$SNAPSHOT\"\n    run rdctl snapshot list --json\n    assert_success\n    assert_output ''\n}\n"
  },
  {
    "path": "bats/tests/snapshots/test-snapshot-list.bats",
    "content": "load '../helpers/load'\n\nlocal_setup() {\n    skip_on_windows \"snapshots test not applicable on Windows\"\n    NON_ALNUM_SNAPSHOT_NAME='@#$%'\n    MULTI_WORD_SNAPSHOT_NAME='=with '\\''single'\\'' and \"double\" quotes, /slashes/, and \\backslashes\\.'\n    EMOJI_SNAPSHOT_NAME=\"emoji's 😍 are cool\"\n    NON_ALNUM_DESCRIPTION='description for non-alnum-snapshot-name'\n    MULTI_WORD_DESCRIPTION='description for multi-word-snapshot-name'\n    EMOJI_DESCRIPTION='description for emoji-snapshot-name'\n    TEMP=$BATS_FILE_TMPDIR\n    if is_windows; then\n        TEMP=\"$(wslpath_from_win32_env TEMP)\"\n    fi\n}\n\n@test 'factory reset and delete all the snapshots' {\n    delete_all_snapshots\n    factory_reset\n}\n\n# This test ensures that we have something to take a snapshot of, because appHome might not exist.\n@test 'start up' {\n    start_kubernetes\n    wait_for_container_engine\n    wait_for_kubelet\n}\n\n@test 'verify empty snapshot-list output' {\n    run rdctl snapshot list --json\n    assert_success\n    assert_output ''\n\n    run rdctl snapshot list\n    assert_success\n    assert_output 'No snapshots present.'\n}\n\n@test 'create three snapshots with RD turned off, spaced every 5 seconds' {\n    # It's much faster to create snapshots when RD isn't running.\n    rdctl shutdown\n\n    # Sleep 5 seconds after creating each snapshot so later we can verify\n    # that the differences in each snapshot's creation time makes sense.\n\n    rdctl snapshot create --description-from - \"$NON_ALNUM_SNAPSHOT_NAME\" <<<\"$NON_ALNUM_DESCRIPTION\"\n    sleep 5\n\n    rdctl snapshot create --description-from - \"$MULTI_WORD_SNAPSHOT_NAME\" <<<\"$MULTI_WORD_DESCRIPTION\"\n    sleep 5\n\n    DESC_FILE=\"$TEMP/emoji-snapshot-description.txt\"\n    echo \"$EMOJI_DESCRIPTION\" >\"$DESC_FILE\"\n    rdctl snapshot create --description-from \"$DESC_FILE\" \"$EMOJI_SNAPSHOT_NAME\"\n}\n\ncreated() {\n    local name\n    name=$(json_string \"$1\")\n    jq_output \"select(.name == $name).created\"\n}\n\n@test 'verify snapshot-list output with snapshots' {\n    run rdctl snapshot list --json\n    assert_success\n    DATE1=$(created \"$MULTI_WORD_SNAPSHOT_NAME\")\n    DATE2=$(created \"$EMOJI_SNAPSHOT_NAME\")\n    if is_macos; then\n        TIME1=$(/bin/date -jf \"%Y-%m-%dT%H:%M:%S\" \"$DATE1\" +%s 2>/dev/null)\n        TIME2=$(/bin/date -jf \"%Y-%m-%dT%H:%M:%S\" \"$DATE2\" +%s 2>/dev/null)\n    elif is_linux; then\n        TIME1=$(date --date=\"$DATE1\" +%s)\n        TIME2=$(date --date=\"$DATE2\" +%s)\n    fi\n    # This is all we can assert, because we don't have an upper bound for the time\n    # between the two `snapshot create's`, and we don't have info on fractions of a second,\n    # so a difference of 4.9999 could show up as 4\n    ((TIME2 - TIME1 > 4))\n\n    run rdctl snapshot list\n    assert_success\n    assert_output --partial \"$NON_ALNUM_SNAPSHOT_NAME\"\n    assert_output --partial \"$MULTI_WORD_SNAPSHOT_NAME\"\n    assert_output --partial \"$EMOJI_SNAPSHOT_NAME\"\n    assert_output --partial \"$NON_ALNUM_DESCRIPTION\"\n    assert_output --partial \"$MULTI_WORD_DESCRIPTION\"\n    assert_output --partial \"$EMOJI_DESCRIPTION\"\n}\n\n@test 'verify k8s is off' {\n    start_container_engine\n    wait_for_container_engine\n    wait_for_backend\n    run rdctl api /v1/settings\n    assert_success\n    run jq_output .kubernetes.enabled\n    assert_success\n    assert_output \"false\"\n}\n\n@test 'create a snapshot with k8s off' {\n    # This tests that wait_for_backend accepts the DISABLED state as a final state.\n    rdctl snapshot create anime-walnut-festival\n    wait_for_container_engine\n    wait_for_backend\n}\n\n@test 'and verify the new snapshot is listed' {\n    run rdctl snapshot list\n    assert_success\n    assert_output --partial anime-walnut-festival\n}\n\n@test 'and clean up' {\n    delete_all_snapshots\n    run rdctl snapshot list\n    assert_success\n    assert_output 'No snapshots present.'\n}\n"
  },
  {
    "path": "bats/tests/snapshots/test_rdctl_snapshot.bats",
    "content": "load '../helpers/load'\n\n# TODO: Uncomment this test when snapshots go unhidden.\n#@test 'snapshot shows up in general help' {\n#    run rdctl --help\n#    assert_success\n#    assert_output -partial snapshot\n#}\n\n@test 'complain about missing argument' {\n    # These test the rdctl cmd layer, can't be easily unit-tested\n    for arg in create restore delete; do\n        run rdctl snapshot \"$arg\"\n        assert_failure\n    done\n}\n"
  },
  {
    "path": "bats/tests/utils/rdctl.bats",
    "content": "load '../helpers/load'\n\n# Verify various operations of `rdctl`\n\n@test 'factory reset' {\n    factory_reset\n}\n\n@test 'start Rancher Desktop' {\n    start_container_engine\n    wait_for_container_engine\n}\n\n@test 'rdctl info' {\n    run --separate-stderr rdctl info\n    assert_success\n    assert_output --partial 'Version:'\n}\n\n@test 'rdctl info --output=json' {\n    run --separate-stderr rdctl info --output=json\n    assert_success\n    json=$output\n    run jq_output .version\n    assert_success\n    assert_output --regexp '^v1\\.'\n    output=$json\n    run jq_output '.[\"ip-address\"]'\n    assert_success\n    assert_output --regexp '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'\n}\n\n@test 'rdctl info --field version' {\n    run rdctl info --field version\n    assert_success\n    assert_output --regexp '^v1\\.'\n}\n\n@test 'rdctl info --field ip-address' {\n    run rdctl info --field ip-address\n    assert_success\n    if is_windows; then\n        # On Windows, the IP address should be constant.\n        assert_output 192.168.127.2\n    elif is_linux; then\n        assert_output 192.168.5.15 # qemu SLIRP\n    elif is_macos; then\n        address=$output\n        if is_true \"$(get_setting '.application.adminAccess')\"; then\n            # This is provided by the user's DHCP server\n            output=$address assert_output --regexp '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$'\n        elif [[ $(get_setting .virtualMachine.type) == vz ]]; then\n            # macOS Virtualization.Framework NAT; should be a local address\n            output=$address assert_output --regexp '^192\\.168\\.'\n            # but the address should not be from the regular SLIRP range\n            output=$address refute_output --regexp '^192\\.168\\.5\\.'\n        else\n            output=$address assert_output 192.168.5.15 # qemu SLIRP\n        fi\n    else\n        fail 'Unknown OS'\n    fi\n}\n"
  },
  {
    "path": "bats/tests/utils/spin.bats",
    "content": "load '../helpers/load'\n\n# Verify that enabling Wasm support will install spin plugins and templates\n\nlocal_setup() {\n    SPIN_DATA_DIR=\"${PATH_APP_HOME}/spin\"\n}\n\ncmd_exe() {\n    \"${SYSTEMROOT}/system32/cmd.exe\" /c \"$@\"\n}\n\ndir_exists() {\n    if using_windows_exe; then\n        run --separate-stderr cmd_exe if exist \"$(host_path \"$1\")\" echo True\n        # Output may have trailing \\r\n        [[ $output =~ ^True ]]\n    else\n        [[ -d $1 ]]\n    fi\n}\n\n@test 'delete spin plugins and templates' {\n    if using_windows_exe; then\n        run cmd_exe rmdir /s /q \"$(host_path \"${SPIN_DATA_DIR:?}\")\"\n        assert_nothing\n    else\n        rm -rf \"${SPIN_DATA_DIR:?}\"\n    fi\n}\n\n@test 'confirm the spin directory is gone' {\n    run dir_exists \"$SPIN_DATA_DIR\"\n    assert_failure\n}\n\n@test 'start container engine with wasm support enabled' {\n    factory_reset\n    start_container_engine --experimental.container-engine.web-assembly.enabled\n    wait_for_container_engine\n}\n\n@test 'plugins are installed' {\n    run dir_exists \"${SPIN_DATA_DIR}/plugins/kube\"\n    assert_success\n}\n\n@test 'templates are installed' {\n    if using_windows_exe; then\n        run --separate-stderr cmd_exe dir /b \"$(host_path \"${SPIN_DATA_DIR}/templates\")\"\n        assert_success\n    else\n        run ls -1 \"${SPIN_DATA_DIR}/templates\"\n        assert_success\n    fi\n    assert_line --regexp \"^http-go_\" # from spin\n    assert_line --regexp \"^http-js_\" # from spin-js-sdk\n    assert_line --regexp \"^http-py_\" # from spin-python-sdk\n}\n"
  },
  {
    "path": "build/electron-publisher-custom.js",
    "content": "const childProcess = require('child_process');\nconst fs = require('fs');\nconst path = require('path');\nconst url = require('url');\nconst util = require('util');\n\nconst electronPublish = require('electron-publish');\n\nclass LonghornPublisher extends electronPublish.Publisher {\n  providerName = 'longhorn';\n  toString() {\n    return '<Longhorn Publisher>';\n  }\n\n  upload() {\n    // We're not doing any uploading here.\n    return Promise.resolve();\n  }\n\n  /**\n   * checkAndResolveOptions is used to resolve publisher configs, which is then\n   * stored in the `app-update.yml` config file shipped with the application.\n   */\n  static async checkAndResolveOptions(options) {\n    // Try to auto-fill the GitHub repository info.\n    if (!options.owner || !options.repo) {\n      // Try to get the repository info from package.json\n      let repository;\n      const packagePath = path.join(path.dirname(module.path), 'package.json');\n      const packageData = JSON.parse(await fs.promises.readFile(packagePath, { encoding: 'utf8' }));\n\n      if (packageData.repository.url) {\n        repository = new url.URL(packageData.repository.url);\n      } else {\n        // Try to get the repository info from git config\n        const execFile = util.promisify(childProcess.execFile);\n        const { stdout } = await execFile('git', ['config', 'remote.origin.url']);\n\n        repository = new url.URL(stdout.trim());\n      }\n\n      if (repository.hostname === 'github.com') {\n        const [, owner, repo] = repository.pathname.replace(/\\.git$/, '').split('/');\n\n        options.owner = options.owner || owner;\n        options.repo = options.repo || repo;\n      }\n    }\n  }\n}\nmodule.exports = LonghornPublisher;\n"
  },
  {
    "path": "build/license.rtf",
    "content": "{\\rtf1\n{\\fonttbl{\\f0\\fmodern\\fcharset0}}\n\\f0\\fs18\n{\\qc\nApache License\\line\nVersion 2.0, January 2004\\line\nhttp://www.apache.org/licenses/\n\\par\n}\n\\par\\ql\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\\par\\par\n\n1. Definitions.\\par\\par\n\n{\\li200\n\"License\" shall mean the terms and conditions for use, reproduction, and\n distribution as defined by Sections 1 through 9 of this document.\\par\\par\n\n\"Licensor\" shall mean the copyright owner or entity authorized by the copyright\n owner that is granting the License.\\par\\par\n\n\"Legal Entity\" shall mean the union of the acting entity and all other entities\n that control, are controlled by, or are under common control with that entity.\n For the purposes of this definition, \"control\" means (i) the power, direct or\n indirect, to cause the direction or management of such entity, whether by\n contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the\n outstanding shares, or (iii) beneficial ownership of such entity.\\par\\par\n\n\"You\" (or \"Your\") shall mean an individual or Legal Entity exercising\n permissions granted by this License.\\par\\par\n\n\"Source\" form shall mean the preferred form for making modifications, including\n but not limited to software source code, documentation source, and\n configuration files.\\par\\par\n\n\"Object\" form shall mean any form resulting from mechanical transformation or\n translation of a Source form, including but not limited to compiled object\n code, generated documentation, and conversions to other media types.\\par\\par\n\n\"Work\" shall mean the work of authorship, whether in Source or Object form, made\n available under the License, as indicated by a copyright notice that is\n included in or attached to the work (an example is provided in the Appendix\n below).\\par\\par\n\n\"Derivative Works\" shall mean any work, whether in Source or Object form, that\n is based on (or derived from) the Work and for which the editorial revisions,\n annotations, elaborations, or other modifications represent, as a whole, an\n original work of authorship. For the purposes of this License, Derivative Works\n shall not include works that remain separable from, or merely link (or bind by\n name) to the interfaces of, the Work and Derivative Works thereof.\\par\\par\n\n\"Contribution\" shall mean any work of authorship, including the original version\n of the Work and any modifications or additions to that Work or Derivative Works\n thereof, that is intentionally submitted to Licensor for inclusion in the Work\n by the copyright owner or by an individual or Legal Entity authorized to submit\n on behalf of the copyright owner. For the purposes of this definition,\n \"submitted\" means any form of electronic, verbal, or written communication sent\n to the Licensor or its representatives, including but not limited to\n communication on electronic mailing lists, source code control systems, and\n issue tracking systems that are managed by, or on behalf of, the Licensor for\n the purpose of discussing and improving the Work, but excluding communication\n that is conspicuously marked or otherwise designated in writing by the\n copyright owner as \"Not a Contribution.\"\\par\\par\n\n\"Contributor\" shall mean Licensor and any individual or Legal Entity on behalf\n of whom a Contribution has been received by Licensor and subsequently\n incorporated within the Work.\\par\\par\n}\n\n2. Grant of Copyright License. Subject to the terms and conditions of this\n License, each Contributor hereby grants to You a perpetual, worldwide,\n non-exclusive, no-charge, royalty-free, irrevocable copyright license to\n reproduce, prepare Derivative Works of, publicly display, publicly perform,\n sublicense, and distribute the Work and such Derivative Works in Source or\n Object form.\\par\\par\n\n3. Grant of Patent License. Subject to the terms and conditions of this\n License, each Contributor hereby grants to You a perpetual, worldwide,\n non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this\n section) patent license to make, have made, use, offer to sell, sell, import,\n and otherwise transfer the Work, where such license applies only to those\n patent claims licensable by such Contributor that are necessarily infringed by\n their Contribution(s) alone or by combination of their Contribution(s) with the\n Work to which such Contribution(s) was submitted. If You institute patent\n litigation against any entity (including a cross-claim or counterclaim in a\n lawsuit) alleging that the Work or a Contribution incorporated within the Work\n constitutes direct or contributory patent infringement, then any patent\n licenses granted to You under this License for that Work shall terminate as of\n the date such litigation is filed.\\par\\par\n\n4. Redistribution. You may reproduce and distribute copies of the Work\n or Derivative Works thereof in any medium, with or without modifications, and\n in Source or Object form, provided that You meet the following conditions:\n \\par\\par\n\n{\\fi-100\\li200\n a. You must give any other recipients of the Work or Derivative Works a\n copy of this License; and\\par\\par\n\n b. You must cause any modified files to carry prominent notices stating\n that You changed the files; and\\par\\par\n\n c. You must retain, in the Source form of any Derivative Works that You\n distribute, all copyright, patent, trademark, and attribution notices from the\n Source form of the Work, excluding those notices that do not pertain to any\n part of the Derivative Works; and\\par\\par\n\n d. If the Work includes a \"NOTICE\" text file as part of its\n distribution, then any Derivative Works that You distribute must include a\n readable copy of the attribution notices contained within such NOTICE file,\n excluding those notices that do not pertain to any part of the Derivative\n Works, in at least one of the following places: within a NOTICE text file\n distributed as part of the Derivative Works; within the Source form or\n documentation, if provided along with the Derivative Works; or, within a\n display generated by the Derivative Works, if and wherever such third-party\n notices normally appear. The contents of the NOTICE file are for informational\n purposes only and do not modify the License. You may add Your own attribution\n notices within Derivative Works that You distribute, alongside or as an\n addendum to the NOTICE text from the Work, provided that such additional\n attribution notices cannot be construed as modifying the License.\\par\\par\n\n You may add Your own copyright statement to Your modifications and may provide\n additional or different license terms and conditions for use, reproduction, or\n distribution of Your modifications, or for any such Derivative Works as a\n whole, provided Your use, reproduction, and distribution of the Work otherwise\n complies with the conditions stated in this License.\\par\\par\n}\n\n5. Submission of Contributions. Unless You explicitly state otherwise,\n any Contribution intentionally submitted for inclusion in the Work by You to\n the Licensor shall be under the terms and conditions of this License, without\n any additional terms or conditions. Notwithstanding the above, nothing herein\n shall supersede or modify the terms of any separate license agreement you may\n have executed with Licensor regarding such Contributions.\\par\\par\n\n6. Trademarks. This License does not grant permission to use the trade\n names, trademarks, service marks, or product names of the Licensor, except as\n required for reasonable and customary use in describing the origin of the Work\n and reproducing the content of the NOTICE file.\\par\\par\n\n7. Disclaimer of Warranty. Unless required by applicable law or agreed\n to in writing, Licensor provides the Work (and each Contributor provides its\n Contributions) on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY\n KIND, either express or implied, including, without limitation, any warranties\n or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n PARTICULAR PURPOSE. You are solely responsible for determining the\n appropriateness of using or redistributing the Work and assume any risks\n associated with Your exercise of permissions under this License.\\par\\par\n\n8. Limitation of Liability. In no event and under no legal theory,\n whether in tort (including negligence), contract, or otherwise, unless required\n by applicable law (such as deliberate and grossly negligent acts) or agreed to\n in writing, shall any Contributor be liable to You for damages, including any\n direct, indirect, special, incidental, or consequential damages of any\n character arising as a result of this License or out of the use or inability to\n use the Work (including but not limited to damages for loss of goodwill, work\n stoppage, computer failure or malfunction, or any and all other commercial\n damages or losses), even if such Contributor has been advised of the\n possibility of such damages.\\par\\par\n\n9. Accepting Warranty or Additional Liability. While redistributing the\n Work or Derivative Works thereof, You may choose to offer, and charge a fee\n for, acceptance of support, warranty, indemnity, or other liability obligations\n and/or rights consistent with this License. However, in accepting such\n obligations, You may act only on Your own behalf and on Your sole\n responsibility, not on behalf of any other Contributor, and only if You agree\n to indemnify, defend, and hold each Contributor harmless for any liability\n incurred by, or claims asserted against, such Contributor by reason of your\n accepting any such warranty or additional liability.\\par\\par\n\n\\par END OF TERMS AND CONDITIONS\\par\n\n\\par\nHow to apply the Apache License to your work\\par\\par\n\nInclude a copy of the Apache License, typically in a file called LICENSE, in\n your work, and consider also including a NOTICE file that references the\n License.\n\\par\nTo apply the Apache License to specific files in your work, attach the following\n boilerplate declaration, replacing the fields enclosed by brackets \"[]\" with\n your own identifying information. (Don't include the brackets!) Enclose the\n text in the appropriate comment syntax for the file format. We also recommend\n that you include a file or class name and description of purpose on the same\n \"printed page\" as the copyright notice for easier identification within\n third-party archives.\n\\par\n\\pard\n\\par Copyright [yyyy] [name of copyright owner]\n\\par\n\\par Licensed under the Apache License, Version 2.0 (the \"License\");\n\\par you may not use this file except in compliance with the License.\n\\par You may obtain a copy of the License at\n\\par\n\\par     http://www.apache.org/licenses/LICENSE-2.0\n\\par\n\\par Unless required by applicable law or agreed to in writing, software\n\\par distributed under the License is distributed on an \"AS IS\" BASIS,\n\\par WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\\par See the License for the specific language governing permissions and\n\\par limitations under the License.}\n"
  },
  {
    "path": "build/signing-config-mac.yaml",
    "content": "# This file describes the code signing configuration for macOS.\n\n# List of entitlements.\nentitlements:\n  # This contains the default entitlements, for files not otherwise listed.\n  default:\n  - com.apple.security.inherit\n\n  # Entitlement overrides.  This is a list of overrides, each with a \"paths\"\n  # key describing which paths to override, and an \"entitlements\" key for the\n  # overriding entitlements.\n  overrides:\n  - paths:\n    - '' # This is the main application\n    entitlements:\n    - com.apple.security.cs.allow-jit\n  - paths:\n    - Contents/Resources/resources/darwin/lima/bin/limactl\n    entitlements:\n    - com.apple.security.virtualization\n  - paths:\n    - Contents/Resources/resources/darwin/lima/bin/qemu-system-aarch64\n    - Contents/Resources/resources/darwin/lima/bin/qemu-system-x86_64\n    entitlements:\n    - com.apple.security.cs.allow-jit\n    - com.apple.security.hypervisor\n  - paths:\n    - Contents/Resources/resources/darwin/internal/spin\n    entitlements:\n    - com.apple.security.cs.allow-unsigned-executable-memory\n  - paths:\n    - Contents/Frameworks/Rancher Desktop Helper (GPU).app\n    - Contents/Frameworks/Rancher Desktop Helper (Renderer).app\n    entitlements:\n    - com.apple.security.cs.allow-jit\n  - paths:\n    - Contents/Frameworks/Rancher Desktop Helper (Plugin).app\n    entitlements:\n    - com.apple.security.cs.allow-unsigned-executable-memory\n    - com.apple.security.cs.disable-library-validation\n\n# List of launch constraints.\n# This is similar to entitlements, but has no default: it's just a list of\n# paths, plus matching \"self\", \"parent\", and \"responsible\" constraints.\nconstraints:\n- paths:\n  - Contents/Resources/resources/darwin/lima/bin/limactl\n  self:\n    team-identifier: '${AC_TEAMID}'\n\n# A list of files/directories to remove before signing.\nremove:\n- Contents/build\n- Contents/electron-builder.yml\n"
  },
  {
    "path": "build/signing-config-win.yaml",
    "content": "# This file describes the code signing configuration for Windows.\n\n# The key is a directory name, relative to the unpacked zip file.\n# The value is an array of files in that directory to sign, or an explicit\n# negation (prefixed with \"!\").  Any files not listed is an error.\n.:\n- Rancher Desktop.exe\n- wix-custom-action.dll\n- '!dxcompiler.dll'     # spellcheck-ignore-line\n- '!ffmpeg.dll'\n- '!libEGL.dll'         # spellcheck-ignore-line\n- '!libGLESv2.dll'      # spellcheck-ignore-line\n- '!vk_swiftshader.dll' # spellcheck-ignore-line\n- '!vulkan-1.dll'       # spellcheck-ignore-line\nresources/resources/win32/bin:\n- docker.exe\n- docker-credential-none.exe\n- nerdctl.exe\n- rdctl.exe\n- spin.exe\n- '!docker-credential-ecr-login.exe'\n- '!docker-credential-wincred.exe'\n- '!helm.exe'\n- '!kubectl.exe'\n- '!kuberlr.exe'\nresources/resources/win32/docker-cli-plugins:\n- '!docker-buildx.exe'\n- '!docker-compose.exe'\nresources/resources/win32/internal:\n- host-switch.exe\n- steve.exe\n- wsl-helper.exe\n- '!spin.exe'\n"
  },
  {
    "path": "build/wix/dialogs.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n  <Fragment>\n    <!-- Let the dialogs know we support both per-user and per-machine -->\n    <WixVariable Id=\"WixUISupportPerUser\" Value=\"1\" Overridable=\"yes\" />\n    <WixVariable Id=\"WixUISupportPerMachine\" Value=\"1\" Overridable=\"yes\" />\n\n    <UI Id=\"WixUI_RD\">\n\n      <TextStyle Id=\"WixUI_Font_Normal\" FaceName=\"Segoe UI\" Size=\"8\" />\n      <TextStyle Id=\"WixUI_Font_Bigger\" FaceName=\"Segoe UI\" Size=\"12\" />\n      <TextStyle Id=\"WixUI_Font_Title\" FaceName=\"Segoe UI\" Size=\"9\" Bold=\"yes\" />\n      <TextStyle Id=\"WixUI_Font_Emphasized\" FaceName=\"Segoe UI\" Size=\"8\" Bold=\"yes\" />\n\n      <Property Id=\"DefaultUIFont\" Value=\"WixUI_Font_Normal\" />\n      <Property Id=\"WixUI_Mode\" Value=\"RD\" />\n      <Property Id=\"ALLUSERS\" Secure=\"yes\" Value=\"2\" />\n      <!-- MSIINSTALLPERUSER will get modified when the user advances from InstallScopeDlg -->\n      <Property Id=\"MSIINSTALLPERUSER\" Secure=\"yes\" Value=\"0\" />\n\n      <Error Id=\"100\">!(loc.Error_WSLNotInstalled)</Error>\n\n      <DialogRef Id=\"ErrorDlg\" />\n      <DialogRef Id=\"FatalError\" />\n      <DialogRef Id=\"FilesInUse\" />\n      <DialogRef Id=\"MsiRMFilesInUse\" />\n      <DialogRef Id=\"PrepareDlg\" />\n      <DialogRef Id=\"ResumeDlg\" />\n      <DialogRef Id=\"UserExit\" />\n      <DialogRef Id=\"RDWelcomeDlg\" />\n      <UIRef Id=\"WixUI_ErrorProgressText\" />\n\n      <Publish Dialog=\"ExitDialog\"\n         Control=\"Finish\"\n         Event=\"DoAction\"\n         Value=\"LaunchApplication\"\n      >WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>\n      <Publish Dialog=\"ExitDialog\" Control=\"Finish\" Event=\"EndDialog\" Value=\"Return\" Order=\"999\">1</Publish>\n\n      <Publish Dialog=\"VerifyReadyDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\">1</Publish>\n\n      <Publish Dialog=\"MaintenanceWelcomeDlg\" Control=\"Next\" Event=\"NewDialog\" Value=\"MaintenanceTypeDlg\">1</Publish>\n\n      <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RepairButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n      <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"RemoveButton\" Event=\"NewDialog\" Value=\"VerifyReadyDlg\">1</Publish>\n      <Publish Dialog=\"MaintenanceTypeDlg\" Control=\"Back\" Event=\"NewDialog\" Value=\"MaintenanceWelcomeDlg\">1</Publish>\n    </UI>\n\n    <Property Id=\"WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT\" Value=\"Run Rancher Desktop\" />\n    <Property Id=\"WixShellExecTarget\" Value=\"[#mainExecutable]\" />\n    <CustomAction Id=\"LaunchApplication\" BinaryKey=\"WixCA\" DllEntry=\"WixShellExec\" Impersonate=\"yes\" />\n    <InstallUISequence>\n      <Show Dialog=\"MaintenanceWelcomeDlg\" Before=\"RDWelcomeDlg\">Installed AND NOT RESUME AND NOT Preselected AND NOT PATCH</Show>\n    </InstallUISequence>\n\n    <UIRef Id=\"WixUI_Common\" />\n  </Fragment>\n</Wix>\n"
  },
  {
    "path": "build/wix/main.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  - Main WiX source for the Rancher Desktop Installer\n  -\n  - This file is a Mustache template that is rendered via installer-win32.ts.\n  -->\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\"\n  xmlns:fire=\"http://schemas.microsoft.com/wix/FirewallExtension\">\n  <Product\n    Id=\"*\"\n    Name=\"Rancher Desktop\"\n    UpgradeCode=\"{1F717D5A-A55B-5FE2-9103-C0D74F7FBDE3}\"\n    Version=\"{{appVersion}}\"\n    Language=\"1033\"\n    Codepage=\"65001\"\n    Manufacturer=\"SUSE\">\n    <Package Compressed=\"yes\" InstallerVersion=\"500\" />\n    <!-- As of Windows 10/11, msiexec.exe is manifested for Windows 8.1 -->\n    <Condition Message=\"Windows 10 and above is required\">\n      <![CDATA[ Installed OR VersionNT >= 603 ]]>\n    </Condition>\n    <MajorUpgrade\n      AllowSameVersionUpgrades=\"yes\"\n      DowngradeErrorMessage='A newer version of [ProductName] is already installed.'\n      Schedule=\"afterInstallInitialize\" />\n    <MediaTemplate CompressionLevel=\"high\" EmbedCab=\"yes\" />\n    <UIRef Id=\"WixUI_RD\" />\n\n    <Property Id=\"ApplicationFolderName\" Value=\"Rancher Desktop\" />\n    <Icon Id=\"RancherDesktopIcon.exe\" SourceFile=\"$(var.iconPath)\" />\n    <Property Id=\"ARPPRODUCTICON\" Value=\"RancherDesktopIcon.exe\" />\n    <Property Id=\"ARPNOMODIFY\" Value=\"1\" />\n    <Property Id=\"ARPURLINFOABOUT\" Value=\"https://rancherdesktop.io/\" />\n\n    <WixVariable Id=\"AppGUID\" Value=\"358d85cc-bb94-539e-a3cd-9231b877c7a4\" />\n\n    <DirectoryRef Id=\"TARGETDIR\" />\n\n    <SetProperty Id=\"ALLUSERS\" Sequence=\"both\" After=\"ValidateProductID\" Value=\"1\">\n      NOT WSLINSTALLED AND NOT Installed\n    </SetProperty>\n    <SetProperty Id=\"MSIINSTALLPERUSER\" Sequence=\"first\" After=\"ValidateProductID\" Value=\"0\">\n      (NOT WSLINSTALLED OR NOT MSIINSTALLPERUSER) AND NOT Installed\n    </SetProperty>\n\n    <!-- Custom action to detect if WSL is installed -->\n    <Binary Id=\"CustomActionBinary\" SourceFile=\"wix-custom-action.dll\" />\n    <CustomAction Id=\"DetectWSL\" BinaryKey=\"CustomActionBinary\" DllEntry=\"DetectWSL\"\n      Execute=\"immediate\" Return=\"check\" />\n    <!-- Custom action to update WSL -->\n    <CustomAction Id=\"UpdateWSL\" BinaryKey=\"CustomActionBinary\" DllEntry=\"UpdateWSL\"\n      Execute=\"deferred\" Return=\"check\" Impersonate=\"yes\" />\n    <!-- Custom action to raise an error because WSL was not installed -->\n    <CustomAction Id=\"ErrorWSLNotInstalled\" Error=\"100\" />\n\n    <InstallUISequence>\n      <!--\n        - DetectWSL may set the WSLINSTALLED and WSLKERNELOUTDATED properties\n        - depending on the state of the system.\n        -->\n      <Custom Action=\"DetectWSL\" After=\"AppSearch\">\n        NOT WSLINSTALLED <!-- Skip search if user overrides -->\n      </Custom>\n      <Custom Action=\"ErrorWSLNotInstalled\" After=\"RDWelcomeDlg\">\n        NOT WSLINSTALLED AND NOT Installed\n      </Custom>\n    </InstallUISequence>\n    <InstallExecuteSequence>\n      <Custom Action=\"DetectWSL\" After=\"AppSearch\">\n        NOT WSLINSTALLED <!-- Skip search if user overrides -->\n      </Custom>\n      <Custom Action=\"ErrorWSLNotInstalled\" After=\"LaunchConditions\">\n        NOT WSLINSTALLED AND NOT Installed\n      </Custom>\n      <Custom Action=\"UpdateWSL\" After=\"InstallFiles\">\n        WSLINSTALLED AND WSLKERNELOUTDATED\n      </Custom>\n    </InstallExecuteSequence>\n\n    <!-- Check if the NSIS-based Rancher Desktop is installed, and uninstall if yes. -->\n    <Property Id=\"NSISUNINSTALLCOMMAND\">\n      <RegistrySearch\n        Id=\"NSISInstalled\"\n        Root=\"HKCU\"\n        Key=\"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\!(wix.AppGUID)\"\n        Name=\"QuietUninstallString\"\n        Type=\"raw\" />\n    </Property>\n    <!--\n      - We use a CustomAction with a Directory= so we have full control of the\n      - execution (action type 34); a more obvious Property= (type 50) will\n      - interpret the whole string as the executable (argv[0]) and fail.\n      - Since this is run before anything is installed, we pick a random\n      - system directory as the working directory (i.e. Directory=).\n      - Note that this action *must* be run as the non-privileged user (so\n      - that it will clear out the uninstall registry key).\n      -->\n    <CustomAction\n      Id=\"UninstallNSIS\"\n      ExeCommand=\"[NSISUNINSTALLCOMMAND]\"\n      Execute=\"immediate\"\n      Impersonate=\"yes\"\n      Directory=\"ProgramFiles64Folder\"\n      Return=\"check\"\n    />\n    <InstallExecuteSequence>\n      <Custom Action=\"UninstallNSIS\" After=\"InstallInitialize\">\n        NSISUNINSTALLCOMMAND AND NOT Installed\n      </Custom>\n    </InstallExecuteSequence>\n\n    <!-- Setting the PATH variable -->\n    <Component Id=\"PathUser\" Directory=\"APPLICATIONFOLDER\">\n      <Condition>MSIINSTALLPERUSER = 1</Condition>\n      <RegistryValue\n        Root=\"HKCU\"\n        Key=\"SOFTWARE\\!(wix.AppGUID)\"\n        Name=\"EnvironmentVariablesSet\"\n        Value=\"yes\"\n        Type=\"string\"\n        KeyPath=\"yes\"\n      />\n      <Environment Id=\"PathWindowsUserBin\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"no\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\win32\\bin\\\" />\n      <Environment Id=\"PathWindowsUserDockerCLIPlugins\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"no\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\win32\\docker-cli-plugins\\\" />\n      <Environment Id=\"PathLinuxUserBin\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"no\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\linux\\bin\\\" />\n      <Environment Id=\"PathLinuxUserDockerCLIPlugins\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"no\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\linux\\docker-cli-plugins\\\" />\n    </Component>\n    <Component Id=\"PathSystem\" Directory=\"APPLICATIONFOLDER\">\n      <Condition>\n        <![CDATA[MSIINSTALLPERUSER <> 1]]>\n      </Condition>\n      <RegistryValue\n        Root=\"HKLM\"\n        Key=\"SOFTWARE\\!(wix.AppGUID)\"\n        Name=\"EnvironmentVariablesSet\"\n        Value=\"yes\"\n        Type=\"string\"\n        KeyPath=\"yes\"\n      />\n      <Environment Id=\"PathWindowsSystemBin\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"yes\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\win32\\bin\\\" />\n      <Environment Id=\"PathWindowsSystemDockerCLIPlugins\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"yes\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\win32\\docker-cli-plugins\\\" />\n      <Environment Id=\"PathLinuxSystemBin\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"yes\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\linux\\bin\\\" />\n      <Environment Id=\"PathLinuxSystemDockerCLIPlugins\" Name=\"PATH\"\n        Action=\"set\" Part=\"last\" System=\"yes\" Permanent=\"no\"\n        Value=\"[APPLICATIONFOLDER]resources\\resources\\linux\\docker-cli-plugins\\\" />\n    </Component>\n\n\n    <!-- Add Admin install registry element -->\n    <Component Id=\"AdminInstallRegistry\" Directory=\"APPLICATIONFOLDER\" Guid=\"752E8274-9706-4B97-8533-1E7C5080320A\">\n      <Condition>\n      <![CDATA[MSIINSTALLPERUSER <> 1]]>\n      </Condition>\n      <RegistryValue\n        Root=\"HKLM\"\n        Key='SOFTWARE\\SUSE\\RancherDesktop'\n        Name=\"AdminInstall\"\n        Value=\"true\"\n        Type=\"string\"\n        KeyPath=\"yes\"\n      />\n    </Component>\n\n    <!-- Add FirewallException element -->\n    <Component Id=\"AdminInstallFirewall\" Directory=\"APPLICATIONFOLDER\" Guid=\"B03D3C75-F835-47A9-8224-1D1238B4F74F\" KeyPath=\"yes\">\n      <Condition>\n      <![CDATA[MSIINSTALLPERUSER <> 1]]>\n      </Condition>\n      <fire:FirewallException\n        Id=\"HostSwitchFirewallPrivateException\"\n        Name=\"Rancher Desktop Networking Private Exception\"\n        Description=\"Rancher Desktop host-switch.exe profile exception for private networks\"\n        Program=\"[APPLICATIONFOLDER]resources\\resources\\win32\\internal\\host-switch.exe\"\n        Profile=\"private\"\n        Scope=\"any\"\n        Protocol=\"tcp\" />\n      <fire:FirewallException\n        Id=\"HostSwitchFirewallDomainException\"\n        Name=\"Rancher Desktop Networking Domain Exception\"\n        Description=\"Rancher Desktop host-switch.exe profile exception for domain networks\"\n        Program=\"[APPLICATIONFOLDER]resources\\resources\\win32\\internal\\host-switch.exe\"\n        Profile=\"domain\"\n        Scope=\"any\"\n        Protocol=\"tcp\" />\n    </Component>\n\n    <!-- If required, run the app after install (for updates) -->\n    <Property Id=\"RDRUNAFTERINSTALL\" Secure=\"yes\" />\n    <CustomAction\n      Id=\"RunApplication\"\n      FileKey=\"mainExecutable\"\n      ExeCommand=\"\"\n      Execute=\"commit\"\n      Impersonate=\"yes\"\n      Return=\"asyncNoWait\"\n    />\n    <InstallExecuteSequence>\n      <Custom Action=\"RunApplication\" Before=\"InstallFinalize\">\n        RDRUNAFTERINSTALL\n      </Custom>\n    </InstallExecuteSequence>\n\n    <Feature Id=\"ProductFeature\" Absent=\"disallow\">\n      <ComponentGroupRef Id=\"ProductComponents\" />\n      <ComponentRef Id=\"PathUser\" />\n      <ComponentRef Id=\"PathSystem\" />\n      <ComponentRef Id=\"AdminInstallRegistry\" />\n      <ComponentRef Id=\"AdminInstallFirewall\" />\n    </Feature>\n  </Product>\n  {{ &fileList }}\n</Wix>\n"
  },
  {
    "path": "build/wix/scope.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--\n  - This describes the install scope dialog; we are customizing this one to\n  - emphasize per-machine installation, as that is required for privileged\n  - service. (If WSL needs to be installed, this dialog is skipped and we\n  - always install per-machine.)\n  -->\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n  <Fragment>\n    <UI>\n      <Dialog Id=\"RDInstallScopeDlg\" Width=\"370\" Height=\"270\" Title=\"!(loc.InstallScopeDlg_Title)\" KeepModeless=\"yes\">\n        <Control Id=\"Title\" Type=\"Text\" X=\"15\" Y=\"6\" Width=\"200\" Height=\"15\" Transparent=\"yes\" NoPrefix=\"yes\" Text=\"!(loc.InstallScopeDlgTitle)\" />\n        <Control Id=\"Description\" Type=\"Text\" X=\"25\" Y=\"23\" Width=\"280\" Height=\"20\" Transparent=\"yes\" NoPrefix=\"yes\" Text=\"!(loc.InstallScopeDlgDescription)\" />\n        <Control Id=\"BannerBitmap\" Type=\"Bitmap\" X=\"0\" Y=\"0\" Width=\"370\" Height=\"44\" Text=\"!(loc.InstallScopeDlgBannerBitmap)\" />\n        <Control Id=\"BannerLine\" Type=\"Line\" X=\"0\" Y=\"44\" Width=\"370\" Height=\"0\" />\n\n        <Control Id=\"BothScopes\" Type=\"RadioButtonGroup\" Property=\"MSIINSTALLPERUSER\"\n          X=\"20\" Y=\"55\" Width=\"330\" Height=\"120\" Hidden=\"yes\">\n          <RadioButtonGroup Property=\"MSIINSTALLPERUSER\">\n            <RadioButton Value=\"0\"\n              Text=\"!(loc.InstallScopeDlgPerMachine)\"\n              X=\"0\" Y=\"0\" Width=\"295\" Height=\"16\" />\n            <RadioButton Value=\"1\"\n              Text=\"!(loc.InstallScopeDlgPerUser)\"\n              X=\"0\" Y=\"72\" Width=\"295\" Height=\"16\" />\n          </RadioButtonGroup>\n          <Condition Action=\"show\">Privileged AND WSLINSTALLED</Condition>\n        </Control>\n\n        <Control Id=\"PerMachineDescription\" Type=\"Text\" Hidden=\"yes\"\n          NoPrefix=\"yes\" Text=\"!(loc.InstallScopeDlgPerMachineDescription)\"\n          X=\"33\" Y=\"70\" Width=\"300\" Height=\"48\">\n          <Condition Action=\"show\">Privileged</Condition>\n        </Control>\n        <Control Id=\"PerUserDescription\" Type=\"Text\"\n          NoPrefix=\"yes\" Text=\"!(loc.InstallScopeDlgPerUserDescription)\"\n          X=\"33\" Y=\"143\" Width=\"300\" Height=\"48\" />\n\n        <Control Id=\"BottomLine\" Type=\"Line\" X=\"0\" Y=\"234\" Width=\"370\" Height=\"0\" />\n        <Control Id=\"Back\" Type=\"PushButton\" Text=\"!(loc.WixUIBack)\"\n          X=\"180\" Y=\"243\" Width=\"56\" Height=\"17\">\n          <Publish Event=\"NewDialog\" Value=\"RDWelcomeDlg\">1</Publish>\n        </Control>\n        <Control Id=\"Next\" Type=\"PushButton\" Default=\"yes\" Text=\"!(loc.WixUINext)\"\n          X=\"236\" Y=\"243\" Width=\"56\" Height=\"17\">\n          <Publish Order=\"1\" Event=\"NewDialog\" Value=\"RDVerifyReadyDlg\">1</Publish>\n        </Control>\n        <Control Id=\"Cancel\" Type=\"PushButton\" X=\"304\" Y=\"243\" Width=\"56\" Height=\"17\" Cancel=\"yes\" Text=\"!(loc.WixUICancel)\">\n          <Publish Event=\"SpawnDialog\" Value=\"CancelDlg\">1</Publish>\n        </Control>\n      </Dialog>\n    </UI>\n  </Fragment>\n</Wix>\n"
  },
  {
    "path": "build/wix/string-overrides.wxl",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<WixLocalization Culture=\"en-US\" Codepage=\"1252\" xmlns=\"http://schemas.microsoft.com/wix/2006/localization\">\n  <!-- White space is preserved, so we must not line wrap. -->\n  <String Id=\"InstallScopeDlgPerMachineDescription\" Overridable=\"yes\">[ProductName] will be installed in a per-machine folder and be available for all users. This enables listening on non-loopback ports and other features that require additional privileges. You must have local Administrator privileges.</String>\n  <String Id=\"InstallScopeDlgPerUserDescription\">[ProductName] will be installed in a per-user folder and be available just for your user account. You do not need local Administrator privileges. This will disable support for listening on non-loopback ports, and requires WSL2 to already be installed on your machine.</String>\n  <String Id=\"InstallScopeDlgNoPerUserDescription\" Overridable=\"yes\">[ProductName] cannot be installed per-user, as Windows Subsystem for Linux 2 was not found. Continuing to install [ProductName] will also install WSL2.</String>\n  <String Id=\"Error_WSLNotInstalled\" Overridable=\"yes\">[ProductName] requires Windows Subsystem for Linux 2 (WSL2) to be installed as a prerequisite. Please follow the instructions at https://aka.ms/wslinstall</String>\n</WixLocalization>\n"
  },
  {
    "path": "build/wix/verify.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n  <Fragment>\n    <UI>\n      <Dialog Id=\"RDVerifyReadyDlg\" Title=\"!(loc.VerifyReadyDlg_Title)\"\n        Width=\"370\" Height=\"270\" TrackDiskSpace=\"yes\">\n        <Control Id=\"InstallTitle\" Type=\"Text\" NoPrefix=\"yes\"\n          X=\"15\" Y=\"15\" Width=\"300\" Height=\"15\" Transparent=\"yes\"\n          Text=\"!(loc.VerifyReadyDlgInstallTitle)\" />\n        <Control Id=\"BannerBitmap\" Type=\"Bitmap\"\n          X=\"0\" Y=\"0\" Width=\"370\" Height=\"44\"\n          Text=\"!(loc.VerifyReadyDlgBannerBitmap)\" />\n        <Control Id=\"BannerLine\" Type=\"Line\"\n          X=\"0\" Y=\"44\" Width=\"370\" Height=\"0\" />\n\n        <Control Id=\"InstallText\" Type=\"Text\"\n          X=\"25\" Y=\"70\" Width=\"320\" Height=\"80\" NoPrefix=\"yes\"\n          Text=\"!(loc.VerifyReadyDlgInstallText)\" />\n\n        <Control Id=\"BottomLine\" Type=\"Line\" X=\"0\" Y=\"234\" Width=\"370\" Height=\"0\" />\n        <Control Id=\"Back\" Type=\"PushButton\" X=\"156\" Y=\"243\" Width=\"56\" Height=\"17\" Text=\"!(loc.WixUIBack)\">\n          <Publish Order=\"1\" Event=\"NewDialog\" Value=\"RDInstallScopeDlg\" />\n        </Control>\n\n        <Control Id=\"Install\" Type=\"PushButton\" ElevationShield=\"yes\"\n          X=\"212\" Y=\"243\" Width=\"80\" Height=\"17\"\n          Default=\"yes\" Hidden=\"yes\"\n          Text=\"!(loc.VerifyReadyDlgInstall)\">\n          <Condition Action=\"show\">\n            <![CDATA[ MSIINSTALLPERUSER <> 1]]>\n          </Condition>\n          <Publish Order=\"1\" Property=\"MSIINSTALLPERUSER\" Value=\"{}\">1</Publish>\n          <Publish Order=\"2\" Property=\"ALLUSERS\" Value=\"1\">1</Publish>\n          <Publish Order=\"3\" Event=\"EndDialog\" Value=\"Return\">\n            <![CDATA[OutOfDiskSpace <> 1]]>\n          </Publish>\n          <Publish Order=\"4\" Event=\"SpawnDialog\" Value=\"OutOfRbDiskDlg\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND (PROMPTROLLBACKCOST=\"P\" OR NOT PROMPTROLLBACKCOST)</Publish>\n          <Publish Order=\"5\" Event=\"EndDialog\" Value=\"Return\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=\"D\"</Publish>\n          <Publish Order=\"6\" Event=\"EnableRollback\" Value=\"False\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=\"D\"</Publish>\n          <Publish Order=\"7\" Event=\"SpawnDialog\" Value=\"OutOfDiskDlg\">(OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 1) OR (OutOfDiskSpace = 1 AND PROMPTROLLBACKCOST=\"F\")</Publish>\n        </Control>\n        <Control Id=\"InstallNoShield\" Type=\"PushButton\" ElevationShield=\"no\"\n          X=\"212\" Y=\"243\" Width=\"80\" Height=\"17\"\n          Default=\"yes\" Hidden=\"yes\"\n          Text=\"!(loc.VerifyReadyDlgInstall)\">\n          <Condition Action=\"show\">MSIINSTALLPERUSER = 1</Condition>\n          <Publish Order=\"1\" Property=\"ALLUSERS\" Value=\"{}\">1</Publish>\n          <!--\n            - When running with full UI, the installer initially assumes we're\n            - going to install per-machine. This means that when we looked for\n            - older products to upgrade from (by matching UpgradeCode), per-user\n            - installs were ignored. Now that we have confirmed we're about to\n            - install per-user, re-do FindRelatedProducts so that we can find\n            - any existing installs we need to upgrade from.\n           -->\n          <Publish Order=\"2\" Event=\"DoAction\" Value=\"FindRelatedProducts\">1</Publish>\n          <Publish Order=\"3\" Event=\"EndDialog\" Value=\"Return\">\n            <![CDATA[OutOfDiskSpace <> 1]]>\n          </Publish>\n          <Publish Order=\"4\" Event=\"SpawnDialog\" Value=\"OutOfRbDiskDlg\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND (PROMPTROLLBACKCOST=\"P\" OR NOT PROMPTROLLBACKCOST)</Publish>\n          <Publish Order=\"5\" Event=\"EndDialog\" Value=\"Return\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=\"D\"</Publish>\n          <Publish Order=\"6\" Event=\"EnableRollback\" Value=\"False\">OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=\"D\"</Publish>\n          <Publish Order=\"7\" Event=\"SpawnDialog\" Value=\"OutOfDiskDlg\">(OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 1) OR (OutOfDiskSpace = 1 AND PROMPTROLLBACKCOST=\"F\")</Publish>\n        </Control>\n\n        <Control Id=\"Cancel\" Type=\"PushButton\" Cancel=\"yes\"\n          X=\"304\" Y=\"243\" Width=\"56\" Height=\"17\" Text=\"!(loc.WixUICancel)\">\n          <Publish Event=\"SpawnDialog\" Value=\"CancelDlg\">1</Publish>\n        </Control>\n      </Dialog>\n    </UI>\n  </Fragment>\n</Wix>\n"
  },
  {
    "path": "build/wix/welcome.wxs",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!-- This describes the welcome dialog -->\n<Wix xmlns=\"http://schemas.microsoft.com/wix/2006/wi\">\n  <Fragment>\n    <UI>\n      <Dialog Id=\"RDWelcomeDlg\" Width=\"370\" Height=\"270\" Title=\"!(loc.WelcomeEulaDlg_Title)\">\n        <Control Id=\"Bitmap\" Type=\"Bitmap\" X=\"0\" Y=\"0\" Width=\"370\" Height=\"234\"\n          TabSkip=\"no\" Text=\"!(loc.WelcomeEulaDlgBitmap)\" />\n        <Control Id=\"Title\" Type=\"Text\" X=\"130\" Y=\"6\" Width=\"225\" Height=\"30\"\n          Transparent=\"yes\" NoPrefix=\"yes\" Text=\"!(loc.WelcomeEulaDlgTitle)\" />\n        <Control Id=\"BottomLine\" Type=\"Line\"\n          X=\"0\" Y=\"234\" Width=\"370\" Height=\"0\" />\n        <Control Id=\"LicenseAcceptedCheckBox\" Type=\"CheckBox\"\n          X=\"130\" Y=\"207\" Width=\"226\" Height=\"18\"\n          CheckBoxValue=\"1\" Property=\"RDLicenseAccepted\"\n          Text=\"!(loc.WelcomeEulaDlgLicenseAcceptedCheckBox)\" />\n        <Control Id=\"Back\" Type=\"PushButton\" Disabled=\"yes\"\n          X=\"180\" Y=\"243\" Width=\"56\" Height=\"17\" Text=\"!(loc.WixUIBack)\" />\n        <Control Id=\"Next\" Type=\"PushButton\" Default=\"yes\"\n          X=\"236\" Y=\"243\" Width=\"56\" Height=\"17\" Text=\"!(loc.WixUINext)\">\n          <Condition Action=\"disable\">\n            <![CDATA[RDLicenseAccepted <> \"1\"]]>\n          </Condition>\n          <Condition Action=\"enable\">RDLicenseAccepted = \"1\"</Condition>\n          <!-- If WSL is not installed, we abort the installation now as we are\n            - no longer able to install WSL as part of our process.\n            -->\n          <Publish Event=\"NewDialog\" Order=\"1\" Value=\"RDInstallScopeDlg\">1</Publish>\n          <Publish Event=\"DoAction\" Order=\"2\" Value=\"ErrorWSLNotInstalled\">NOT WSLINSTALLED</Publish>\n        </Control>\n        <Control Id=\"Cancel\" Type=\"PushButton\" Cancel=\"yes\"\n          X=\"304\" Y=\"243\" Width=\"56\" Height=\"17\" Text=\"!(loc.WixUICancel)\">\n          <Publish Event=\"SpawnDialog\" Value=\"CancelDlg\">1</Publish>\n        </Control>\n        <Control Id=\"LicenseText\" Type=\"ScrollableText\" Sunken=\"yes\" TabSkip=\"no\"\n          X=\"130\" Y=\"36\" Width=\"226\" Height=\"162\">\n          <Text SourceFile=\"$(var.licenseFile)\" />\n        </Control>\n      </Dialog>\n    </UI>\n\n    <InstallUISequence>\n      <!-- Only show the welcome dialog if we're not repairing, upgrading, or removing. -->\n      <Show Dialog=\"RDWelcomeDlg\" Before=\"ProgressDlg\" Overridable=\"yes\">NOT Installed AND NOT WIX_UPGRADE_DETECTED AND NOT REMOVE</Show>\n    </InstallUISequence>\n  </Fragment>\n</Wix>\n"
  },
  {
    "path": "dev-app-update.yml",
    "content": "owner: rancher-sandbox\nrepo: rancher-desktop\nupdaterCacheDirName: rancher-desktop\nprovider: custom\nupgradeServer: https://desktop.version.rancher.io/v1/checkupgrade\nvPrefixedTagName: true\n"
  },
  {
    "path": "docs/development/README.md",
    "content": "# Developer Documentation\n\nNote that the below table of contents may be out of date\n(we may forget to update it). If you don't find what you need,\nask around!\n\n[Information About Factory Reset](factory-reset.md)  \n[Feature Tracker](features.md)  \n[Tips for Working with OBS](obs.md)  \n[Linux Release Process](linux-release-process.md)  \n[Release Checklist](release-checklist.md)  \n[Signing Rancher Desktop Releases](signing.md)  \n[Generating Screenshots for User Documentation](../../screenshots/README.md)  \n[Information on how to setup and run BATS tests](../../bats/README.md)  \n"
  },
  {
    "path": "docs/development/env.md",
    "content": "# Internal Rancher Desktop environment variables\n\nThese variables are used for build and development purposes; they are not meant to be set by users.\n\nThey do not form an API and may be changed or removed at any time without prior notice.\n\n## RD_DEBUG_ENABLED=anything\n\nForces debug logging to always be enabled. Useful to debug first-run issues when there is no `settings.yaml` yet to set debug mode.\n\n## RD_FORCE_UPDATES_ENABLED=anything\n\nWhen set, it will force auto-update to be enabled even in `yarn dev` mode. Updates will be checked and downloaded, but **not** installed.\n\n## RD_MOCK_MACOS_VERSION=semver\n\nUsed for testing compatibility of the app with the OS version, for upgrade responder tests, and for enabling/disabling certain parts of the preferences (related to VZ emulation mode).\n\n## RD_UPGRADE_RESPONDER_URL=http://localhost:8314/v1/checkupgrade\n\nSet an alternate upgrade responder endpoint for testing.\n"
  },
  {
    "path": "docs/development/factory-reset.md",
    "content": "When `rdctl reset --factory` is launched from the UI, it writes its stdout into\n`TMP/rdctl-stdout.txt`\n\nwhere on linux `TMP` is usually `/tmp`,\n\non macOS it's given by `$TMPDIR`\n\nand on Windows by `%TEMP%`(command shell) or `$env:TEMP`(powershell).\n\n\nThis is most useful during development. When the UI runs in debug mode, it spawns `rdctl reset --factory` with the `--verbose` option.\n\nWe can't write the output into the `logs` directory as `reset --factory` deletes it.\n"
  },
  {
    "path": "docs/development/features.md",
    "content": "# Rancher Desktop Features\n\nThis document lists the high-level Rancher Desktop features and their current status.\n\n| Symbol | Description |\n| ------------- | ---------------- |\n| :heavy_check_mark: | released |\n| :calendar: | targeted for the [next] or the [later] milestone release |\n| :sun_with_face:| not planned yet, but considering for a future release |\n\nNote:\n- Items under the [next] milestone are targeted for the upcoming monthly release, which usually happens on the 4th Wednesday of the month.\n- Items under the [later] milestone and any spillover items from the [next] milestone are targeted for the release after.\n- Items under the [next] and [later] milestones might change based on user feedback, technical challenges, etc.\n\n[next]: https://github.com/rancher-sandbox/rancher-desktop/projects/1?card_filter_query=milestone%3Anext\n[later]: https://github.com/rancher-sandbox/rancher-desktop/projects/1?card_filter_query=milestone%3Alater\n\n### OS & Platform Support\n\n:heavy_check_mark: Win 10/11\n\n:heavy_check_mark: Mac (Intel)\n\n:heavy_check_mark: Mac M1 (apple silicon)\n\n:heavy_check_mark: Linux\n\n:sun_with_face: Linux AArch64\n\n:sun_with_face: Windows on AArch64\n\n:sun_with_face: Windows Containers\n\n### Container Engines\n\n:heavy_check_mark:  Multiple CR support (containerd, dockerd)\n\n### Docker\n\n:heavy_check_mark: CLI\n\n:heavy_check_mark: Swarm\n\n:heavy_check_mark: Compose\n\n:heavy_check_mark: Docker-only\n\n### Kubernetes\n\n:heavy_check_mark: K3s bundled\n\n:heavy_check_mark: Multiple versions support\n\n### Bundled Tooling\n\n:heavy_check_mark: Helm\n\n:sun_with_face: Kubectx\n\n:sun_with_face: [kwctl]\n\n[kwctl]: https://github.com/kubewarden/kwctl\n\n### Image Management\n\n:heavy_check_mark: Build, Push, Pull & Scan images\n\n:calendar: Registry Configuration\n\n:sun_with_face: Registry Access Control\n\n### Networking\n\n:heavy_check_mark: Simple VPN\n\n:calendar: Restricted VPN (Ex: Cisco AnyConnect)\n\n### Host Access\n\n:sun_with_face: GPU\n\n:sun_with_face: USB\n\n### Performance & System Resources\n\n:heavy_check_mark: System resource allocation\n\n:sun_with_face: Pause app to save power  \n\n### Security\n\n:heavy_check_mark: Signed builds\n\n:sun_with_face: SBOM generation for images\n\n:sun_with_face: Image Signing\n\n:sun_with_face: Attain SLSA Level\n\n### Troubleshooting\n\n:heavy_check_mark: View logs\n\n:heavy_check_mark: Partial Reset\n\n:heavy_check_mark: Factory Reset\n\n### GUI/Installation\n\n:heavy_check_mark: View Containers\n\n:heavy_check_mark: View Images\n\n:heavy_check_mark: Port forwarding\n\n:heavy_check_mark: Auto updates\n\n:heavy_check_mark: Cluster exploration - Rancher Dashboard (Preview)\n\n:sun_with_face: Container Exploration\n\n:sun_with_face: Configuration settings\n\n:sun_with_face: Start/Stop/Pause Containers\n\n:sun_with_face: Silent (No-GUI) Install\n\n:sun_with_face: CLI/Headless mode\n\n:calendar: Offline (air gap) mode\n\n:heavy_check_mark: Rancher Desktop CLI aka rdctl (Preview)\n\n### IDE Compatibility\n\n:heavy_check_mark: VS Code extension (With dockerd(moby))\n\n:sun_with_face: Visual Studio IDE (Needs Validation)\n\n:sun_with_face: Eclipse (Needs Validation)\n\n### Integration with Other Rancher Projects\n\n:heavy_check_mark: k3s\n\n:calendar: Rancher Dashboard\n\n:sun_with_face: Epinio\n\n:sun_with_face: NeuVector\n\n:sun_with_face: Marketplace\n\n:sun_with_face: Kubewarden\n\n### Development\n\n:heavy_check_mark: Open source\n\n:heavy_check_mark: Public roadmap\n"
  },
  {
    "path": "docs/development/linux-release-process.md",
    "content": "# Linux Release Process\n\n**Note**: please read the [OBS Tips Documentation](obs.md)\nbefore this document. It includes information that is important to be familiar\nwith when working with OBS.\n\n\n## When do I need to modify OBS?\n\nOBS is set up so that you only need to act when you are releasing\na new major or minor version of Rancher Desktop. For example, when\nwe released 1.11.0 we had to make changes. When we released 1.11.1\nnothing had to be done other than the usual checks.\n\n\n## How do I modify OBS when releasing a new major or minor version?\n\nBefore you begin, you must have `osc` set up. Once you have that done,\nyou can create a new package for the new major-minor version. Luckily,\nwe don't have to create a new package from scratch: we can use the\n`osc copypac` command to copy an existing package. This command has\nthe following signature:\n```\nosc copypac <source_project> <source_package> <destination_project> <destination_package>\n```\nFor example, if we wanted to copy the `rancher-desktop-release-1.11`\npackage from the `isv:Rancher:dev` project to\n`rancher-desktop-release-1.12`, also in the `isv:Rancher:dev` project,\nwe would run `osc copypac` as follows:\n```\nosc copypac isv:Rancher:dev rancher-desktop-release-1.11 isv:Rancher:dev rancher-desktop-release-1.12\n```\nOnce this is done, you must update the `_service` file and the `Meta`\ntab in the package to refer to the new major-minor version. The\neasiest way to do this is via the OBS web interface, which you will\nneed to be logged into. Generally speaking, you can simply replace\nall instances of `1.11` with `1.12` (assuming we're using the above\nexample). Of course, it is best to understand what you are changing -\nthe next section will help you with that. Once you have made these\nchanges, the services will run and the builds should start and\ncomplete successfully.\n\nFinally, you should check the results. This is important - sometimes\nthe build process falls over, sometimes VMs aren't available to build\nyour package, and so on. If you run into issues, they are usually\nresolved by triggering a rebuild in the web interface. This can\nbe done by clicking \"Trigger Services\" in the left navigation bar.\nAlternatively, you can trigger a rebuild for a specific package format\nby clicking on that package format (i.e. AppImage) from the main page\nof the package and then clicking \"Trigger rebuild\". You will need to\nbe logged into the web interface to take these actions.\n\nYou should also check that the link used to download the \"latest\"\nAppImage *actually* downloads the latest AppImage - the link is\nsometimes not updated, at least, not updated promptly.\n\n\n## How do Linux releases actually *work*?\n\n### The `dev` Channel\n\nThe `dev` channel is intended to be used by developers and perhaps\nintrepid users. It corresponds to the\n[isv:Rancher:dev OBS project](https://build.opensuse.org/project/show/isv:Rancher:dev).\n\n1. A new commit is pushed to a branch of the form `main` or `release-X.Y`\n   (for example `release-1.2` or `release-1.11`), which triggers the\n   `package.yml` github actions workflow. It builds Rancher Desktop and\n   uploads the resultant .zip file to an S3 bucket under a name of the form\n   `rancher-desktop-linux-<branch_name>.zip`.\n3. As its last step, the `package.yml` workflow triggers a service run in\n   the OBS package that corresponds to the branch that triggered the workflow\n   run.it. This causes OBS to download and unpack the .zip file that was uploaded\n   to S3 in step 2. It also causes OBS to pull some files related to the\n   package formats will build from the rancher-desktop repository.\n4. The new files trigger a build in OBS.\n5. Once the build is complete in OBS, the new versions of the packages are\n   available to users to download via `zypper install`, `apt install`, etc.\n\n\n### The `stable` Channel\n\nThe `stable` channel is where actual releases are hosted. It is\nintended for use by actual users. The `stable` channel corresponds\nto the\n[isv:Rancher:stable OBS project](https://build.opensuse.org/project/show/isv:Rancher:stable).\nThe `stable` build process is similar to the `dev` channel, but works\nslightly differently: OBS builds are triggered by published github\nreleases rather than new commits on branches of a particular format.\n\n1. A new release is published, causing the `linux-release.yml` github\n   actions workflow to run. This workflow fetches the linux .zip file\n   from the release, and uploads it to AWS S3 with a name in the format\n   `rancher-desktop-linux-X.Y.zip` (for example, `rancher-desktop-linux-1.12.zip`).\n2. The `linux-release.yml` workflow triggers a service run in the OBS\n   package that corresponds to the major and minor version of the tag\n   of the published release. This causes OBS to download and unpack the\n   .zip file uploaded to S3 in step 1. It also causes OBS to pull some files\n   related to the package formats will build from the rancher-desktop\n   repository.\n4. The new files trigger a build in OBS.\n5. Once the build is complete in OBS, the new versions of the packages are\n   available to users to download via `zypper install`, `apt install`, etc.\n"
  },
  {
    "path": "docs/development/obs.md",
    "content": "# Tips for Working with OBS\n\nThis document contains information on how to use OBS effectively.\nIf you have not used OBS before, you should read\n[Getting Started](#getting-started) and\n[Important Concepts](#important-concepts) first. Then, come back to\nthe other sections as you begin to work with the relevant parts of OBS.\n\n\n## Getting Started\n\nThe first thing you need to work with OBS is an installation of\nopenSUSE Leap. Tumbleweed may work, but given its bleeding-edge\nnature, Leap is probably a better bet.\n\nThe reason you need an installation of openSUSE is because any\nreal work you do with OBS should be done using the `osc` command\nline tool, which is only available on openSUSE. There *is* a web\ninterface, but it lacks much of the functionality that you will need.\nUse it for checking on the status of your package, and possibly small\nchanges, but for everything else use `osc`.\n\n\n## Important Concepts\n\nThere are a few concepts that one should understand in order to use OBS.\nThe way they work and interact can be unintuitive at first, so a brief\noverview is provided here.\n\nA **project** is the object in which you do everything in OBS.\nEverything falls under projects: repositories, packages, services;\nall of these things must belong to a project. Projects may have\nsubprojects, which are themselves full projects. You have to be an\nOBS admin to create a root-level project, so our project (`Rancher`)\nwas created as a subproject of the `isv` root project. Projects are\nreferred to as each of their parent projects plus their name, all\nseparated by colons. So to refer to our top-level project, you use\nthe name `isv:Rancher`.\n\nA **repository** is configured on a project. The best way to think of\nrepositories is in the context of package managers: they are a remote\nendpoint from which you can download packages. There are several types of\nrepositories - some are true repositories in the sense that tools like\n`apt` and `dnf` can be configured to use them, and others are just endpoints\nyou can download assets from. Also, you can configure multiple repositories\non each project. This is useful for building and serving packages of multiple\nformats from the same binary or source code.\n\nA **package** is also configured on a project. Conceptually, OBS packages\nare different from packages in other contexts. In OBS, a package represents\na set of files that go into a build, such as source files and any package\nmetadata files (such as rpm `.spec` files). Also, from the perspective of the\nuser's package manager, an OBS package represents exactly one version of the\npackage. So if you want to provide multiple versions of the package in each\nrepository, you must have one OBS package for each version.\n\nA **service** is basically a script that can be triggered in a few different\nways. A common use for services is to get the latest version of code from\nversion control before building and packaging that code. For more information\non services see below; also, you may find the\n[documentation for services][service_documentation] helpful.\n\n[service_documentation]: https://openbuildservice.org/help/manuals/obs-user-guide/cha.obs.source_service.html#sec.obs.sserv.about\n\n\n## Service Tips\n\n### Update your services to the latest versions\n\nBefore doing anything with services, you should ensure that you have installed\nthe latest versions of any services you want to work with. This is important\nbecause the remote version of OBS (build.opensuse.org) always uses the latest\nversion of services - if you are working with a different version on your local\nmachine, you may run into issues. Also note that services do not always (ever?)\nuse semantic versioning despite having versions of the form `X.Y.Z`.\n\nThe repositories that openSUSE comes configured with do not contain the latest\nversions of the OBS services. In order to get the latest versions you need to\nadd a repository:\n\n```\nzypper addrepo https://download.opensuse.org/repositories/openSUSE:/Tools/openSUSE_15.3/openSUSE:Tools.repo\nzypper refresh\n```\n\nAfter you do this you can install/update the services you need. If you aren't\non Leap 15.3, you may have to find a different version of this repo, but this\nis what works at the time of writing.\n\n### How to find out what services are available\n\nServices come in the form of rpm packages that can be installed via `zypper`.\nIn order to search your installed repos for services, simply run:\n\n```\nzypper search obs-service\n```\n\n### How to find out what configuration each service takes\n\nOnce services are installed you can look at their interface schema in order\nto understand how to use them. The interface schema (as well as the source code)\nare stored in the directory `/usr/lib/obs/service/`.\n\n\n## Local Build Tips\n\n### How to get around slow mirrors\n\nWhen you do a local build, the first thing `osc` does is cache any dependencies\nof the build. `osc` will download these dependencies from mirrors of their\nrepositories. Unfortunately these mirrors can be very slow. If the dependency\ncaching step is too slow, you can tell `osc build` to only fetch packages from\nthe build.opensuse.org api with the `--download-api-only` flag.\n\n### How to skip running services before build\n\nUse the `--no-service` flag on `osc build` for this.\n\n### How to find the output of a local build\n\nWhen you build locally, it is not always obvious where the output of the build\nhas been saved. To find the location of your build output, look at the text that\nthe build has printed at the screen. At the end of it there should be a path;\nthis is where you can find your built package.\n\n\n## Additional Resources\n\n- The `help-obs` and `discuss-zypp` slack channels are always friendly and helpful.\n- The [OBS documentation][obs_docs] might help resolve any problems you run into.\n- The output of `osc --help` and `osc <command> --help` may be helpful.\n- The [Using the Open Build Service][using_obs] may be helpful for understanding\n  how we build AppImages using OBS.\n\n[obs_docs]: https://openbuildservice.org/help/manuals/obs-user-guide/\n[using_obs]: https://docs.appimage.org/packaging-guide/hosted-services/opensuse-build-service.html\n"
  },
  {
    "path": "docs/development/release-checklist.md",
    "content": "## Release Checklist\n\n- [ ] Update version number in package.json if not done after last release.\n- [ ] Tag release branch. Wait for the CI to build artifacts.\n- [ ] Sign windows installer.\n\n### Sign mac installer (As there's an issue with the zip produced by the build script, we need to manually build and zip, rename the file to replace space with dot etc )\n- [ ] Make sure the required env variables are set for the notarize, signing process.\n- [ ] git clean, reset to make sure a clean (CI equivalent) build.\n- [ ] Manually zip the installer.\n- [ ] Rename installer filename to replace space with dot.\n\n### Release Documentation\n- [ ] Release notes. Update on the GitHub draft Release page.\n- [ ] docs update (Help, Readme..)\n- [ ] Slack Announcements\n- [ ] Newsletter summary\n- [ ] Update metrics, roadmap on Confluence page\n\n### Release\n- [ ] Perform smoke test on release artifacts.\n- [ ] Upload mac, win release artifacts on the GitHub draft Release page.\n- [ ] Update the release version for upgrade responder.\n- [ ] Move from draft release to Release.\n- [ ] Check the auto update functionality.\n"
  },
  {
    "path": "docs/development/signing.md",
    "content": "# Signing Releases\n\nNormally, we build artifacts via GitHub Actions as zip files, which can then be\nsigned offline.  This is necessary as the relevant certificates are not\navailable online during CI.\n\nIn general, the process involves:\n\n1. Download the zip archive from GitHub.\n\n   **Note** The archive will be a zip within a zip.  Extract one level to get\n   the file as generated by electron-builder.\n\n2. Set up the signing environment; see the platform-specific sections below.\n\n3. Run the signing tool:\n\n    ```sh\n    yarn sign path/to/archive.zip\n    ```\n\n4. Look in `dist/` for the signed files (`Rancher.Desktop.Setup.msi`, etc.).\n\n## Windows\n\nOn Windows, it is necessary to obtain a code signing certificate that can be\nused with the Windows infrastructure.  It is then necessary to determine the\nfingerprint of the certificate, and set it as the `CSC_FINGERPRINT` environment\nvariable before running `yarn sign`.\n\n### Generate a Test Certificate\n\nFor testing purposes, we can generate a certificate locally by running the\nfollowing command in PowerShell:\n\n```powershell\nNew-SelfSignedCertificate `\n    -Type Custom `\n    -Subject \"CN=Rancher-Sandbox, C=CA\" `\n    -KeyUsage DigitalSignature `\n    -CertStoreLocation Cert:\\CurrentUser\\My `\n    -FriendlyName \"Rancher-Sandbox Code Signing\" `\n    -TextExtension @(\"2.5.29.37={text}1.3.6.1.5.5.7.3.3\", \"2.5.29.19={text}\")\n```\n\nThis will display the new certificate, with a `Thumbprint` column; this is the\ncertificate fingerprint that we will need to set as `CSC_FINGERPRINT`.  If you\nneed to refer to the certificate later, it can be obtained by running\n`ls Cert:\\CurrentUser\\My` in a PowerShell prompt.\n\n### Using a Certificate Stored on a YubiKey\n\nIf you have a certificate (with private key) that is stored on a YubiKey device,\nit is first necessary to install the [YubiKey Minidriver].  After plugging in\nyour device, you should then be able to run `certutil -scinfo -silent` to locate\nthe fingerprint for your desired certificate.  Note that this will list all the\ncertificates on the device, including the chain to your certificate, and you\nmust search the output for your signing certificate and locate the\n`Cert Hash(sha1):` entry.  That hash is then used for `CSC_FINGERPRINT` above.\n\n[YubiKey Minidriver]: https://www.yubico.com/support/download/smart-card-drivers-tools/\n\n### Verifying the Signed Product\n\nIn explorer, right-click on the final `.exe` file, choose `Properties`, `Digital Signatures`,\nand verify that `Suse LLC` is listed in the Signature List.\n\n## macOS\n\nOn macOS, a signing certificate from Apple is required (via their developer\nprogram).  Please refer to [Apple Documentation] for details.  Note that a\n_Mac Development_ certificate is insufficient for notarization; it must be a\n_Developer ID Application_ certificate.  This will be reflected in the Common\nName of the certificate.\n\nLaunch constraints require macOS Ventura (macOS 13) or newer.  This is therefore\nneeded for production signing.\n\n[Apple Documentation]: https://developer.apple.com/help/account/create-certificates/create-developer-id-certificates\n\n### Generate a test certificate\n\nIf a real certificate from Apple is unavailable, it is possible to generate a\nself-signed test certificate; however, note that this wouldn't properly exercise\nall of the signing code.\n\n```sh\nopenssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \\\n          -keyform pem -sha256 -days 3650 -nodes -subj \\\n          \"/C=XX/ST=NA/L=Some Town/O=No Org/OU=No Unit/CN=RD Test Signing Key\" \\\n          -addext keyUsage=critical,digitalSignature \\\n          -addext extendedKeyUsage=critical,codeSigning\nsecurity import key.pem -t priv -A\nsecurity import cert.pem -t cert -A\nsecurity set-key-partition-list -S apple-tool:,apple:,codesign: -s\nsecurity add-trusted-cert -p codeSign cert.pem\n```\n\n### Configuring Access\n\n- Import your signing certificate into your macOS Keychain.\n- Run `security find-identity -v` to locate the fingerprint of the key to use.\n  Export the long hex string as the `CSC_FINGERPRINT` environment variable.\n  - For a test certificate, use `security find-identity` without `-v`; the\n    certificate to use isn't valid.\n\nFor notarization, the following environment variables are also needed:\n\n- `APPLEID`\n  - This is your Apple ID login; for example, `john.doe@example.com`\n- `AC_PASSWORD`\n  - This is an application-specific password for your Apple ID; to create it:\n    1. Navigate to https://appleid.apple.com/account/manage\n    2. Click on _App-Specific Passwords_ at the bottom.\n    3. Create one (with a label of your choice) and copy the resulting password.\n- `AC_TEAMID`\n  - This is the Apple Team ID.  This is the _Organizational Unit (OU)_ field of\n    the subject of your signing certificate; for Rancher Desktop / SUSE, this is\n    `2Q6FHJR3H3`. <!-- spellcheck-ignore-line -->\n    (This value can be extracted from the published application.)\n\n### Performing signing\n\nWhen signing for M1/aarch64, please set the `M1` environment variable ahead of\ntime as usual.\n\nIf notarization is not required, append `--skip-notarize` to the command:\n\n  ```sh\n  yarn sign --skip-notarize path/to/archive.zip\n  ```\n\nThis is necessary to test the signing flow (since there's no way to notarize\nwithout the production certificate).  This is also necessary to use a test\ncertificate (since Apple will reject it).\n\nWhen using an older version of macOS (12/Monterey or older),\n`--skip-constraints` is also needed to skip assigning launch constraints, as\nthat requires Ventura or later.  This is inappropriate for the actual release.\n"
  },
  {
    "path": "docs/networking/windows/README.md",
    "content": "\n# Rancher Desktop Network Documentation\n\nThe table of contents below provides references to all the projects that comprise the Rancher Desktop network stack on windows platform.\n\n- [Rancher Desktop Guest Agent](rancher-desktop-guest-agent.md)\n- [Rancher Desktop Networking](rancher-desktop-networking.md)\n\n## Feature Parity\n\nBelow is table to demonstrate the feature parity between both classic networking and tunneled networking.\n\n<table>\n<tr>\n<th rowspan=2 colspan=2>feature</th>\n<th colspan=2>classic networking</th>\n<th colspan=2>tunneled network</th>\n</tr>\n<tr>\n<th>admin</th>\n<th>non-admin</th>\n<th>admin</th>\n<th>non-admin</th>\n</tr>\n<tr>\n<td rowspan=2>Docker port forwarding</td>\n<td>localhost</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>0.0.0.0</td>\n<td>✅</td>\n<td>🚫</td>\n<td>✅</td>\n<td>🚫</td>\n</tr>\n<tr>\n<td rowspan=2>Containerd port forwarding</td>\n<td>localhost</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>0.0.0.0</td>\n<td>✅</td>\n<td>🚫</td>\n<td>✅</td>\n<td>🚫</td>\n</tr>\n<tr>\n<td rowspan=2>Kubernetes port forwarding</td>\n<td>localhost</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>0.0.0.0</td>\n<td>✅</td>\n<td>🚫</td>\n<td>✅</td>\n<td>🚫</td>\n</tr>\n<tr>\n<td rowspan=2>iptables port forwarding</td>\n<td>localhost</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>0.0.0.0</td>\n<td>✅</td>\n<td>🚫</td>\n<td>✅</td>\n<td>🚫</td>\n</tr>\n<tr>\n<td>WSL integration</td>\n<td>localhost</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n<tr>\n<td>VPN support</td>\n<td>N/A</td>\n<td>🚫</td>\n<td>🚫</td>\n<td>✅</td>\n<td>✅</td>\n</tr>\n</table>"
  },
  {
    "path": "docs/networking/windows/rancher-desktop-guest-agent.md",
    "content": "# **[Rancher Desktop Guest Agent](../../../src/go/guestagent)**\n\nThe Rancher Desktop Guest Agent operates within the Rancher Desktop WSL distribution, particularly in an isolated namespace when the network tunnel is enabled. It facilitates interactions between various container engine APIs like Moby, containerd, and Kubernetes. The agent monitors container/service creation events from these APIs and, upon detecting ports needing exposure, forwards the port mappings to internal services accordingly. This ensures efficient and automated port forwarding management within the Rancher Desktop environment.\n\n```mermaid\nflowchart  LR;\nsubgraph Host[\"HOST\"]\nhost-switch[\"host-switch\"]\nend\n subgraph VM[\"WSL\"]\n  subgraph netNs[\"Isolated Network Namespace\"]\n   guest-agent[\"Guest Agent\"]\n   docker((\"Docker API\"))\n   containerd((\"Containerd API\"))\n   kubernetes((\"K8s API\"))\n   iptables((\"iptable scanning\"))\n   guest-agent <----> docker\n   guest-agent <----> containerd\n   guest-agent <----> kubernetes\n   guest-agent <----> iptables\n   guest-agent ----> host-switch\n  end\n  subgraph defaultNs[\"Default Namespace\"]\n  wsl-proxy[\"wsl-proxy\"]\n  end\n  guest-agent ----> |UNIX socket| wsl-proxy\n end\n```\n\n### Supported Flags\n\n-   **debug**: Enables debug logging.\n\n-   **docker**: When this flag is enabled, port mapping via docker API monitoring is enabled. See the port mapping and Docker sections below for details.\n\n-   **kubernetes**: Enables Kubernetes service port forwarding. When enabled, the Rancher Desktop Guest Agent creates a watcher for the Kubernetes API, monitoring NodePort and LoadBalancer services needing port forwarding. For services with exposed ports, the agent creates corresponding port mappings, forwarding them to Rancher Desktop Networking’s `host-switch`, which hosts an API for exposing ports from the host into the network namespace. If WSL integration is enabled, the port mapping is also forwarded to Rancher Desktop Networking’s `wsl-proxy`, allowing access from other WSL distributions.\n\n-   **kubeconfig**: Specifies the path to `kubeconfig` for locating the Kubernetes API endpoint. By default, it looks in `/etc/rancher/k3s/k3s.yaml`.\n\n-   **iptables**: This flag enables the scanning of iptables. In newer versions of Kubernetes, kubelet no longer creates listeners for NodePort and LoadBalancer services. To rectify this, we manually create those listeners so the port forwarding functions correctly. The guest agent creates a corresponding port mapping that represents the service’s exposed port. The port mapping is then forwarded to Rancher Desktop Networking’s `host-switch`, which hosts an API for exposing ports from the host into the network namespace. If WSL integration options are enabled within Rancher Desktop, a copy of that port mapping is also forwarded to Rancher Desktop Networking’s `wsl-proxy`. The `wsl-proxy` exposes the service port to enable users to access it from other WSL distros.\n\n-   **containerd**: When this flag is enabled, the guest agent monitors container events from the containerd API. It connects to the Containerd API via the containerd socket (`/run/k3s/containerd/containerd.sock`). Whenever a container is created or deleted, if there are exposed ports associated with that container, the guest agent creates a corresponding port mapping. This port mapping is then forwarded to Rancher Desktop Networking's `host-switch`, which hosts an API for exposing ports from the host into the network namespace. If WSL integration options are enabled within Rancher Desktop, a copy of this port mapping is also forwarded to Rancher Desktop Networking's `wsl-proxy`. The `wsl-proxy` exposes the container's port to enable users to access it from other WSL distros.\n\n-   **containerdSock**: File path for the containerd socket address. If no argument is provided, it defaults to `/run/k3s/containerd/containerd.sock`.\n\n-   **vtunnelAddr**: Peer address for the Vtunnel process that forwards port mappings to the Vtunnel Host process over `AF_VSOCK`. This feature will soon be deprecated.\n\n-   **k8sServiceListenerAddr**: Specifies an IP address (`0.0.0.0` or `127.0.0.1`) to bind Kubernetes services on the host.\n\n-   **adminInstall**: This flag indicates whether Rancher Desktop is installed with administrator privileges. It is used to enable Network Tunnel mode, where port mappings are forwarded to Rancher Desktop Networking's `host-switch`. The `host-switch` hosts an API that exposes ports from the host into the network namespace.\n\n-   **k8sAPIPort**: Specifies the Kubernetes API port, which is forwarded to `wsl-proxy` to allow other distros that are part of WSL integrations to  interact via `kubectl`.\n\n## PortMapping\n\nIs a struct object that represents an exposed container or a service. [Portmapping](../../../src/go/guestagent/pkg/types/portmapping.go#L23) objects consist of the following fields:\n\n```\ntype PortMapping struct {\n\t// Remove indicates whether to remove or add the entry\n\tRemove bool `json:\"remove\"`\n\t// Ports are the port mappings for both IPV4 and IPV6\n\tPorts nat.PortMap `json:\"ports\"`\n\t// ConnectAddrs are the backend addresses to connect to\n\tConnectAddrs []ConnectAddrs `json:\"connectAddrs\"`\n}\n```\n## Networking Mode\n\nRancher Desktop Guest Agent can operate in one of two networking modes, depending on startup arguments:\n\n**-adminInstall**\n\nThe Network Tunnel mode allows the Guest Agent to operate in an isolated network namespace with a dedicated iptables. This mode is enabled through Rancher Desktop Networking.\n\nNone  **-adminInstall**\n\nRancher Desktop Guest Agent operates in non-admin user mode. In this mode, all port mappings are bound to localhost, and the use of privileged ports is restricted.\n\n\n## Containerd\n\nWhen containerd mode is enabled, the guest agent monitors the containerd API for the following container events:\n```\n/tasks/start\n/containers/update\n/tasks/exit\n```\nIf it detects any exposed ports associated with a container, it creates a port mapping object. Depending on the selected network mode, the port mapping object is then forwarded to the host. If the privileged service is enabled, it utilizes the vtunnel peer process to communicate the port mappings with privileged services. Alternatively, if network tunnel mode is enabled, it sends the port mappings to the API offered in the host switch process.\n\nIf network tunnel mode is enabled along with the WSL integration option, a copy of the port mapping is also forwarded to the WSL proxy process, enabling access to the exposed port from other distributions.\n\n## Docker\n\nSimilar to containerd mode, when Docker mode is enabled, the guest agent watches Docker API with the following container events filter:\n```\nFilters: filters.NewArgs(\n    filters.Arg(\"type\", \"container\"),\n    filters.Arg(\"event\", startEvent),\n    filters.Arg(\"event\", stopEvent),\n    filters.Arg(\"event\", dieEvent)\n),\n```\nIf it detects any exposed ports associated with a container, it creates a port mapping object. Depending on the selected network mode, the port mapping object is then forwarded to the host. If the privileged service is enabled, it uses the vtunnel peer process to communicate the port mappings with privileged services. Otherwise, if network tunnel mode is enabled, it sends the port mappings to the API offered in the host switch process.\n\nIf network tunnel mode is enabled along with the WSL integration option, a copy of the port mapping is also forwarded to the `wsl-proxy` process, allowing access to the exposed port from other distributions.\n\nAdditionally, Docker mode creates a series of iptables rules associated with the `PREROUTING` and `POSTROUTING` chains.\n\nThe `PREROUTING` rule rewrites the destination IP address of any packets received by the local system and destined for `192.168.127.2` to `127.0.0.1`. Meanwhile, the `POSTROUTING` chain rule rewrites the source IP address of any packets being sent out through the eth0 network interface to the IP address of that interface (eth0). These rules are necessary because when the port binding is set to `127.0.0.1`, an additional `DNAT` rule is added in the main `DOCKER` chain after the existing rule using `--append`.\n\nThis adjustment is essential because the initial `DOCKER DNAT` rule created by Docker only allows traffic to be routed to `localhost` from `localhost`. Therefore, an additional rule is added to permit traffic to any destination IP address, enabling the service to be discoverable through the namespaced network's subnet.\n\nThese changes are necessary as the traffic is routed via the vm-switch over the tap network. The existing `DNAT` rule is as follows:\n\n```\nDNAT tcp -- anywhere localhost tcp dpt:9119 to:10.4.0.22:80\n```\nThe following rule is added after the existing rule:\n```\nDNAT tcp -- anywhere anywhere tcp dpt:9119 to:10.4.0.22:80\n```\n## Kubernetes\n\nWhen this option is enabled, the Rancher Desktop guest agent uses the Kubernetes service watcher to subscribe to the Kubernetes API for any services of type NodePort and LoadBalancer that require port exposure. If the service watcher detects such services, it creates a port mapping object representing each service. The port mapping object is then forwarded to the host based on the selected network mode. If the privileged service is enabled, it uses the vtunnel peer process to communicate the port mappings with privileged services. Otherwise, if network tunnel mode is enabled, it sends the port mappings to the API provided in the host switch process.\n\nIt is important to note that if the `k8sServiceListenerAddr` flag is provided, the specified IP address (either `0.0.0.0` or `127.0.0.1`) is used to bind the Kubernetes services on the host.\n\nAdditionally, if network tunnel mode is enabled along with the WSL integration option, a copy of the port mapping is also forwarded to the `wsl-proxy` process to allow access to the exposed port from other distributions. However, when the Kubernetes option is enabled, the guest agent statically emits a port mapping to the `wsl-proxy` process in the default network. This port mapping represents the Kubernetes API port (`6443`) to allow access to the Kubernetes API from other distributions.\n\nBelow port mapping is an example of what is emitted to `wsl-proxy`:\n```\ntypes.PortMapping {\n    Remove: false,\n    Ports: nat.PortMap {\n        port: [] nat.PortBinding {\n            {\n                HostIP: \"127.0.0.1\",\n                HostPort: 6443,\n            },\n        },\n    },\n}\n```\n\n## iptables\n\nIn [newer versions](https://github.com/rancher-sandbox/rancher-desktop/blob/bb7f71f18828c45b711d6d4982a2dcaf19f8f3fa/pkg/rancher-desktop/backend/k3sHelper.ts#L1152) of Kubernetes, kubelet no longer automatically creates listeners for NodePort and LoadBalancer services. To address this, we manually create these listeners to ensure proper port forwarding functionality. Service ports requiring forwarding are identified in iptables DNAT. When iptables identifies such ports, it creates a port mapping object representing that service. Depending on the selected network mode, the port mapping object is then forwarded to the host. If the privileged service is enabled, it uses the vtunnel peer process to communicate the port mappings with privileged services. Otherwise, if network tunnel mode is enabled, it sends the port mappings to the API provided by the host switch process.\n\nIf network tunnel mode is enabled along with the WSL integration option, a copy of the port mapping is also forwarded to the `wsl-proxy` process, allowing access to the exposed port from other distributions.\n\n## Port forwarding (Network Tunnel)\n\n```mermaid\nsequenceDiagram\n  box VM\n    participant dockerd\n    participant containerd\n    participant kubernetes\n    participant iptables\n    participant guest-agent\n    participant wsl-proxy\n  end\n  box Host\n    participant host-switch as host-switch.exe\n  end\n  rect transparent\n    note over dockerd,containerd: adding port\n    alt containerd\n      containerd ->> guest-agent: /tasks/start\n      guest-agent ->> guest-agent: loopback iptables\n    else dockerd\n      dockerd ->> guest-agent: event[start]\n      guest-agent ->> guest-agent: loopback iptables\n    else kubernetes\n      kubernetes ->> guest-agent: event[not deleted]\n    else iptables\n      iptables ->> iptables: poll iptables\n      iptables ->> guest-agent: add new ports\n    end\n\n    guest-agent ->> wsl-proxy: add port\n    wsl-proxy ->> wsl-proxy: listen in default namespace\n    guest-agent ->> host-switch: add port (APITracker)\n    host-switch ->> host-switch: add port via gvisor\n\n  end\n  rect transparent\n    note over dockerd, containerd: updating port\n    alt containerd\n      containerd ->> guest-agent: /containers/update\n    end\n\n    guest-agent ->> wsl-proxy: remove port\n    wsl-proxy ->> wsl-proxy: remove listener in default namespace\n    guest-agent ->> host-switch: remove port (APITracker)\n    host-switch ->> host-switch: remove port via gvisor\n    guest-agent ->> wsl-proxy: add port\n    wsl-proxy ->> wsl-proxy: listen in default namespace\n    guest-agent ->> host-switch: add port (APITracker)\n    host-switch ->> host-switch: add port via gvisor\n\n  end\n  rect transparent\n    note over dockerd,containerd: removing port\n    alt containerd\n      containerd ->> guest-agent: /tasks/exit\n    else dockerd\n      dockerd ->> guest-agent: event[stop]\n      dockerd ->> guest-agent: event[die]\n    else kubernetes\n      kubernetes ->> guest-agent: event[deleted]\n    else iptables\n      iptables ->> iptables: poll iptables\n      iptables ->> guest-agent: remove old ports\n    end\n\n    guest-agent ->> wsl-proxy: remove port\n    wsl-proxy ->> wsl-proxy: remove listener in default namespace\n    guest-agent ->> host-switch: remove port (APITracker)\n    host-switch ->> host-switch: remove port via gvisor\n\n  end\n\n```\n"
  },
  {
    "path": "docs/networking/windows/rancher-desktop-networking.md",
    "content": "\n# [Rancher Desktop Networking](../../../src/go/networking/)\n\nRancher Desktop Networking primarily acts as a layer 2 switch between the host (currently Windows only) and the VM (WSL) using the `AF_VSOCK` protocol. It facilitates the transmission of Ethernet frames from the VM to the host. Additionally, it provides `DNS`, `DHCP`, and dynamic port forwarding functionalities. The Rancher Desktop Networking comprises several key services: `host-switch`, `vm-switch`, `network-setup`, and `wsl-proxy`. It utilizes [gvisor's](https://github.com/google/gvisor) network stack and draws inspiration from the [gvisor-tap-vsock](https://github.com/google/gvisor) project.\n\nThe diagram below demonstrates the overall architecture of Rancher Desktop Networking:\n\n```mermaid\nflowchart  LR\nsubgraph  hostSwitch[\"host-switch.exe\"]\nvsockHost{\"main loop\"}\neth((\"reconstruct ETH frames\"))\nportForwarding[\"Port Forwarding API\"]\nend\nsubgraph  Host[\"HOST\"]\ndns[\"DNS\"]\nsyscall((\"OS syscall\"))\nhostSwitch\nend\nsubgraph  netNs[\"Isolated Network Namespace\"]\nvsockVM{\"VM Switch\"}\ntapDevice(\"eth0\")\nveth-rd-ns(\"veth-rd-ns\")\ncontainers[\"containers\"]\nservices[\"services\"]\nend\nsubgraph  WSL[\"WSL\"]\nnetNs\nother-distro((\"Other Distros\"))\nveth-rd-wsl(\"veth-rd-wsl\")\nwsl-proxy{\"wsl-proxy\"}\nend\nvsockHost  <---->  eth  &  dns\neth  <---->  syscall\nvsockHost  ---->  portForwarding\ntapDevice  -- ethernet frames -->  vsockVM\nveth-rd-ns  -- ethernet frames -->  vsockVM\nveth-rd-wsl  <---->  veth-rd-ns\nother-distro  <---->  wsl-proxy\nwsl-proxy  <---->  veth-rd-wsl\ncontainers  <---->  tapDevice\nservices  <---->  tapDevice\nvsockVM <-- AF_VSOCK ---> vsockHost\n```\n\n## host-switch:\n\nThe host-switch runs on the Windows host and acts as a receiver for all traffic originating from the network namespace within the WSL VM. It performs a handshake to identify the correct VM to communicate with over `AF_VSOCK`. This process retrieves the GUID for the appropriate Hyper-V VM (most likely WSL). It then performs a handshake with the network-setup process running in the WSL distribution to ensure the `AF_VSOCK` connection is established with the correct VM. Once the ready signal is received from the vm-switch, an `AF_VSOCK` connection is established to listen for incoming traffic from that VM. Additionally, the host-switch provides a DNS resolver that runs in the user space network and an API for dynamic port forwarding. The port forwarding API offers the following endpoints:\n\n- `/services/forwarder/all`: Lists all the currently forwarded ports.\n- `/services/forwarder/expose`: Exposes a port.\n- `/services/forwarder/unexpose`: Unexposes a port.\n\n## Supported Flags:\n\n- **debug**: Enables debug logging.\n- **subnet**: This flag defines a subnet range with a CIDR suffix for a virtual network. If it is not defined, it uses `192.168.127.0/24` as the default range. It is important to note that this value needs to match the [subnet](https://github.com/rancher-sandbox/rancher-desktop/blob/6abacdc804d6414f17439a97f22e0c9c87f6249d/cmd/vm/switch_linux.go#L59) flag in the vm-switch.\n- **port-forward**: This is a list of static ports that need to be pre-forwarded to the WSL VM. These ports are not dynamically retrieved from any of the APIs that the Rancher Desktop guest agent interacts with.\n\n## network-setup:\n\nThe reason for its creation was that the `AF_VSOCK` connection could not be established between the host and a process residing inside the network namespace within the VM, as such capability is not currently supported by `AF_VSOCK`. As a result, the network setup was created. Its main responsibility is to respond to the handshake request from the `host-switch.exe`. Once the handshake process is successful with the `host-switch`, the `network-setup` process creates a new network namespace and attempts to start its subprocess, `vm-switch`, in the newly created network namespace. It also hands over the `AF_VSOCK` connection to the `vm-switch` as a file descriptor in the new namespace.\n\nAdditionally, it calls unshare with provided arguments through [---unshare-args](https://github.com/rancher-sandbox/rancher-desktop/blob/6abacdc804d6414f17439a97f22e0c9c87f6249d/cmd/network/setup_linux.go#L272). The process also establishes a Virtual Ethernet pair consisting of two endpoints: `veth-rd-ns` and `veth-rd-wsl`. `veth-rd-wsl` resides within the default namespace and is configured to listen on the IP address `192.168.143.2`. Conversely, `veth-rd-ns` is located within a network namespace and is assigned the IP address `192.168.143.1`. The virtual Ethernet pair allows accessibility from the default network into the network namespace, which is particularly useful when WSL integration is enabled.\n\n## Supported Flags:\n\n- **debug**: enable the debug logging\n\n- **tap-interface**: The name of the tap interface that is created by the vm-switch upon startup, e.g., `eth0`, `eth1`. This value is passed to the `vm-switch` process when the `network-setup` attempts to start it. If no value is provided, the default name of `eth0` is used.\n\n- **subnet**: A subnet range with a CIDR suffix that is associated with the tap interface in the network namespace. If it is not defined, it uses `192.168.127.0/24` as the default range. It is important to note that this value needs to match the [subnet](https://github.com/rancher-sandbox/rancher-desktop/blob/6abacdc804d6414f17439a97f22e0c9c87f6249d/cmd/host/switch_windows.go#L54) flag in the `host-switch`.\n\n- **tap-mac-address**: MAC address associated with the tap interface created by the vm-switch in the network namespace. If no address is provided, the default address of `5a:94:ef:e4:0c:ee`is used.\n\n- **vm-switch-path**: The path to the `vm-switch` binary that will run in a new namespace. This value is used with `nsenter` to switch the namespace and start the `vm-switch` in the network namespace.\n\n- **vm-switch-logfile**: The path to the logfile for the vm-switch process.\n\n- **unshare-arg**: The command argument to pass to the unshare program in addition to the following [arguments](https://github.com/rancher-sandbox/rancher-desktop/blob/6abacdc804d6414f17439a97f22e0c9c87f6249d/cmd/network/setup_linux.go#L272).\n\n- **logfile**: Path to the logfile for the `network-setup` process.\n\n## vm-switch:\n\nOnce the network-setup starts the `vm-switch` process in the new namespace, the `vm-switch` creates a tap device (`eth0`) and a loopback device (`lo`). When the `eth0` tap device is successfully created, it uses the `DHCP` client to acquire an IP address within the defined range from the `DHCP` server. Once the `eth0` tap device is up and running, the kernel forwards all raw Ethernet frames originating from the network namespace to the tap device. In addition to the traffic from the network namespace, the kernel also forwards all the traffic that arrives at `veth-rd-ns` from its pair, `veth-rd-wsl`, in the default namespace.\n\nThe tap device forwards the Ethernet frames over [vsock](https://wiki.qemu.org/Features/VirtioVsock) to the host. The process on the host (`host-switch.exe`) decapsulates the frames. Since host-switch maintains both internal (`vm-switch` to `host-switch.exe`) and external (`host-switch.exe` to the internet) connections, it connects to the external endpoints via syscalls.\n\n## Supported Flags:\n\n- **debug**: Enable the debug logging\n\n- **tap-interface**: Tap interface name to create, eg. eth0, eth1\n\n- **tap-mac-address** : MAC address that is associated with the tap interface\n\n- **subnet**: The subnet range with CIDR suffix associated with the tap interface. Although this value is passed from network-setup, it must match the subnet flag in `host-switch` and `network-setup`.\n\n- **logfile**: Path to `vm-switch` process logfile\n\n## wsl-proxy:\n\nIts primary function comes into play when WSL integration is activated alongside the network tunnel. Running within the default network namespace, it establishes a Unix socket listener (`/run/wsl-proxy.sock`) for the guest agent process to connect to from inside the network namespace. The guest agent forwards port mappings from various APIs (docker, containerd, and K8s) over the Unix socket to the `wsl-proxy`. Upon receiving the port mappings, the wsl-proxy sets up listeners bound to localhost for those ports. When traffic arrives at these listeners, it forwards the traffic to the bridge interface connecting the default namespace to the namespaced network, facilitating bidirectional traffic flow.\n\n## Supported Flags:\n\n- **debug**: Enable the debug logging\n\n- **logfile**: Path to the logfile for `wsl-proxy` process\n\n- **socketFile**: This is the path to the `.sock` file for the UNIX socket connection established between the Rancher Desktop guest agent and the `wsl-proxy`. If not provided, the default value of `/run/wsl-proxy.sock` is used.\n\n- **upstreamAddress**: This is the IP address associated with the upstream server to use. It corresponds to the address of the veth pair connecting the default namespace to the network namespace, specifically `veth-rd-ns`. The default value is `192.168.143.1`.\n\n\n## Process Timelines:\n\nBelow is a flow chart that demonstrates the process start up orders.\n\n```mermaid\nsequenceDiagram\n    participant wsl-init (pid n)\n    participant network-setup\n    participant vm-switch\n    participant wsl-init (pid 1)\n    participant host-switch.exe\n    Note over wsl-init (pid n),wsl-init (pid 1): WSL distro (Network Namespace)\n    Note over host-switch.exe: windows host\n    wsl-init (pid n)->>network-setup: spawn process\n    host-switch.exe->>network-setup: handshake request\n    network-setup->>host-switch.exe: handshake response (READY signal)\n    host-switch.exe->>network-setup: vsock listener ready\n    network-setup->>network-setup: open vsock\n    network-setup->>network-setup: create namespace\n    network-setup->>network-setup: create veth pair (veth-rd)\n    network-setup->>vm-switch: spawn\n    Note over network-setup,vm-switch: spawn in network namespace\n    Note over network-setup,vm-switch: pass in vsock connection as fd\n    network-setup->>wsl-init (pid 1): spawn\n    Note over network-setup,wsl-init (pid 1): spawns in netns, new mnt/pid ns\n    vm-switch->>vm-switch: create lo/eth0\n    vm-switch->>vm-switch: DHCP eth0\n    vm-switch->>vm-switch: listen for ethernet frames\n    vm-switch->>host-switch.exe: forward ethernet\n    wsl-init (pid 1)-->>wsl-init (pid 1): Spawn /sbin/init\n```\n"
  },
  {
    "path": "e2e/assets/k8s-deploy-sample/nginx-sample-app.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: nginx-app\nspec:\n  selector:\n    matchLabels:\n      app: nginx\n      version: v1\n  replicas: 1\n  template:\n    metadata:\n      labels:\n        app: nginx\n        version: v1\n    spec:\n      containers:\n      - name: nginx\n        image: nginx:latest\n        ports:\n        - containerPort: 80\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: nginx-app\n  labels:\n    app: nginx\nspec:\n  type: NodePort\n  ports:\n  - port: 80\n    name: http\n  selector:\n    app: nginx\n"
  },
  {
    "path": "e2e/backend.e2e.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { test, expect } from '@playwright/test';\nimport _ from 'lodash';\nimport semver from 'semver';\n\nimport { NavPage } from './pages/nav-page';\nimport { getAlternateSetting, startSlowerDesktop, teardown } from './utils/TestUtils';\n\nimport { Settings, ContainerEngine, VMType, MountType } from '@pkg/config/settings';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial, RecursiveKeys } from '@pkg/utils/typeUtils';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\ntest.describe.serial('KubernetesBackend', () => {\n  let electronApp: ElectronApplication;\n  let page: Page;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [electronApp, page] = await startSlowerDesktop(testInfo, {\n      kubernetes: {\n        enabled: false,\n      },\n      virtualMachine: {\n        mount: {\n          type: MountType.REVERSE_SSHFS,\n        },\n      },\n    });\n  });\n\n  test.afterAll(({ colorScheme }, testInfo) => teardown(electronApp, testInfo));\n\n  test('should start loading the background services and hide progress bar', async() => {\n    const navPage = new NavPage(page);\n\n    await navPage.progressBecomesReady();\n    await expect(navPage.progressBar).toBeHidden();\n  });\n\n  test.describe('requiresRestartReasons', () => {\n    let serverState: { user: string, password: string, port: string, pid: string };\n\n    test.afterEach(async() => {\n      // Wait for the backend to stop (it's okay to fail to start here though)\n      const navPage = new NavPage(page);\n\n      while (await navPage.progressBar.count() > 0) {\n        await navPage.progressBar.waitFor({ state: 'detached', timeout: 120_000 });\n      }\n    });\n\n    test('should emit connection information', async() => {\n      const dataPath = path.join(paths.appHome, 'rd-engine.json');\n      const dataRaw = await fs.promises.readFile(dataPath, 'utf-8');\n\n      serverState = JSON.parse(dataRaw);\n      expect(serverState).toEqual(expect.objectContaining({\n        user:     expect.any(String),\n        password: expect.any(String),\n        port:     expect.any(Number),\n        pid:      expect.any(Number),\n      }));\n    });\n\n    async function get(requestPath: string) {\n      const auth = Buffer.from(`${ serverState.user }:${ serverState.password }`).toString('base64');\n      const result = await fetch(`http://127.0.0.1:${ serverState.port }/${ requestPath.replace(/^\\//, '') }`, { headers: { Authorization: `basic ${ auth }` } });\n\n      expect(result).toEqual(expect.objectContaining({ ok: true }));\n\n      return await result.json();\n    }\n\n    async function put(requestPath: string, body: any) {\n      const auth = Buffer.from(`${ serverState.user }:${ serverState.password }`).toString('base64');\n      const result = await fetch(`http://127.0.0.1:${ serverState.port }/${ requestPath.replace(/^\\//, '') }`, {\n        body:    JSON.stringify(body),\n        headers: { Authorization: `basic ${ auth }` },\n        method:  'PUT',\n      });\n      const text = await result.text();\n\n      try {\n        return JSON.parse(text);\n      } catch (cause) {\n        throw new Error(`Response text is not JSON: \\n${ text }`, { cause });\n      }\n    }\n\n    test('should detect changes', async() => {\n      const currentSettings = (await get('/v1/settings')) as Settings;\n\n      if (!currentSettings.kubernetes.version) {\n        // The Kubernetes version could be empty if it's previously disabled.\n        // Set something.\n        const updatedSettings: RecursivePartial<Settings> = {\n          kubernetes: { version: '1.29.4' },\n          version:    10 as Settings['version'],\n        };\n\n        await expect(put('/v1/settings', updatedSettings)).resolves.toBeDefined();\n      }\n\n      const newSettings: RecursivePartial<Settings> = {\n        containerEngine: { name: getAlternateSetting(currentSettings, 'containerEngine.name', ContainerEngine.CONTAINERD, ContainerEngine.MOBY) },\n        kubernetes:      {\n          version: getAlternateSetting(currentSettings, 'kubernetes.version', '1.29.6', '1.29.5'),\n          port:    getAlternateSetting(currentSettings, 'kubernetes.port', 6443, 6444),\n          enabled: getAlternateSetting(currentSettings, 'kubernetes.enabled', true, false),\n          options: {\n            traefik: getAlternateSetting(currentSettings, 'kubernetes.options.traefik', true, false),\n            flannel: getAlternateSetting(currentSettings, 'kubernetes.options.flannel', true, false),\n          },\n        },\n      };\n      /** Platform-specific changes to `newSettings`. */\n      const platformSettings: Partial<Record<NodeJS.Platform, RecursivePartial<Settings>>> = {\n        win32:  { kubernetes: { ingress: { localhostOnly: getAlternateSetting(currentSettings, 'kubernetes.ingress.localhostOnly', true, false) } } },\n        darwin: { virtualMachine: { type: getAlternateSetting(currentSettings, 'virtualMachine.type', VMType.VZ, VMType.QEMU) } },\n      };\n\n      _.merge(newSettings, platformSettings[process.platform] ?? {});\n      if (['darwin', 'linux'].includes(process.platform)) {\n        // Lima-specific changes to `newSettings`.\n        _.merge(newSettings, {\n          virtualMachine: {\n            numberCPUs: getAlternateSetting(currentSettings, 'virtualMachine.numberCPUs', 1, 2),\n            memoryInGB: getAlternateSetting(currentSettings, 'virtualMachine.memoryInGB', 3, 4),\n          },\n          application: { adminAccess: getAlternateSetting(currentSettings, 'application.adminAccess', false, true) },\n        });\n      }\n\n      /**\n       * Helper type; an (incomplete) mapping where the key is the preference\n       * name, and the value is a boolean value indicating whether reset is needed.\n       */\n      type ExpectedDefinition = Partial<Record<RecursiveKeys<Settings>, boolean>>;\n\n      const expectedDefinition: ExpectedDefinition = {\n        'kubernetes.version':         semver.lt(newSettings.kubernetes?.version ?? '0.0.0', currentSettings.kubernetes.version),\n        'kubernetes.port':            false,\n        'containerEngine.name':       false,\n        'kubernetes.enabled':         false,\n        'kubernetes.options.traefik': false,\n        'kubernetes.options.flannel': false,\n      };\n\n      /** Platform-specific additions to `expectedDefinition`. */\n      const platformExpectedDefinitions: Partial<Record<NodeJS.Platform, ExpectedDefinition>> = {\n        win32:  { 'kubernetes.ingress.localhostOnly': false },\n        darwin: { 'virtualMachine.type': false },\n      };\n\n      _.merge(expectedDefinition, platformExpectedDefinitions[process.platform] ?? {});\n\n      if (['darwin', 'linux'].includes(process.platform)) {\n        // Lima additions to expectedDefinition\n        expectedDefinition['application.adminAccess'] = false;\n        expectedDefinition['experimental.virtualMachine.diskSize'] = false;\n        expectedDefinition['virtualMachine.numberCPUs'] = false;\n        expectedDefinition['virtualMachine.memoryInGB'] = false;\n      }\n\n      const expected: Record<string, { current: any, desired: any, severity: 'reset' | 'restart' }> = {};\n\n      for (const [key, reset] of Object.entries(expectedDefinition)) {\n        const entry = {\n          current:  _.get(currentSettings, key),\n          desired:  _.get(newSettings, key),\n          severity: reset ? 'reset' : 'restart' as 'reset' | 'restart',\n        };\n\n        expected[key] = entry;\n      }\n\n      await expect(put('/v1/propose_settings', newSettings)).resolves.toEqual(expected);\n    });\n\n    test('should handle WSL integrations', async() => {\n      test.skip(os.platform() !== 'win32', 'WSL integration only supported on Windows');\n      const random = `${ Date.now() }${ Math.random() }`;\n      const newSettings: RecursivePartial<Settings> = {\n        WSL: {\n          integrations: {\n            [`true-${ random }`]:  true,\n            [`false-${ random }`]: false,\n          },\n        },\n      };\n\n      await expect(put('/v1/propose_settings', newSettings)).resolves.toMatchObject({\n        'WSL.integrations': {\n          desired:  newSettings.WSL?.integrations,\n          severity: 'restart',\n        },\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/config/playwright-config.ts",
    "content": "import * as path from 'path';\n\nimport { defineConfig } from '@playwright/test';\n\nconst ci = !!process.env.CI;\nconst outputDir = path.join(import.meta.dirname, '..', 'e2e', 'test-results');\nconst testDir = path.join(import.meta.dirname, '..', '..', 'e2e');\n// The provisioned github runners are much slower overall than cirrus's, so allow 2 hours for a full e2e run\nconst timeScale = ci ? 4 : 1;\n\nconst config = defineConfig({\n  testDir,\n  outputDir,\n  timeout:       10 * 60 * 1000 * timeScale,\n  globalTimeout: 30 * 60 * 1000 * timeScale,\n  workers:       1,\n  reporter:      'list',\n  retries:       ci ? 2 : 0,\n  use:           {\n    trace: {\n      mode:        'on-all-retries',\n      screenshots: true,\n    },\n  },\n});\n\nexport default config;\n"
  },
  {
    "path": "e2e/containers.e2e.spec.ts",
    "content": "import { expect, test, ElectronApplication, Page } from '@playwright/test';\n\nimport { ContainerLogsPage } from './pages/container-logs-page';\nimport { ContainerShellPage } from './pages/container-shell-page';\nimport { ContainersPage } from './pages/containers-page';\nimport { NavPage } from './pages/nav-page';\nimport { startSlowerDesktop, teardown, tool } from './utils/TestUtils';\n\nimport { ContainerEngine } from '@pkg/config/settings';\n\nlet page: Page;\n\ntest.describe.serial('Containers Tests', () => {\n  let electronApp: ElectronApplication;\n  let testContainerId: string;\n  let testContainerName: string;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [electronApp, page] = await startSlowerDesktop(testInfo, {\n      kubernetes:      { enabled: false },\n      containerEngine: { name: ContainerEngine.MOBY, allowedImages: { enabled: false } },\n    });\n\n    const navPage = new NavPage(page);\n    await navPage.progressBecomesReady();\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    if (testContainerId) {\n      try {\n        await tool('docker', 'rm', '-f', testContainerId);\n      } catch (error) {}\n    }\n    await teardown(electronApp, testInfo);\n  });\n\n  test('should navigate to containers page', async() => {\n    const navPage = new NavPage(page);\n    const containersPage = await navPage.navigateTo('Containers');\n\n    await expect(navPage.mainTitle).toHaveText('Containers');\n    await containersPage.waitForTableToLoad();\n  });\n\n  test('should create and display test container', async() => {\n    testContainerName = `test-logs-container-${ Date.now() }`;\n\n    const output = await tool(\n      'docker',\n      'run',\n      '--detach',\n      '--name',\n      testContainerName,\n      'alpine',\n      'sh',\n      '-c',\n      'echo \"Starting\"; for i in $(seq 1 10); do echo \"L$i: msg$i\"; done; echo \"Finished\"',\n    );\n    testContainerId = output.trim();\n\n    expect(testContainerId).toMatch(/^[a-f0-9]{64}$/);\n\n    await page.reload();\n\n    const navPage = new NavPage(page);\n    const containersPage = await navPage.navigateTo('Containers');\n    await containersPage.waitForTableToLoad();\n\n    await containersPage.waitForContainerToAppear(testContainerId);\n    await containersPage.viewContainerInfo(testContainerId);\n\n    await page.waitForURL(`**/containers/info/${ testContainerId }**`, {\n      timeout: 10_000,\n    });\n  });\n\n  test('should display container logs page', async() => {\n    const containerLogsPage = new ContainerLogsPage(page);\n\n    await expect(containerLogsPage.containerInfo).toBeVisible();\n\n    await expect(containerLogsPage.terminal).toBeVisible();\n    await expect(containerLogsPage.loadingIndicator).not.toBeVisible();\n  });\n\n  test('should show container information', async() => {\n    const containerLogsPage = new ContainerLogsPage(page);\n\n    await expect(containerLogsPage.containerInfo).toBeVisible();\n\n    await expect(containerLogsPage.containerName).toContainText(\n      testContainerName,\n    );\n    await expect(containerLogsPage.containerState).not.toBeEmpty();\n  });\n\n  test('should display logs content', async() => {\n    const containerLogsPage = new ContainerLogsPage(page);\n\n    await containerLogsPage.waitForLogsToLoad();\n\n    await expect(containerLogsPage.terminal).toContainText('L1: msg1');\n  });\n\n  test('should support log search', async() => {\n    const containerLogsPage = new ContainerLogsPage(page);\n\n    await expect(containerLogsPage.searchInput).toBeVisible();\n\n    const searchTerm = 'msg';\n    await containerLogsPage.searchLogs(searchTerm);\n\n    const searchHighlight = page.locator('span.xterm-decoration-top');\n    await expect(searchHighlight).toBeVisible();\n\n    const highlightedRow = containerLogsPage.terminal.locator(\n      '.xterm-rows div',\n      {\n        has: page.locator('.xterm-decoration-top'),\n      },\n    );\n\n    await expect(highlightedRow).toContainText('L1: msg1');\n\n    await containerLogsPage.searchNextButton.click();\n\n    await expect(searchHighlight).toBeVisible();\n    await expect(highlightedRow).toContainText('L2: msg2');\n\n    await containerLogsPage.searchPrevButton.click();\n\n    await expect(searchHighlight).toBeVisible();\n    await expect(highlightedRow).toContainText('L1: msg1');\n\n    await containerLogsPage.searchClearButton.click();\n    await expect(containerLogsPage.searchInput).toBeEmpty();\n\n    await containerLogsPage.terminal.click();\n\n    await expect(searchHighlight).not.toBeVisible();\n  });\n\n  test('should handle terminal scrolling', async() => {\n    const scrollTestContainerName = `test-scroll-container-${ Date.now() }`;\n    let scrollTestContainerId: string;\n\n    try {\n      const output = await tool(\n        'docker',\n        'run',\n        '--detach',\n        '--name',\n        scrollTestContainerName,\n        'alpine',\n        'sh',\n        '-c',\n        'for i in $(seq 1 100); do echo \"Line $i:\"; done; sleep 1',\n      );\n      scrollTestContainerId = output.trim();\n\n      const navPage = new NavPage(page);\n      const containersPage = await navPage.navigateTo('Containers');\n\n      await page.reload();\n      await containersPage.waitForTableToLoad();\n\n      await containersPage.waitForContainerToAppear(scrollTestContainerId);\n      await containersPage.viewContainerInfo(scrollTestContainerId);\n\n      await page.waitForURL(`**/containers/info/${ scrollTestContainerId }**`, {\n        timeout: 10_000,\n      });\n\n      const containerLogsPage = new ContainerLogsPage(page);\n      await containerLogsPage.waitForLogsToLoad();\n\n      const terminalRows = containerLogsPage.terminal.locator('.xterm-rows');\n      const lastLine = terminalRows.getByText('Line 100:', { exact: false });\n      const firstLine = terminalRows.getByText('Line 1:', { exact: false });\n\n      await expect(lastLine).toBeVisible();\n      await expect(firstLine).not.toBeVisible();\n\n      await containerLogsPage.scrollToTop();\n\n      await expect(firstLine).toBeVisible();\n      await expect(lastLine).not.toBeVisible();\n\n      await containerLogsPage.scrollToBottom();\n\n      await expect(lastLine).toBeVisible();\n      await expect(firstLine).not.toBeVisible();\n    } finally {\n      if (scrollTestContainerId) {\n        try {\n          await tool('docker', 'rm', '-f', scrollTestContainerId);\n        } catch (cleanupError) {}\n      }\n    }\n  });\n\n  test('should output logs if container not exited', async() => {\n    const longRunningContainerName = `test-not-exited-logs-${ Date.now() }`;\n    let longRunningContainerId: string;\n\n    try {\n      const output = await tool(\n        'docker',\n        'run',\n        '--detach',\n        '--name',\n        longRunningContainerName,\n        'alpine',\n        'sh',\n        '-c',\n        'while true; do echo \"Log $(date +%s)\"; sleep 2; done',\n      );\n      longRunningContainerId = output.trim();\n\n      const navPage = new NavPage(page);\n      const containersPage = await navPage.navigateTo('Containers');\n\n      await page.reload();\n      await containersPage.waitForTableToLoad();\n\n      await containersPage.waitForContainerToAppear(longRunningContainerId);\n      await containersPage.viewContainerInfo(longRunningContainerId);\n\n      await page.waitForURL(`**/containers/info/${ longRunningContainerId }**`, {\n        timeout: 10000,\n      });\n\n      const containerLogsPage = new ContainerLogsPage(page);\n      await containerLogsPage.waitForLogsToLoad();\n\n      const locator = containerLogsPage.terminal.locator('.xterm-screen');\n      await expect(locator.getByText(/Log \\d+/).nth(1)).toBeVisible();\n\n      await expect(containerLogsPage.terminal).toContainText('Log ');\n\n      await tool('docker', 'rm', '-f', longRunningContainerId);\n    } finally {\n      if (longRunningContainerId) {\n        try {\n          await tool('docker', 'rm', '-f', longRunningContainerId);\n        } catch (cleanupError) {}\n      }\n    }\n  });\n\n  test('should auto-refresh containers list', async() => {\n    const containersPage = new ContainersPage(page);\n    const autoRefreshContainerName = `auto-refresh-test-${ Date.now() }`;\n    let autoRefreshContainerId = '';\n\n    try {\n      const navPage = new NavPage(page);\n      await navPage.navigateTo('Containers');\n      await containersPage.waitForTableToLoad();\n\n      // Remove all existing containers to ensure clean state\n      try {\n        const existingContainers = await tool('docker', 'ps', '--all', '--quiet');\n        const containerIds = existingContainers.trim().split(/\\s+/);\n\n        if (containerIds.length > 0) {\n          await tool('docker', 'rm', '--force', ...containerIds);\n        }\n      } catch {}\n\n      await expect(containersPage.containers).toHaveCount(0);\n\n      const output = await tool(\n        'docker',\n        'run',\n        '--detach',\n        '--name',\n        autoRefreshContainerName,\n        'alpine',\n        'sleep',\n        'infinity',\n      );\n      autoRefreshContainerId = output.trim();\n\n      await expect(\n        containersPage.getContainerRow(autoRefreshContainerId),\n      ).toBeVisible();\n\n      await tool('docker', 'rm', '--force', autoRefreshContainerId);\n\n      await expect(\n        containersPage.getContainerRow(autoRefreshContainerId),\n      ).toBeHidden();\n    } finally {\n      if (autoRefreshContainerId) {\n        try {\n          await tool('docker', 'rm', '-f', autoRefreshContainerId);\n        } catch {}\n      }\n    }\n  });\n});\n\ntest.describe.serial('Container Shell Tab', () => {\n  let electronApp: ElectronApplication;\n  let shellContainerId: string;\n  let unsupportedContainerId: string;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [electronApp, page] = await startSlowerDesktop(testInfo, {\n      kubernetes:      { enabled: false },\n      containerEngine: { name: ContainerEngine.MOBY, allowedImages: { enabled: false } },\n    });\n\n    const navPage = new NavPage(page);\n    await navPage.progressBecomesReady();\n\n    // Start a long-running container for the shell tests.\n    // Ubuntu is used because the base Alpine image does not include `script`\n    // (util-linux), which is required for the interactive shell session.\n    const output = await tool('docker', 'run', '--detach', 'ubuntu', 'sleep', 'infinity');\n    shellContainerId = output.trim();\n\n    // Alpine container for the \"unsupported\" test — Alpine's base image has no\n    // `script` command, so the shell tab should show the unsupported banner.\n    const alpineOutput = await tool('docker', 'run', '--detach', 'alpine', 'sleep', 'infinity');\n    unsupportedContainerId = alpineOutput.trim();\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    if (shellContainerId) {\n      try {\n        await tool('docker', 'rm', '-f', shellContainerId);\n      } catch {}\n    }\n    if (unsupportedContainerId) {\n      try {\n        await tool('docker', 'rm', '-f', unsupportedContainerId);\n      } catch {}\n    }\n    await teardown(electronApp, testInfo);\n  });\n\n  async function navigateToShellTab() {\n    const navPage = new NavPage(page);\n    await navPage.navigateTo('Containers');\n    const containersPage = new ContainersPage(page);\n    await containersPage.waitForTableToLoad();\n    await containersPage.waitForContainerToAppear(shellContainerId);\n    await containersPage.clickContainerAction(shellContainerId, 'info');\n    await page.waitForURL(`**/containers/info/${ shellContainerId }**`, { timeout: 10_000 });\n    const shellPage = new ContainerShellPage(page);\n    await shellPage.clickTab();\n    await shellPage.waitForTerminal();\n    await shellPage.waitForShellReady();\n\n    return shellPage;\n  }\n\n  test('should show the Shell tab on a running container', async() => {\n    const shellPage = await navigateToShellTab();\n    await expect(shellPage.tab).toBeVisible();\n    await expect(shellPage.terminal).toBeVisible();\n    await expect(shellPage.notRunningBanner).not.toBeVisible();\n  });\n\n  test('should execute a command and display its output', async() => {\n    const shellPage = new ContainerShellPage(page);\n    // Unique marker avoids false positives from any earlier terminal history.\n    const marker = `RDTEST_${ Date.now() }`;\n    await shellPage.runCommand(`echo ${ marker }`);\n    await shellPage.waitForOutput(marker);\n  });\n\n  test('should preserve the session when switching between Logs and Shell tabs', async() => {\n    const shellPage = new ContainerShellPage(page);\n    const logsTab = page.getByTestId('tab-logs');\n    // A unique marker is required: we must distinguish \"this exact output is\n    // still in the buffer\" from \"the shell printed something similar\".\n    const marker = `RDTEST_PERSIST_${ Date.now() }`;\n\n    await shellPage.runCommand(`echo ${ marker }`);\n    await shellPage.waitForOutput(marker);\n\n    // Switch to Logs and back.\n    await logsTab.click();\n    await shellPage.clickTab();\n\n    // History must still be visible — session was preserved.\n    await shellPage.waitForOutput(marker);\n  });\n\n  test('should show the unsupported banner for containers without script', async() => {\n    // Navigate to the Alpine container — it has no `script`, so the backend\n    // will send container-exec/unsupported instead of starting a session.\n    const navPage = new NavPage(page);\n    await navPage.navigateTo('Containers');\n    const containersPage = new ContainersPage(page);\n    await containersPage.waitForTableToLoad();\n    await containersPage.waitForContainerToAppear(unsupportedContainerId);\n    await containersPage.clickContainerAction(unsupportedContainerId, 'info');\n    await page.waitForURL(`**/containers/info/${ unsupportedContainerId }**`, { timeout: 10_000 });\n\n    const shellPage = new ContainerShellPage(page);\n    await shellPage.clickTab();\n\n    // The unsupported banner must appear and the terminal must not be rendered.\n    await expect(shellPage.unsupportedBanner).toBeVisible({ timeout: 15_000 });\n    await expect(shellPage.terminal).not.toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/credentials-server.e2e.spec.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This file includes end-to-end testing for the HTTP control interface\n */\n\nimport { spawnSync } from 'child_process';\nimport * as crypto from 'crypto';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport process from 'process';\nimport stream from 'stream';\n\nimport { findHomeDir } from '@kubernetes/client-node';\nimport { expect, test } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { getFullPathForTool, startSlowerDesktop, teardown, tool } from './utils/TestUtils';\n\nimport { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\nlet credStore = '';\nlet dockerConfigPath = '';\nlet originalDockerConfigContents: string | undefined;\nlet plaintextConfigPath = '';\nlet originalPlaintextConfigContents: string | undefined;\n\ninterface entryType {\n  ServerURL: string;\n  Username:  string;\n  Secret:    string;\n}\n\nfunction makeEntry(url: string, username: string, secret: string): entryType {\n  return {\n    ServerURL: url, Username: username, Secret: secret,\n  };\n}\n\n/**\n * This function does multiple-duty:\n * 1. Skip all the tests if there is no working configured credential helper.\n * 2. Assign values to the global variables declared after the above `import` statements.\n *    This includes saving the current contents of the docker config files, to be restored at end.\n */\nfunction haveCredentialServerHelper(): boolean {\n  const homeDir = findHomeDir() ?? '/';\n  const dockerDir = path.join(homeDir, '.docker');\n\n  dockerConfigPath = path.join(dockerDir, 'config.json');\n  plaintextConfigPath = path.join(dockerDir, 'plaintext-credentials.config.json');\n  try {\n    originalPlaintextConfigContents = fs.readFileSync(plaintextConfigPath).toString();\n  } catch { }\n  try {\n    originalDockerConfigContents = fs.readFileSync(dockerConfigPath).toString();\n    const configObject = JSON.parse(originalDockerConfigContents);\n\n    credStore = configObject.credsStore;\n    if (!credStore) {\n      credStore = configObject.credsStore = 'none';\n      fs.writeFileSync(dockerConfigPath, JSON.stringify(configObject, undefined, 2));\n    }\n    if (credStore === 'none') {\n      return true;\n    }\n    const result = spawnSync(getFullPathForTool(`docker-credential-${ credStore }`), ['list'], { stdio: 'pipe' });\n\n    return !result.error;\n  } catch (err: any) {\n    if (err.code === 'ENOENT' && process.env.CI) {\n      try {\n        console.log('Attempting to set up docker-credential-none on CI.');\n        fs.mkdirSync(dockerDir, { recursive: true });\n        fs.writeFileSync(dockerConfigPath, JSON.stringify({ credsStore: 'none' }, undefined, 2));\n\n        return true;\n      } catch (err2: any) {\n        console.log(`Failed to create a .docker/config.json on the fly for CI: stdout: ${ err2.stdout?.toString() }, stderr: ${ err2.stderr?.toString() }`);\n      }\n    }\n\n    return false;\n  }\n}\n\nconst describeWithCreds = haveCredentialServerHelper() ? test.describe : test.describe.skip;\nconst describeCredHelpers = credStore === 'none' ? test.describe.skip : test.describe;\nconst testUnix = os.platform() === 'win32' ? test.skip : test;\n\ndescribeWithCreds('Credentials server', () => {\n  let electronApp: ElectronApplication;\n  let serverState: ServerState;\n  let authString: string;\n  let page: Page;\n  const curlCommand = os.platform() === 'win32' ? 'curl.exe' : 'curl';\n  const initialArgs: string[] = []; // Assigned once we have auth string on first use.\n\n  async function doRequest(path: string, body = '', ignoreStderr = false) {\n    const args = initialArgs.concat([`http://localhost:${ serverState.port }/${ path }`]);\n\n    if (body.length) {\n      args.push('--data', body);\n    }\n    const { stdout, stderr } = await spawnFile(curlCommand, args, { stdio: 'pipe' });\n\n    if (stderr) {\n      if (ignoreStderr) {\n        console.log(`doRequest: spawn ${ curlCommand } ${ args.join(' ') } => ${ stderr }`);\n      } else {\n        expect(stderr).toEqual('');\n      }\n    }\n\n    return stdout;\n  }\n\n  async function doRequestExpectStatus(path: string, body: string, expectedStatus: number) {\n    const args = initialArgs.concat(['-v', `http://localhost:${ serverState.port }/${ path }`]);\n\n    if (body.length) {\n      args.push('--data', body);\n    }\n    const { stderr } = await spawnFile(curlCommand, args, { stdio: 'pipe' });\n\n    expect(stderr).toContain(`HTTP/1.1 ${ expectedStatus }`);\n  }\n\n  async function addEntry(helper: string, entry: entryType): Promise<void> {\n    const pathToHelper = getFullPathForTool(`docker-credential-${ helper }`);\n    const body = stream.Readable.from(JSON.stringify(entry));\n\n    await spawnFile(pathToHelper, ['store'], { stdio: [body, 'pipe', 'pipe'] });\n  }\n\n  async function listEntries(helper: string, matcher: string): Promise<Record<string, string>> {\n    const pathToHelper = getFullPathForTool(`docker-credential-${ helper }`);\n    const { stdout } = await spawnFile(pathToHelper, ['list'], { stdio: ['ignore', 'pipe', 'pipe'] });\n    const entries: Record<string, string> = JSON.parse(stdout);\n\n    for (const k in entries) {\n      if (!k.includes(matcher)) {\n        delete entries[k];\n      }\n    }\n\n    return entries;\n  }\n\n  async function removeEntries(helper: string, matcher: string) {\n    const dcName = `docker-credential-${ helper }`;\n    const stdout = await tool(dcName, 'list');\n    const servers = Object.keys(JSON.parse(stdout));\n    let finalException: any | undefined;\n\n    for (const server of servers) {\n      if (!server.includes(matcher)) {\n        continue;\n      }\n      const body = stream.Readable.from(server);\n\n      try {\n        const pathToHelper = getFullPathForTool(dcName);\n        const { stdout } = await spawnFile(pathToHelper, ['erase'], { stdio: [body, 'pipe', 'pipe'] });\n\n        if (stdout) {\n          const msg = `Problem deleting ${ server } using ${ dcName }: got output stdout: ${ stdout }`;\n\n          console.log(msg);\n          finalException ??= new Error(msg);\n        }\n      } catch (ex) {\n        console.log(`Problem deleting ${ server } using ${ dcName }: `, ex);\n        finalException ??= ex;\n      }\n    }\n    if (finalException) {\n      throw finalException;\n    }\n  }\n\n  function rdctlPath() {\n    return getFullPathForTool('rdctl');\n  }\n\n  async function rdctlCredWithStdin(command: string, input?: string): Promise<{ stdout: string, stderr: string }> {\n    try {\n      const body = stream.Readable.from(input ?? '');\n      const args = ['shell', 'sh', '-c', `CREDFWD_CURL_OPTS=--show-error /usr/local/bin/docker-credential-rancher-desktop ${ command }`];\n\n      return await spawnFile(rdctlPath(), args, { stdio: [body, 'pipe', 'pipe'] });\n    } catch (err: any) {\n      throw {\n        stdout: err?.stdout ?? '',\n        stderr: err?.stderr ?? '',\n        error:  err,\n      };\n    }\n  }\n\n  test.describe.configure({ mode: 'serial' });\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    await tool('rdctl', 'reset', '--factory', '--verbose');\n    [electronApp, page] = await startSlowerDesktop(testInfo, { kubernetes: { enabled: false } });\n  });\n\n  test.afterAll(async() => {\n    if (originalDockerConfigContents !== undefined && !process.env.CI && !process.env.RD_E2E_DO_NOT_RESTORE_CONFIG) {\n      try {\n        await fs.promises.writeFile(dockerConfigPath, originalDockerConfigContents);\n      } catch (e: any) {\n        console.error(`Failed to restore config file ${ dockerConfigPath }: `, e);\n      }\n    }\n    if (originalPlaintextConfigContents !== undefined && !process.env.CI && !process.env.RD_E2E_DO_NOT_RESTORE_CONFIG) {\n      try {\n        await fs.promises.writeFile(plaintextConfigPath, originalPlaintextConfigContents);\n      } catch (e: any) {\n        console.error(`Failed to restore config file ${ plaintextConfigPath }: `, e);\n      }\n    }\n  });\n\n  test.afterAll(({ colorScheme }, testInfo) => teardown(electronApp, testInfo));\n\n  test('should start loading the background services and hide progress bar', async() => {\n    const navPage = new NavPage(page);\n\n    await navPage.progressBecomesReady();\n    await expect(navPage.progressBar).toBeHidden();\n  });\n\n  test('should emit connection information', async() => {\n    const dataPath = path.join(paths.appHome, 'credential-server.json');\n    const dataRaw = await fs.promises.readFile(dataPath, 'utf-8');\n\n    serverState = JSON.parse(dataRaw);\n    expect(serverState).toEqual(expect.objectContaining({\n      user:     expect.any(String),\n      password: expect.any(String),\n      port:     expect.any(Number),\n      pid:      expect.any(Number),\n    }));\n\n    // Check if the process is running.\n    try {\n      expect(process.kill(serverState.pid, 0)).toBeTruthy();\n    } catch (ex: any) {\n      // Exception here is acceptable, if the error is due to EPERM.\n      expect(ex).toHaveProperty('code', 'EPERM');\n    }\n\n    // Now is a good time to initialize the various connection-related values.\n    authString = `${ serverState.user }:${ serverState.password }`;\n    // common arguments for curl\n    initialArgs.push('--silent', '--user', authString, '--request', 'POST');\n  });\n\n  test('should require authentication', async() => {\n    const url = `http://127.0.0.1:${ serverState.port }/list`;\n    const resp = await fetch(url);\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(401);\n  });\n\n  test('should be able to use the API', async() => {\n    const bobsURL = 'https://bobs.fish/tackle';\n    const bobsFirstSecret = 'loblaw';\n    const bobsSecondSecret = 'shoppers with spaces and % and \\' and &s';\n\n    const body = {\n      ServerURL: bobsURL, Username: 'bob', Secret: bobsFirstSecret,\n    };\n    let stdout: string = await doRequest('list');\n\n    if (JSON.parse(stdout)[bobsURL]) {\n      await doRequestExpectStatus('erase', bobsURL, 200);\n    }\n\n    await doRequestExpectStatus('store', JSON.stringify(body), 200);\n\n    stdout = await doRequest('list');\n    expect(JSON.parse(stdout)).toMatchObject({ [bobsURL]: 'bob' });\n\n    stdout = await doRequest('get', bobsURL);\n    expect(JSON.parse(stdout)).toMatchObject(body);\n\n    // Verify we can store and retrieve passwords with wacky characters in them.\n    body.Secret = bobsSecondSecret;\n    await doRequestExpectStatus('store', JSON.stringify(body), 200);\n\n    stdout = await doRequest('get', bobsURL);\n    expect(JSON.parse(stdout)).toMatchObject(body);\n\n    await doRequestExpectStatus('erase', bobsURL, 200);\n\n    // Instead of returning an error message,\n    // `docker-credential-pass` will happily return an object with `ServerURL` set to the provided argument,\n    // and empty strings for Username and Secret.\n    // This is a bit crazy, because `pass show noSuchEntry` gives an error message.\n    // Upstream error: https://github.com/docker/docker-credential-helpers/issues/220\n    if (credStore !== 'pass') {\n      stdout = await doRequest('get', bobsURL);\n      expect(stdout).toContain('credentials not found in native keychain');\n    }\n\n    // Don't bother trying to test erasing a nonexistent credential, because the\n    // behavior is all over the place. Fails with osxkeychain, succeeds with wincred.\n  });\n\n  test('it should complain about an unrecognized command', async() => {\n    const badCommand = 'gazornaanplatt';\n    const stdout = await doRequest(badCommand);\n\n    expect(stdout).toContain(`Unknown credential action '${ badCommand }' for the credential-server, must be one of [erase|get|list|store]`);\n  });\n\n  test('it should complain about non-POST requests', async() => {\n    const args = initialArgs.concat([`http://localhost:${ serverState.port }/list`]);\n    const postIndex = args.indexOf('POST');\n\n    if (postIndex > -1) {\n      args.splice(postIndex - 1, 2);\n    }\n    await expect(spawnFile(curlCommand, args, { stdio: 'pipe' })).resolves.toMatchObject({\n      stdout: expect.stringContaining('Expecting a POST method for the credential-server list request, received GET'),\n      stderr: '',\n    });\n  });\n\n  test('should be able to use the script', async() => {\n    const bobsURL = 'https://bobs.fish/tackle';\n    const bobsFirstSecret = 'loblaw';\n    const bobsSecondSecret = 'shoppers with spaces and % and \\' and &s and even a 😱';\n\n    const body = {\n      ServerURL: bobsURL,\n      Username:  'bob',\n      Secret:    bobsFirstSecret,\n    };\n\n    let { stdout } = await rdctlCredWithStdin('list');\n\n    if (stdout && JSON.parse(stdout)[bobsURL]) {\n      ({ stdout } = await rdctlCredWithStdin('erase', bobsURL));\n      expect(stdout).toEqual('');\n    }\n\n    await expect(rdctlCredWithStdin('store', JSON.stringify(body))).resolves.toMatchObject({ stdout: '' });\n\n    ({ stdout } = await rdctlCredWithStdin('list'));\n    expect(JSON.parse(stdout)).toMatchObject({ [bobsURL]: 'bob' });\n\n    ({ stdout } = await rdctlCredWithStdin('get', bobsURL));\n    expect(JSON.parse(stdout)).toMatchObject(body);\n\n    // Verify we can store and retrieve passwords with wacky characters in them.\n    body.Secret = bobsSecondSecret;\n    await expect(rdctlCredWithStdin('store', JSON.stringify(body))).resolves.toMatchObject({ stdout: '' });\n\n    ({ stdout } = await rdctlCredWithStdin('get', bobsURL));\n    expect(JSON.parse(stdout)).toMatchObject(body);\n\n    if (credStore !== 'pass') {\n      // See above comment discussing the consequences of `echo ARG | docker-credential-pass get` never failing.\n      await expect(rdctlCredWithStdin('erase', bobsURL)).resolves.toMatchObject({ stdout: '' });\n      await expect(rdctlCredWithStdin('get', bobsURL)).rejects.toMatchObject({\n        stdout: expect.stringContaining('credentials not found in native keychain'),\n        stderr: expect.stringContaining('Error: exit status 22'),\n      });\n    }\n  });\n\n  // This test currently fails on Windows due to https://github.com/docker/docker-credential-helpers/issues/190\n  testUnix('complains when the limit is exceeded (on the server - do an inexact check)', async() => {\n    const args = [\n      'shell',\n      'sh',\n      '-c',\n      `export CREDFWD_CURL_OPTS=\"--show-error\"; \\\n       SECRET=$(tr -dc 'A-Za-z0-9,._=' < /dev/urandom |  head -c5242880); \\\n       echo '{\"ServerURL\":\"https://example.com/v1\",\"Username\":\"alice\",\"Secret\":\"'$SECRET'\"}' |\n         /usr/local/bin/docker-credential-rancher-desktop store`,\n    ];\n\n    try {\n      // This should throw, but we care about more than one error field, so use a try-catch\n      const { stdout } = await spawnFile(rdctlPath(), args, { stdio: ['ignore', 'pipe', 'pipe'] });\n\n      expect(stdout).toEqual('should have failed');\n    } catch (err: any) {\n      expect(err).toMatchObject({\n        stdout: expect.stringContaining('request body is too long, request body size exceeds 4194304'),\n        stderr: expect.stringContaining('The requested URL returned error: 413\\nError: exit status 22'),\n      });\n    }\n  });\n\n  // This test currently fails on Windows due to https://github.com/docker/docker-credential-helpers/issues/190\n  testUnix('handles long, legal payloads that can be verified', async() => {\n    const calsURL = 'https://cals.nightcrawlers.com/guaranteed';\n    const keyLength = 5000;\n    const secret = crypto.randomBytes(keyLength / 2).toString('hex');\n    const args = [\n      'shell',\n      'sh',\n      '-c',\n      `echo '{\"ServerURL\":\"${ calsURL }\",\"Username\":\"cal\",\"Secret\":\"${ secret }\"}' |\n         /usr/local/bin/docker-credential-rancher-desktop store`,\n    ];\n\n    await expect(spawnFile(rdctlPath(), args, { stdio: ['ignore', 'pipe', 'pipe'] })).resolves.toBeDefined();\n    const { stdout } = await rdctlCredWithStdin('get', calsURL);\n\n    expect(JSON.parse(stdout).Secret).toEqual(secret);\n  });\n\n  test.describe('should be able to detect errors', () => {\n    const bobsURL = 'https://bobs.fish/bait';\n\n    test('it should complain when no ServerURL is given', async() => {\n      const body: Record<string, string> = {};\n\n      await expect(rdctlCredWithStdin('store', JSON.stringify(body))).rejects.toMatchObject({\n        stdout: expect.stringContaining('no credentials server URL'),\n        stderr: expect.stringContaining('Error: exit status 22'),\n      });\n    });\n\n    test('it should complain when no username is given', async() => {\n      const body: Record<string, string> = { ServerURL: bobsURL };\n\n      await expect(rdctlCredWithStdin('store', JSON.stringify(body))).rejects.toMatchObject({\n        stdout: expect.stringContaining('no credentials username'),\n        stderr: expect.stringContaining('Error: exit status 22'),\n      });\n    });\n\n    test('it should not complain about extra fields', async() => {\n      const body: Record<string, string> = {\n        ServerURL: bobsURL, Username: 'bob', Soup: 'gazpacho',\n      };\n\n      await expect(rdctlCredWithStdin('store', JSON.stringify(body))).resolves.toMatchObject({ stdout: '' });\n\n      const { stdout, stderr } = await rdctlCredWithStdin('get', bobsURL);\n\n      expect({ stdout: JSON.parse(stdout), stderr }).toMatchObject({\n        // Playwright type definitions for `expect.not` is missing; see\n        // playwright issue #15087.\n        stdout: (expect as any).not.objectContaining({ Soup: 'gazpacho' }),\n      });\n    });\n  });\n\n  // Skip these tests if config.credsStore and the credHelpers are both using 'none'\n  describeCredHelpers('handles credHelpers', () => {\n    const peopleEntries: Record<string, entryType> = {\n      bob:     makeEntry('https://bobs.fish/clams01', 'Bob', 'clams01'),\n      carol:   makeEntry('https://bobs.fish/clams02', 'Carol', 'clams02'),\n      ted:     makeEntry('https://bobs.fish/clams03', 'Ted', 'clams03'),\n      alice:   makeEntry('https://bobs.fish/clams04', 'Alice', 'clams04'),\n      fakeTed: makeEntry('https://bobs.fish/clams03', 'Fake-Ted', 'Fake-clams03'),\n    };\n    const dockerConfig = {\n      auths:          {},\n      credsStore:     '',\n      currentContext: 'rancher-desktop',\n      credHelpers:    {\n        'https://bobs.fish/clams03': 'none',\n        'https://bobs.fish/clams05': 'none',\n      },\n    };\n    let existingDockerConfig: Buffer | undefined;\n\n    test.beforeAll(async() => {\n      const platform = os.platform();\n\n      if (platform.startsWith('win')) {\n        dockerConfig.credsStore = 'wincred';\n      } else if (platform === 'darwin') {\n        dockerConfig.credsStore = 'osxkeychain';\n      } else if (platform === 'linux') {\n        dockerConfig.credsStore = 'pass';\n      } else {\n        throw new Error(`Unexpected platform of ${ platform }`);\n      }\n      try {\n        existingDockerConfig = await fs.promises.readFile(dockerConfigPath);\n      } catch (ex) {\n        if (Object(ex).code !== 'ENOENT') {\n          throw ex;\n        }\n      }\n      await fs.promises.writeFile(dockerConfigPath, JSON.stringify(dockerConfig, undefined, 2));\n    });\n\n    test.afterAll(async() => {\n      if (existingDockerConfig) {\n        await fs.promises.writeFile(dockerConfigPath, existingDockerConfig);\n      } else {\n        await fs.promises.unlink(dockerConfigPath);\n      }\n    });\n\n    // removeEntries and addEntry return Promise<void>,\n    // so the best we can do is assert that they don't throw an exception.\n\n    test.beforeEach(async() => {\n      await expect(removeEntries(dockerConfig.credsStore, 'https://bobs.fish/clams')).resolves.not.toThrow();\n      await expect(removeEntries('none', 'https://bobs.fish/clams')).resolves.not.toThrow();\n    });\n\n    test('reading prepopulated entries through d-c-rd', async() => {\n      await expect(addEntry(dockerConfig.credsStore, peopleEntries.bob)).resolves.not.toThrow();\n      await expect(addEntry(dockerConfig.credsStore, peopleEntries.carol)).resolves.not.toThrow();\n      await expect(addEntry('none', peopleEntries.ted)).resolves.not.toThrow();\n      // These two should not be found\n      await expect(addEntry('none', peopleEntries.alice)).resolves.not.toThrow();\n      await expect(addEntry(dockerConfig.credsStore, peopleEntries.fakeTed)).resolves.not.toThrow();\n\n      // Now verify that `rdctl dcrd list` gives 01 ... 03 but not Fake-Ted 03, and not 04 because it's not discoverable.\n\n      const entries = JSON.parse(await doRequest('list'));\n\n      expect(entries).toMatchObject({\n        [peopleEntries.bob.ServerURL]:   peopleEntries.bob.Username,\n        [peopleEntries.carol.ServerURL]: peopleEntries.carol.Username,\n        [peopleEntries.ted.ServerURL]:   peopleEntries.ted.Username,\n      });\n      expect(entries).not.toMatchObject({\n        [peopleEntries.alice.ServerURL]:   peopleEntries.alice.Username,\n        [peopleEntries.fakeTed.ServerURL]: peopleEntries.fakeTed.Username,\n      });\n\n      // Then verify we can dcrd-get clams01, 02, and 03, but not 04 or 05\n      for (const name of ['bob', 'carol', 'ted']) {\n        const record = JSON.parse(await doRequest('get', peopleEntries[name].ServerURL));\n\n        expect(record).toMatchObject(peopleEntries[name] as unknown as Record<string, string>);\n      }\n      if (dockerConfig.credsStore !== 'pass') {\n        // TODO: Stop testing for pass once we bring in d-c-pass 0.7.0 or higher\n        await expect(doRequest('get', peopleEntries.alice.ServerURL, true))\n          .resolves.toEqual('credentials not found in native keychain\\n');\n        await expect(doRequest('get', 'https://bobs.fish/clams05', true))\n          .resolves.toEqual('credentials not found in native keychain\\n');\n      }\n\n      // Then dcrd-delete all of them, and verify that dcrd-list is empty.\n      // But use lower-level dc helpers to show that clams04 and Fake-Ted clams03 are still around,\n      // and then delete them.\n      await expect(doRequest('erase', peopleEntries.bob.ServerURL)).resolves.toEqual('');\n      await expect(doRequest('erase', peopleEntries.carol.ServerURL)).resolves.toEqual('');\n      await expect(doRequest('erase', peopleEntries.ted.ServerURL)).resolves.toEqual('');\n      // Looks like different credential-helpers handle missing erase arguments differently, so don't check results\n      await doRequest('erase', peopleEntries.alice.ServerURL);\n      await doRequest('erase', peopleEntries.fakeTed.ServerURL);\n\n      const postDeleteEntries = JSON.parse(await doRequest('list'));\n\n      expect(postDeleteEntries).not.toMatchObject({\n        [peopleEntries.bob.ServerURL]:   peopleEntries.bob.Username,\n        [peopleEntries.carol.ServerURL]: peopleEntries.carol.Username,\n        [peopleEntries.ted.ServerURL]:   peopleEntries.ted.Username,\n        [peopleEntries.alice.ServerURL]: peopleEntries.alice.Username,\n      });\n      await expect(listEntries(dockerConfig.credsStore, 'https://bobs.fish/clams')).resolves\n        .toMatchObject({ [peopleEntries.fakeTed.ServerURL]: peopleEntries.fakeTed.Username });\n      await expect(listEntries('none', 'https://bobs.fish/clams')).resolves\n        .toMatchObject({ [peopleEntries.alice.ServerURL]: peopleEntries.alice.Username });\n    });\n\n    test('dcrd store uses credHelpers', async() => {\n      // Use dcrd-store to store clams 01 ... 04, and show that they ended up where expected.\n      // This is the inverse of the previous test.\n      await doRequestExpectStatus('store', JSON.stringify(peopleEntries.bob), 200);\n      await doRequestExpectStatus('store', JSON.stringify(peopleEntries.carol), 200);\n      await doRequestExpectStatus('store', JSON.stringify(peopleEntries.ted), 200);\n      await doRequestExpectStatus('store', JSON.stringify(peopleEntries.alice), 200);\n\n      await expect(listEntries(dockerConfig.credsStore, 'https://bobs.fish/clams')).resolves.toMatchObject({\n        [peopleEntries.bob.ServerURL]:   peopleEntries.bob.Username,\n        [peopleEntries.carol.ServerURL]: peopleEntries.carol.Username,\n        [peopleEntries.alice.ServerURL]: peopleEntries.alice.Username,\n      });\n      await expect(listEntries(dockerConfig.credsStore, 'https://bobs.fish/clams'))\n        .resolves.not.toMatchObject({ [peopleEntries.ted.ServerURL]: peopleEntries.ted.Username });\n\n      await expect(listEntries('none', 'https://bobs.fish/clams')).resolves\n        .toMatchObject({ [peopleEntries.ted.ServerURL]: peopleEntries.ted.Username });\n      await expect(listEntries('none', 'https://bobs.fish/clams'))\n        .resolves.not.toMatchObject({\n          [peopleEntries.bob.ServerURL]:   peopleEntries.bob.Username,\n          [peopleEntries.carol.ServerURL]: peopleEntries.carol.Username,\n          [peopleEntries.alice.ServerURL]: peopleEntries.alice.Username,\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/extensions.e2e.spec.ts",
    "content": "/*\n * This tests interactions with the extension front end.\n * An E2E test is required to have access to the web page context.\n */\n\nimport os from 'os';\nimport path from 'path';\n\nimport {\n  ElectronApplication, Page, test, expect, JSHandle, TestInfo,\n} from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport {\n  getFullPathForTool, getResourceBinDir, reportAsset, retry, startSlowerDesktop, teardown,\n} from './utils/TestUtils';\n\nimport { ContainerEngine, Settings } from '@pkg/config/settings';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { Log } from '@pkg/utils/logging';\n\nimport type { BrowserWindow, WebContentsView } from 'electron';\n\n/** The top level source directory, assuming we're always running from the tree */\nconst srcDir = path.dirname(import.meta.dirname);\nconst rdctl = getFullPathForTool('rdctl');\n\n// On Windows there's an eval routine that treats backslashes as escape-sequence leaders,\n// so it's better to replace them with forward slashes. The file can still be found,\n// and we don't have to deal with unintended escape-sequence processing.\nconst execPath = process.execPath.replace(/\\\\/g, '/');\n\nlet console: Log;\nconst NAMESPACE = 'rancher-desktop-extensions';\n\ntest.describe.serial('Extensions', () => {\n  let app: ElectronApplication;\n  let page: Page;\n  let isContainerd = false;\n\n  async function ctrctl(...args: string[]) {\n    let tool = getFullPathForTool('nerdctl');\n\n    if (isContainerd) {\n      args = ['--namespace', NAMESPACE].concat(args);\n    } else {\n      tool = getFullPathForTool('docker');\n      if (process.platform !== 'win32') {\n        args = ['--context', 'rancher-desktop'].concat(args);\n      }\n    }\n\n    return await spawnFile(tool, args,\n      {\n        stdio: 'pipe',\n        env:   {\n          ...process.env,\n          PATH: `${ process.env.PATH }${ path.delimiter }${ getResourceBinDir() }`,\n        },\n      });\n  }\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [app, page] = await startSlowerDesktop(testInfo, {\n      containerEngine: { name: ContainerEngine.MOBY },\n      kubernetes:      { enabled: false },\n    });\n    console = new Log(path.basename(import.meta.filename, '.ts'), reportAsset(testInfo, 'log'));\n  });\n\n  test.afterAll(({ colorScheme }, testInfo) => teardown(app, testInfo));\n\n  // Set things up so console messages from the UI gets logged too.\n  let currentTestInfo: TestInfo;\n\n  test.beforeEach(({ browserName }, testInfo) => {\n    currentTestInfo = testInfo;\n  });\n  test.beforeAll(() => {\n    page.on('console', (message) => {\n      console.error(`${ currentTestInfo.titlePath.join(' >> ') } >> ${ message.text() }`);\n    });\n  });\n\n  test('should load backend', async() => {\n    await (new NavPage(page)).progressBecomesReady();\n  });\n\n  test('determine container engine in use', async() => {\n    const { stdout } = await spawnFile(rdctl, ['list-settings'], { stdio: 'pipe' });\n    const settings: Settings = JSON.parse(stdout);\n\n    expect(settings.containerEngine.name).toMatch(/^(?:containerd|moby)$/);\n    isContainerd = settings.containerEngine.name === ContainerEngine.CONTAINERD;\n  });\n\n  test('wait for buildkit', async() => {\n    test.skip(!isContainerd, 'Not running containerd, no need to wait for buildkit');\n\n    // `buildctl debug info` talks to the backend (to fetch info about it), so\n    // if it succeeds it means the backend is up and can respond to requests.\n    await retry(() => spawnFile(rdctl, ['shell', 'buildctl', 'debug', 'info']));\n  });\n\n  test('wait for docker context', async() => {\n    test.skip(isContainerd, 'Not running moby, no need to wait for context');\n    test.skip(process.platform === 'win32', 'Not setting context on Windows');\n\n    await retry(() => ctrctl('context', 'inspect', 'rancher-desktop'));\n  });\n\n  test('wait for docker daemon to be up', async() => {\n    test.skip(isContainerd, 'Not running moby, no need to wait for context');\n\n    // On Windows, the docker proxy can flap for a while. So we try a few times\n    // in a row (with pauses in the middle) to ensure the backend is stable\n    // before continuing.\n    for (let i = 0; i < 10; ++i) {\n      await retry(() => ctrctl('system', 'info'));\n      await new Promise(resolve => setTimeout(resolve, 1_000));\n    }\n  });\n\n  test('build and install testing extension', async() => {\n    const dataDir = path.join(srcDir, 'bats', 'tests', 'extensions', 'testdata');\n\n    try {\n      await ctrctl('build', '--tag', 'rd/extension/everything', '--build-arg', 'variant=everything', dataDir);\n      await spawnFile(rdctl, ['api', '-XPOST', '/v1/extensions/install?id=rd/extension/everything']);\n    } catch (ex) {\n      console.error(ex);\n      throw ex;\n    }\n  });\n\n  test('use extension protocol handler', async() => {\n    const result = await page.evaluate(async() => {\n      const data = await fetch('x-rd-extension://72642f657874656e73696f6e2f65766572797468696e67/ui/dashboard-tab/ui/index.html');\n\n      return await data.text();\n    });\n\n    expect(result).toContain('ddClient');\n  });\n\n  test.describe('extension API', () => {\n    let view: JSHandle<WebContentsView>;\n\n    test('extension UI can be loaded', async() => {\n      const window: JSHandle<BrowserWindow> = await app.browserWindow(page);\n\n      await page.click('.nav .nav-item[data-id=\"extension:rd/extension/everything\"]');\n\n      // Try until we can get a BrowserView for the extension (because it can\n      // take some time to load).\n      view = await retry(async() => {\n        // Evaluate script remotely to look for the appropriate BrowserView\n        const result = await window.evaluateHandle((window: BrowserWindow) => {\n          for (const view of window.contentView.children) {\n            // Because this runs in the remote window, we don't have access to\n            // imports, and therefore types; hence we can't do an `instanceof`\n            // check here.\n            if ('webContents' in view) {\n              if ((view as any).webContents.mainFrame.url.startsWith('x-rd-extension://')) {\n                return view;\n              }\n            }\n          }\n        }) as JSHandle<WebContentsView | undefined>;\n\n        // Check that the result evaluated to the view, and not undefined.\n        if (await (result).evaluate(v => typeof v) === 'undefined') {\n          throw new Error('Could not find extension view');\n        }\n\n        return result as JSHandle<WebContentsView>;\n      });\n\n      await view.evaluate((v, { window }) => {\n        v.webContents.addListener('console-message', (event) => {\n          const {\n            message, level, lineNumber, sourceId,\n          } = event;\n          const outputMessage = `[${ level }] ${ message } @${ sourceId }:${ lineNumber }`;\n\n          window.webContents.executeJavaScript(`console.log(${ JSON.stringify(outputMessage) })`);\n        });\n      }, { window });\n    });\n\n    /** evaluate a short snippet in the extension context. */\n    async function evalInView(script: string): Promise<any> {\n      // view.evaluate doesn't pass rejections correctly; instead, we convert it\n      // to a resolved object, and convert it back to a rejection on the other end.\n      const { rejected, result } = await view.evaluate(async(v, { script }) => {\n        try {\n          return { rejected: false, result: await v.webContents.executeJavaScript(script) };\n        } catch (ex) {\n          return { rejected: true, result: ex };\n        }\n      }, { script });\n\n      if (rejected) {\n        throw result;\n      }\n\n      return result;\n    }\n\n    test('exposes API endpoint', async() => {\n      const result = {\n        platform: await evalInView('ddClient.host.platform'),\n        arch:     await evalInView('ddClient.host.arch'),\n        hostname: await evalInView('ddClient.host.hostname'),\n      };\n\n      expect(result).toEqual({\n        platform: os.platform(),\n        arch:     os.arch(),\n        hostname: os.hostname(),\n      });\n    });\n\n    test.describe('ddClient.extension.host.cli.exec', () => {\n      const wrapperName = process.platform === 'win32' ? 'dummy.exe' : 'dummy.sh';\n\n      test('capturing output', async() => {\n        const script = `\n          ddClient.extension.host.cli.exec(\"${ wrapperName }\", [\n            \"${ execPath }\", \"-e\", \"console.log(1 + 1)\"\n          ]).then(({cmd, killed, signal, code, stdout, stderr}) => ({\n            /* Rebuild the object so it can be serialized properly */\n            cmd, killed, signal, code, stdout, stderr\n          }));\n        `;\n        const result = await evalInView(script);\n\n        expect(result).toEqual(expect.objectContaining({\n          cmd:    expect.stringContaining(wrapperName),\n          code:   0,\n          stdout: expect.stringContaining('2'),\n          stderr: expect.stringContaining(''),\n        }));\n      });\n\n      test('streaming output', async() => {\n        const script = `\n          (new Promise((resolve) => {\n            let output = [], errors = [], exitCodes = [];\n            ddClient.extension.host.cli.exec(\"${ wrapperName }\", [\n              \"${ execPath }\", \"-e\",\n              \"console.log(2 + 2); console.error(3 + 3);\"],\n              {\n                stream: {\n                  onOutput: (data) => {\n                    output.push(data);\n                  },\n                  onError: (err) => {\n                    errors.push(err);\n                    resolve(output, errors, exitCodes);\n                  },\n                  onClose: (exitCode) => {\n                    exitCodes.push(exitCode);\n                    resolve({output, errors, exitCodes});\n                  },\n                }\n            });\n          })).catch(ex => ex);\n        `;\n\n        const result = await evalInView(script);\n\n        expect(result).toEqual(expect.objectContaining({\n          output: expect.arrayContaining([\n            { stdout: expect.stringContaining('4') },\n            { stderr: expect.stringContaining('6') },\n          ]),\n          errors:    [],\n          exitCodes: [0],\n        }));\n      });\n\n      test('bypass CORS', async() => {\n        // This is the dashboard URL; it does not have CORS set up so it would\n        // normally fail to fetch due to CORS reasons.  However, this test case\n        // checks that our CORS bypass is working.\n        const url = 'http://127.0.0.1:6120/c/local/explorer/node';\n        const script = `\n          (async () => {\n            const result = await fetch('${ url }');\n            return Object.fromEntries(result.headers.entries());\n          })()\n        `;\n        const result = await evalInView(script);\n\n        expect(result).toHaveProperty('content-type');\n      });\n    });\n\n    test.describe('ddClient.docker', () => {\n      test('ddClient.docker.cli.exec', async() => {\n        const script = `\n          ddClient.docker.cli.exec(\"info\", [\"--format\", \"{{ json . }}\"])\n          .then(v => v.parseJsonObject())\n          .then(j => JSON.stringify(j));\n        `;\n        const result = JSON.parse(await evalInView(script));\n\n        expect(result).toEqual(expect.objectContaining({\n          ID:          expect.any(String),\n          Driver:      expect.any(String),\n          Plugins:     expect.objectContaining({}),\n          MemoryLimit: expect.any(Boolean),\n          SwapLimit:   expect.any(Boolean),\n          MemTotal:    expect.any(Number),\n          OSType:      'linux',\n        }));\n      });\n      test('ddClient.docker.listImages', async() => {\n        const options = {\n          digests:   true,\n          namespace: isContainerd ? NAMESPACE : undefined,\n        };\n        const script = `ddClient.docker.listImages(${ JSON.stringify(options) })`;\n        const result = await evalInView(script);\n\n        expect(result).toEqual(expect.arrayContaining([\n          expect.objectContaining({\n            Id:          expect.any(String),\n            ParentId:    expect.any(String),\n            RepoTags:    expect.arrayContaining(['rd/extension/everything:latest']),\n            Created:     expect.any(Number),\n            Size:        expect.any(Number),\n            SharedSize:  expect.any(Number),\n            VirtualSize: expect.anything(),\n            Labels:      expect.any(Object),\n            Containers:  expect.any(Number),\n          }),\n        ]));\n      });\n      test('ddClient.docker.listContainers', async() => {\n        const options = {\n          size:      !isContainerd, // nerdctl doesn't implement --size\n          namespace: isContainerd ? NAMESPACE : undefined,\n        };\n        const script = `ddClient.docker.listContainers(${ JSON.stringify(options) })`;\n        const result = await evalInView(script);\n        const container = result.find((r: { Image: string; }) => r.Image.startsWith('rd/extension/everything'));\n\n        // The playwright copy of expect() produces terrible error messages when\n        // things don't match, making it difficult to find what was wrong.\n        // Match properties individually to make things easier to spot.\n        expect(container).toBeTruthy();\n        expect(container).toHaveProperty('Id', expect.any(String));\n        expect(container).toHaveProperty('Names', expect.arrayContaining([expect.any(String)]));\n        expect(container).toHaveProperty('Image', expect.stringContaining('rd/extension/everything'));\n        expect(container).toHaveProperty('ImageID', expect.any(String));\n        expect(container).toHaveProperty('Command', expect.any(String));\n        expect(container).toHaveProperty('Created', expect.any(Number));\n        expect(container).toHaveProperty('Ports', expect.anything());\n        expect(container).toHaveProperty('SizeRw', expect.any(Number));\n        expect(container).toHaveProperty('SizeRootFs', expect.any(Number));\n        expect(container).toHaveProperty('Labels', expect.any(Object));\n        expect(container).toHaveProperty('State', expect.any(String));\n        expect(container).toHaveProperty('Status', expect.any(String));\n        expect(container).toHaveProperty('HostConfig', expect.any(Object));\n        expect(container).toHaveProperty('NetworkSettings', expect.any(Object));\n        expect(container).toHaveProperty('Mounts', expect.any(Array));\n      });\n    });\n\n    test.describe('ddClient.extension.vm.cli.exec', () => {\n      test('capturing output', async() => {\n        // `.exec()` returns an object that has methods, which cannot be passed\n        // via `webContents.executeJavaScript`; serialize it as JSON and\n        // deserialize instead.\n        const result = evalInView(`\n          ddClient.extension.vm.cli.exec(\"/bin/echo\", [\"xyzzy\"])\n          .then(v => JSON.parse(JSON.stringify(v)))\n        `);\n\n        await expect(result).resolves.toMatchObject({\n          stdout: 'xyzzy\\n',\n          code:   0,\n        });\n      });\n    });\n    test.describe('ddClient.extension.host.cli.exec', () => {\n      test('reject when command is not found', async() => {\n        // Errors cannot be round-tripped correctly.\n        const result = evalInView(`\n          ddClient.extension.host.cli.exec('command-not-found', [])\n          .catch(v => Promise.reject(v instanceof Error ? v.toString() : JSON.stringify(v)))\n        `);\n\n        await expect(result).rejects.toMatch(/ENOENT|The system cannot find the file specified/);\n      });\n      test('reject when command fails', async() => {\n        const command = process.platform === 'win32' ? 'dummy.exe' : 'dummy.sh';\n        // The returned exception has methods, which cannot be cloned across\n        // evalInView; we serialize it as JSON and deserialize again to remove them.\n        const result = evalInView(`\n          ddClient.extension.host.cli.exec(\"${ command }\", [\"false\"])\n          .then(v => Promise.resolve(JSON.parse(JSON.stringify(v))))\n          .catch(v => Promise.reject(JSON.parse(JSON.stringify(v))))\n        `);\n\n        await expect(result).rejects.toMatchObject({\n          code: 1,\n          cmd:  expect.stringMatching(/dummy.*false/),\n        });\n      });\n    });\n\n    test.describe('ddClient.extension.vm.service', () => {\n      test('can fetch from the backend', async() => {\n        const url = '/get/etc/os-release';\n\n        await retry(async() => {\n          const result = evalInView(`ddClient.extension.vm.service.get(\"${ url }\")`);\n\n          await expect(result).resolves.toContain('VERSION_ID');\n        });\n      });\n      test('can fetch from external sources', async() => {\n        const url = 'http://127.0.0.1:6120/c/local/explorer/node'; // dashboard\n\n        await retry(async() => {\n          const result = evalInView(`ddClient.extension.vm.service.get(\"${ url }\")`);\n\n          await expect(result).resolves.toContain('<title>Rancher</title>');\n        });\n      });\n      test.describe('can post values', () => {\n        test('with string body', async() => {\n          await retry(async() => {\n            const result = evalInView(`ddClient.extension.vm.service.post(\"/post\", \"hello\")`);\n\n            await expect(result).resolves.toMatchObject({\n              headers: { 'Content-Type': expect.arrayContaining([expect.stringMatching(/^text\\/plain\\b/)]) },\n              body:    'hello',\n            });\n          });\n        });\n        test('with JSON body', async() => {\n          await retry(async() => {\n            const result = evalInView(`ddClient.extension.vm.service.post(\"/post\", {foo: 'bar'})`);\n\n            await expect(result).resolves.toMatchObject({\n              headers: { 'Content-Type': expect.arrayContaining([expect.stringMatching(/^application\\/json\\b/)]) },\n              body:    JSON.stringify({ foo: 'bar' }),\n            });\n          });\n        });\n        test.describe('throws with error status', () => {\n          for (const statusCode of [400, 401, 403, 404, 451, 500, 503, 504]) {\n            for (const method of ['get', 'post']) {\n              test(`${ method } ${ statusCode }`, async() => {\n                const result = evalInView(`ddClient.extension.vm.service.${ method }(\"/status/${ statusCode }\", {})`);\n\n                await expect(result).rejects.toMatchObject({\n                  statusCode,\n                  message: expect.stringContaining(`${ statusCode }`),\n                });\n              });\n            }\n          }\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/lockedFields.e2e.spec.ts",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * Integration tests that verify that the deployment profile reader is finding locked fields,\n * and that rdctl can't change those locked preferences.\n */\n\nimport os from 'os';\nimport path from 'path';\n\nimport { expect, test } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { verifyNoSystemProfile } from './utils/ProfileUtils';\nimport {\n  createDefaultSettings, setUserProfile, startRancherDesktop, teardown, tool,\n} from './utils/TestUtils';\n\nimport type { DeploymentProfileType } from '@pkg/config/settings';\nimport { CURRENT_SETTINGS_VERSION } from '@pkg/config/settings';\nimport { readDeploymentProfiles } from '@pkg/main/deploymentProfiles';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { reopenLogs } from '@pkg/utils/logging';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\ntest.describe('Locked fields', () => {\n  let electronApp: ElectronApplication;\n  let page: Page;\n  const appPath = path.dirname(import.meta.dirname);\n  let deploymentProfile: DeploymentProfileType | null = null;\n\n  function rdctlPath() {\n    return path.join(appPath, 'resources', os.platform(), 'bin', os.platform() === 'win32' ? 'rdctl.exe' : 'rdctl');\n  }\n\n  async function rdctl(commandArgs: string[]): Promise< { stdout: string, stderr: string, error?: any }> {\n    try {\n      return await spawnFile(rdctlPath(), commandArgs, { stdio: 'pipe' });\n    } catch (err: any) {\n      return {\n        stdout: err?.stdout ?? '', stderr: err?.stderr ?? '', error: err,\n      };\n    }\n  }\n\n  async function saveUserProfile() {\n    // Ignore any errors in the existing profile, but it means they won't be saved.\n    try {\n      deploymentProfile = await readDeploymentProfiles();\n    } catch { }\n  }\n\n  async function restoreUserProfile() {\n    await setUserProfile(deploymentProfile?.defaults ?? null, deploymentProfile?.locked ?? null);\n  }\n\n  test.describe.configure({ mode: 'serial' });\n  const lockedK8sVersion = '1.26.3';\n  const proposedK8sVersion = '1.26.1';\n\n  test.beforeAll(async() => {\n    await tool('rdctl', 'reset', '--factory', '--verbose');\n    reopenLogs();\n  });\n\n  test.afterAll(async() => {\n    await restoreUserProfile();\n  });\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    createDefaultSettings({ containerEngine: { allowedImages: { enabled: true, patterns: ['a', 'b', 'c', 'e'] } } });\n    await saveUserProfile();\n    await setUserProfile(\n      { version: 10 as typeof CURRENT_SETTINGS_VERSION, containerEngine: { allowedImages: { enabled: true } } },\n      {\n        version:         10,\n        containerEngine: { allowedImages: { enabled: true, patterns: ['c', 'd', 'f'] } },\n        kubernetes:      { version: lockedK8sVersion },\n      },\n    );\n    electronApp = await startRancherDesktop(testInfo);\n    page = await electronApp.firstWindow();\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    await teardown(electronApp, testInfo);\n    await tool('rdctl', 'reset', '--factory', '--verbose');\n    reopenLogs();\n  });\n\n  test('should start up', async() => {\n    const navPage = new NavPage(page);\n\n    await navPage.progressBecomesReady();\n    await expect(navPage.progressBar).toBeHidden();\n  });\n\n  test('should not allow a locked field to be changed via rdctl set', async() => {\n    const { stdout, stderr, error } = await rdctl(['list-settings']);\n    const skipReasons = await verifyNoSystemProfile();\n\n    test.skip(skipReasons.length > 0, skipReasons.join('\\n'));\n    expect({ stderr, error }).toEqual({ error: undefined, stderr: '' });\n    const originalSettings = JSON.parse(stdout);\n    const newEnabled = !originalSettings.containerEngine.allowedImages.enabled;\n\n    expect(originalSettings.containerEngine.allowedImages.patterns.sort()).toEqual(['c', 'd', 'f']);\n    await expect(rdctl(['set', `--container-engine.allowed-images.enabled=${ newEnabled }`]))\n      .resolves.toMatchObject({\n        stdout: '',\n        stderr: expect.stringContaining(`field \"containerEngine.allowedImages.enabled\" is locked`),\n      });\n    await expect(rdctl(['set', `--kubernetes.version=${ proposedK8sVersion }`]))\n      .resolves.toMatchObject({\n        stdout: '',\n        stderr: expect.stringContaining(`field \"kubernetes.version\" is locked`),\n      });\n    await expect(rdctl(['set', `--kubernetes.version=${ lockedK8sVersion }`]))\n      .resolves.toMatchObject({\n        stdout: expect.stringContaining('Status: no changes necessary.'),\n        stderr: '',\n      });\n  });\n});\n"
  },
  {
    "path": "e2e/main.e2e.spec.ts",
    "content": "import { test, expect, _electron } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { createDefaultSettings, startRancherDesktop, teardown } from './utils/TestUtils';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\nlet page: Page;\n\n/**\n * Using test.describe.serial make the test execute step by step, as described on each `test()` order\n * Playwright executes test in parallel by default and it will not work for our app backend loading process.\n * */\ntest.describe.serial('Main App Test', () => {\n  let electronApp: ElectronApplication;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    createDefaultSettings();\n\n    electronApp = await startRancherDesktop(testInfo);\n    page = await electronApp.firstWindow();\n  });\n\n  test.afterAll(({ colorScheme }, testInfo) => teardown(electronApp, testInfo));\n\n  test('should start loading the background services and hide progress bar', async() => {\n    const navPage = new NavPage(page);\n\n    await navPage.progressBecomesReady();\n    await expect(navPage.progressBar).toBeHidden();\n  });\n\n  test('should land on General page', async() => {\n    const navPage = new NavPage(page);\n\n    await expect(navPage.mainTitle).toHaveText('Welcome to Rancher Desktop by SUSE');\n  });\n\n  test('should navigate to Port Forwarding and check elements', async() => {\n    const navPage = new NavPage(page);\n    const portForwardPage = await navPage.navigateTo('PortForwarding');\n\n    await expect(navPage.mainTitle).toHaveText('Port Forwarding');\n    await expect(portForwardPage.content).toBeVisible();\n    await expect(portForwardPage.table).toBeVisible();\n    await expect(portForwardPage.fixedHeader).toBeVisible();\n  });\n\n  test('should navigate to Images page', async() => {\n    const navPage = new NavPage(page);\n    const imagesPage = await navPage.navigateTo('Images');\n\n    await expect(navPage.mainTitle).toHaveText('Images');\n    await expect(imagesPage.table).toBeVisible();\n  });\n\n  test('should navigate to Troubleshooting and check elements', async() => {\n    const navPage = new NavPage(page);\n    const troubleshootingPage = await navPage.navigateTo('Troubleshooting');\n\n    await expect(navPage.mainTitle).toHaveText('Troubleshooting');\n    await expect(troubleshootingPage.troubleshooting).toBeVisible();\n    await expect(troubleshootingPage.logsButton).toBeVisible();\n    await expect(troubleshootingPage.factoryResetButton).toBeVisible();\n  });\n});\n"
  },
  {
    "path": "e2e/pages/container-logs-page.ts",
    "content": "import { expect } from '@playwright/test';\n\nimport type { Locator, Page } from '@playwright/test';\n\nexport class ContainerLogsPage {\n  readonly page:              Page;\n  readonly terminal:          Locator;\n  readonly containerInfo:     Locator;\n  readonly containerName:     Locator;\n  readonly containerState:    Locator;\n  readonly searchWidget:      Locator;\n  readonly searchInput:       Locator;\n  readonly searchPrevButton:  Locator;\n  readonly searchNextButton:  Locator;\n  readonly searchClearButton: Locator;\n  readonly errorMessage:      Locator;\n  readonly loadingIndicator:  Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n\n    this.terminal = page.getByTestId('terminal');\n\n    this.containerInfo = page.getByTestId('container-info');\n    this.containerName = page.locator('[data-test=\"mainTitle\"]');\n    this.containerState = page.getByTestId('container-state');\n\n    this.searchWidget = page.getByTestId('search-widget');\n    this.searchInput = page.getByTestId('search-input');\n    this.searchPrevButton = page.getByTestId('search-prev-btn');\n    this.searchNextButton = page.getByTestId('search-next-btn');\n    this.searchClearButton = page.getByTestId('search-clear-btn');\n\n    this.loadingIndicator = page.getByTestId('loading-indicator');\n    this.errorMessage = page.getByTestId('error-message');\n  }\n\n  async waitForLogsToLoad() {\n    await expect(this.terminal).toBeVisible();\n    await expect(this.loadingIndicator).toBeHidden({ timeout: 30_000 });\n  }\n\n  async searchLogs(searchTerm: string) {\n    await this.searchInput.fill(searchTerm);\n    await this.searchInput.press('Enter');\n  }\n\n  async scrollToBottom() {\n    await this.page.evaluate(() => {\n      const container = document.querySelector('[data-testid=\"terminal\"]');\n\n      container?.__xtermTerminal?.scrollToBottom();\n    });\n  }\n\n  async scrollToTop() {\n    await this.page.evaluate(() => {\n      const container = document.querySelector('[data-testid=\"terminal\"]');\n\n      container?.__xtermTerminal?.scrollToTop();\n    });\n  }\n\n  async getScrollPosition(): Promise<number> {\n    return await this.page.evaluate(() => {\n      const container = document.querySelector('[data-testid=\"terminal\"]');\n\n      return container?.__xtermTerminal?.buffer.active.viewportY ?? 0;\n    });\n  }\n}\n"
  },
  {
    "path": "e2e/pages/container-shell-page.ts",
    "content": "import { expect } from '@playwright/test';\n\nimport type { Locator, Page } from '@playwright/test';\n\nexport class ContainerShellPage {\n  readonly page:              Page;\n  readonly tab:               Locator;\n  readonly terminal:          Locator;\n  readonly notRunningBanner:  Locator;\n  readonly unsupportedBanner: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.tab = page.getByTestId('tab-shell');\n    this.terminal = page.getByTestId('terminal');\n    this.notRunningBanner = page.getByTestId('shell-not-running');\n    this.unsupportedBanner = page.getByTestId('shell-unsupported');\n  }\n\n  async clickTab() {\n    // Wait until the Shell tab is enabled (not disabled).\n    // The tab is disabled when isRunning is false, which can happen briefly\n    // after navigating to the container info page: the previous page's\n    // beforeUnmount calls container-engine/unsubscribe (clearing the Vuex\n    // containers store) before the new page's subscription has re-populated\n    // it.  Clicking a disabled tab does nothing and the test would time out.\n    await expect(this.tab).not.toHaveClass(/\\bdisabled\\b/, { timeout: 30_000 });\n    await this.tab.click();\n  }\n\n  async waitForTerminal() {\n    await expect(this.terminal).toBeVisible({ timeout: 30_000 });\n  }\n\n  /** Type a command and press Enter, using the hidden xterm textarea. */\n  async runCommand(command: string) {\n    // ContainerShell auto-focuses the terminal when the shell tab becomes\n    // active for real users, but Playwright's keyboard routing requires an\n    // explicit click to track the focused element correctly.\n    await this.terminal.click();\n    await this.page.keyboard.type(command);\n    await this.page.keyboard.press('Enter');\n  }\n\n  /**\n   * Read terminal content via the xterm.js buffer API.\n   * ContainerShell.vue deliberately exposes the terminal instance as\n   * __xtermTerminal on the container element for e2e testing.  We use the\n   * buffer API rather than .xterm-rows textContent for two reasons:\n   *   1. It avoids coupling to xterm's internal DOM structure, which can\n   *      change between versions.\n   *   2. .xterm-rows textContent concatenates all rows without line\n   *      separators, so multiline patterns would never match even when the\n   *      text is present across consecutive rows.\n   */\n  async getTerminalText(): Promise<string> {\n    return this.page.evaluate(() => {\n      const el = document.querySelector('[data-testid=\"terminal\"]');\n      const term = (el as any)?.__xtermTerminal;\n\n      if (!term) return '';\n      const buf = term.buffer.active;\n      const lines: string[] = [];\n\n      for (let i = 0; i < buf.length; i++) {\n        const line = buf.getLine(i);\n\n        if (line) {\n          lines.push(line.translateToString(true));\n        }\n      }\n\n      return lines.join('\\n');\n    });\n  }\n\n  /** Wait for a text string to appear anywhere in the terminal. */\n  async waitForOutput(text: string, timeout = 15_000) {\n    await expect.poll(\n      () => this.getTerminalText(),\n      { timeout },\n    ).toContain(text);\n  }\n\n  /**\n   * Wait until the shell session is ready to accept input.\n   * ContainerShell.vue sets data-session-active=\"true\" on the terminal element\n   * when the container-exec/ready IPC event fires (which is also when\n   * sessionActive becomes true and keyboard input starts being forwarded).\n   * Using an HTML attribute rather than the xterm buffer means this assertion\n   * works regardless of JS evaluation world boundaries in Playwright/Electron.\n   */\n  async waitForShellReady(timeout = 20_000) {\n    await expect(this.terminal).toHaveAttribute('data-session-active', 'true', { timeout });\n  }\n}\n"
  },
  {
    "path": "e2e/pages/containers-page.ts",
    "content": "import { Locator, Page, expect } from '@playwright/test';\n\ntype ActionString = 'info' | 'stop' | 'start' | 'delete';\n\nexport class ContainersPage {\n  readonly page:              Page;\n  readonly table:             Locator;\n  readonly containers:        Locator;\n  readonly namespaceSelector: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.table = page.locator('.sortable-table');\n    this.containers = this.table.locator(`tr.main-row[data-node-id]`);\n    this.namespaceSelector = page.locator('.select-namespace');\n  }\n\n  getContainerRow(containerId: string) {\n    return this.table.locator(`tr.main-row[data-node-id=\"${ containerId }\"]`);\n  }\n\n  async waitForContainerToAppear(containerId: string, timeout = 30_000) {\n    const containerRow = this.getContainerRow(containerId);\n    await expect(containerRow).toBeVisible({ timeout });\n  }\n\n  async clickContainerAction(containerId: string, action: ActionString) {\n    const containerRow = this.getContainerRow(containerId);\n    // The action button is in the actions column with class 'btn role-multi-action'\n    await containerRow.locator('.btn.role-multi-action').click();\n\n    // Wait for the action menu to appear and click the action by text\n    const actionText = {\n      info:   'Info',\n      stop:   'Stop',\n      start:  'Start',\n      delete: 'Delete',\n    }[action];\n\n    const actionLocator = this.page\n      .getByTestId('actionmenu')\n      .getByText(actionText, { exact: true });\n    await actionLocator.click();\n  }\n\n  async viewContainerInfo(containerId: string) {\n    await this.clickContainerAction(containerId, 'info');\n    await this.page.getByTestId('tab-logs').click();\n  }\n\n  async stopContainer(containerId: string) {\n    await this.clickContainerAction(containerId, 'stop');\n  }\n\n  async startContainer(containerId: string) {\n    await this.clickContainerAction(containerId, 'start');\n  }\n\n  async deleteContainer(containerId: string) {\n    await this.clickContainerAction(containerId, 'delete');\n  }\n\n  async getContainerCount(): Promise<number> {\n    const rows = this.table.locator('tr.main-row');\n    return await rows.count();\n  }\n\n  async waitForTableToLoad() {\n    await this.table.waitFor({ state: 'visible' });\n  }\n}\n"
  },
  {
    "path": "e2e/pages/diagnostics-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\ninterface CheckerRows {\n  muteButton: Locator;\n}\n\nexport class DiagnosticsPage {\n  readonly page:        Page;\n  readonly diagnostics: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.diagnostics = page.locator('[data-test=\"diagnostics\"]');\n  }\n\n  checkerRows(id: string): CheckerRows {\n    return { muteButton: this.page.locator(`[data-test=\"diagnostics-mute-row-${ id }\"]`) };\n  }\n}\n"
  },
  {
    "path": "e2e/pages/extensions-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class ExtensionsPage {\n  readonly page:          Page;\n  readonly cardEpinio:    Locator;\n  readonly buttonInstall: Locator;\n  readonly tabInstalled:  Locator;\n  readonly tabCatalog:    Locator;\n  readonly navEpinio:     Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.cardEpinio = page.locator('[data-test=\"extension-card-epinio\"]');\n    this.buttonInstall = page.locator('[data-test=\"button-install\"]');\n    this.tabInstalled = page.locator('.tab >> text=Installed');\n    this.tabCatalog = page.locator('.tab >> text=Catalog');\n    this.navEpinio = page.locator('[data-test=\"extension-nav-epinio\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/images-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class ImagesPage {\n  readonly page:        Page;\n  readonly fixedHeader: Locator;\n  readonly table:       Locator;\n  readonly rows:        Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.fixedHeader = page.locator('.fixed-header-actions');\n    this.table = page.locator('[data-test=\"imagesTable\"]');\n    this.rows = page.locator('[data-test=\"imagesTableRows\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/k8s-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\nexport class K8sPage {\n  readonly page:             Page;\n  readonly engineRuntime:    Locator;\n  readonly memorySlider:     Locator;\n  readonly resetButton:      Locator;\n  readonly cpuSlider:        Locator;\n  readonly port:             Locator;\n  readonly enableKubernetes: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.memorySlider = page.locator('[id=\"memoryInGBWrapper\"]');\n    this.resetButton = page.locator('[data-test=\"k8sResetBtn\"]');\n    this.cpuSlider = page.locator('[id=\"numCPUWrapper\"]');\n    this.engineRuntime = page.locator('.engine-selector');\n    this.port = page.locator('[data-test=\"portConfig\"]');\n    this.enableKubernetes = page.locator('[data-test=\"enableKubernetes\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/nav-page.ts",
    "content": "import util from 'util';\n\nimport { ContainersPage } from './containers-page';\nimport { DiagnosticsPage } from './diagnostics-page';\nimport { ExtensionsPage } from './extensions-page';\nimport { ImagesPage } from './images-page';\nimport { K8sPage } from './k8s-page';\nimport { PortForwardPage } from './portforward-page';\nimport { SnapshotsPage } from './snapshots-page';\nimport { TroubleshootingPage } from './troubleshooting-page';\nimport { VolumesPage } from './volumes-page';\nimport { WSLIntegrationsPage } from './wsl-integrations-page';\nimport { tool } from '../utils/TestUtils';\n\nimport type { Locator, Page } from '@playwright/test';\n\nconst pageConstructors = {\n  General:         (page: Page) => page,\n  K8s:             (page: Page) => new K8sPage(page),\n  WSLIntegrations: (page: Page) => new WSLIntegrationsPage(page),\n  Containers:      (page: Page) => new ContainersPage(page),\n  PortForwarding:  (page: Page) => new PortForwardPage(page),\n  Images:          (page: Page) => new ImagesPage(page),\n  Troubleshooting: (page: Page) => new TroubleshootingPage(page),\n  Snapshots:       (page: Page) => new SnapshotsPage(page),\n  Diagnostics:     (page: Page) => new DiagnosticsPage(page),\n  Extensions:      (page: Page) => new ExtensionsPage(page),\n  Volumes:         (page: Page) => new VolumesPage(page),\n};\n\nexport class NavPage {\n  readonly page:              Page;\n  readonly progressBar:       Locator;\n  readonly mainTitle:         Locator;\n  readonly preferencesButton: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.mainTitle = page.locator('[data-test=\"mainTitle\"]');\n    this.progressBar = page.locator('.progress');\n    this.preferencesButton = page.getByTestId('preferences-button');\n  }\n\n  protected async getBackendState(): Promise<string> {\n    try {\n      return JSON.parse(await tool('rdctl', 'api', '/v1/backend_state')).vmState;\n    } catch {\n      return 'NOT_READY';\n    }\n  }\n\n  protected async moveToNextState(currentState: string, timeout: number): Promise<string> {\n    const start = new Date().valueOf();\n    const expired = start + timeout;\n    const delay = 500; // msec\n\n    while (true) {\n      try {\n        const nextState = JSON.parse(await tool('rdctl', 'api', '/v1/backend_state')).vmState;\n\n        if (nextState !== currentState) {\n          return nextState;\n        }\n      } catch (e: any) {\n        console.log(`Error trying to get backend state: ${ e }`);\n      }\n      const now = new Date().valueOf();\n\n      if (now >= expired) {\n        throw new Error(`app watcher timed out at state ${ currentState } waiting for state change after ${ timeout / 1000 } seconds`);\n      }\n      await util.promisify(setTimeout)(delay);\n    }\n  }\n\n  /**\n   * This process wait the progress bar to be visible and then\n   * waits until the progress bar be detached/hidden.\n   * This is a workaround until we implement:\n   * https://github.com/rancher-sandbox/rancher-desktop/issues/1217\n   */\n  /*\n    STOPPED = 'STOPPED', // The engine is not running.\n    STARTING = 'STARTING', // The engine is attempting to start.\n    STARTED = 'STARTED', // The engine is started; the dashboard is not yet ready.\n    STOPPING = 'STOPPING', // The engine is attempting to stop.\n    ERROR = 'ERROR', // There is an error and we cannot recover automatically.\n    DISABLED = 'DISABLED', // The container backend is ready but the Kubernetes engine is disabled.\n    NOT_READY = 'NOT_READY', // call to `rdctl api /v1/backend_state` failed, so assume the server isn't ready\n   */\n\n  // Implement a state-machine based on the backend states until we hit STOPPED, DISABLED, or ERROR, or timeout\n  // Then verify the progress bar is gone\n  async progressBecomesReady() {\n    const timeout = 900_000;\n    const maxAllowedStateChanges = 20;\n    let i;\n    let backendState = await this.getBackendState();\n    const finalStates = ['STARTED', 'ERROR', 'DISABLED'];\n\n    for (i = 0; i < maxAllowedStateChanges && !finalStates.includes(backendState); i++) {\n      if (backendState !== 'STARTING') {\n        console.log(`Backend is currently at state ${ backendState }, waiting for a change...`);\n      }\n      backendState = await this.moveToNextState(backendState, timeout);\n    }\n    if (i === maxAllowedStateChanges && !finalStates.includes(backendState)) {\n      throw new Error(`The backend is stuck in state ${ backendState }; doesn't look good`);\n    }\n\n    // Wait until progress bar be detached. With that we can make sure the services were started\n    // This seems to sometimes return too early; actually check the result.\n    while (await this.progressBar.count() > 0) {\n      await this.progressBar.waitFor({ state: 'detached', timeout: Math.round(timeout * 0.6) });\n    }\n  }\n\n  /**\n   * Navigate to a given tab, returning the page object model appropriate for\n   * the destination tab.\n   */\n  async navigateTo<pageName extends keyof typeof pageConstructors>(tab: pageName):\n  Promise<ReturnType<typeof pageConstructors[pageName]>>;\n\n  async navigateTo(tab: keyof typeof pageConstructors) {\n    const pageLoadHooks: Partial<Record<keyof typeof pageConstructors, (this: NavPage) => Promise<unknown>>> = {\n      Extensions: async function() { await this.page.waitForSelector('.extensions-page', { timeout: 60_000 }) },\n    };\n\n    await this.page.click(`.nav li[item=\"/${ tab }\"] a`);\n    await this.page.waitForURL(`**/${ tab }*`, { timeout: 60_000 });\n    await pageLoadHooks[tab]?.call(this);\n\n    return pageConstructors[tab](this.page);\n  }\n}\n"
  },
  {
    "path": "e2e/pages/portforward-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\nexport class PortForwardPage {\n  readonly page:        Page;\n  readonly fixedHeader: Locator;\n  readonly content:     Locator;\n  readonly table:       Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.content = page.locator('.body > .content');\n    this.table = this.content.getByTestId('sortable-table-list-container');\n    this.fixedHeader = this.table.locator('.fixed-header-actions');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/application.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\nexport class ApplicationNav {\n  readonly page:                     Page;\n  readonly nav:                      Locator;\n  readonly tabBehavior:              Locator;\n  readonly tabEnvironment:           Locator;\n  readonly tabGeneral:               Locator;\n  readonly administrativeAccess:     Locator;\n  readonly automaticUpdates:         Locator;\n  readonly automaticUpdatesCheckbox: Locator;\n  readonly statistics:               Locator;\n  readonly autoStart:                Locator;\n  readonly background:               Locator;\n  readonly notificationIcon:         Locator;\n  readonly pathManagement:           Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.nav = page.locator('[data-test=\"nav-application\"]');\n    this.tabBehavior = page.locator('.tab >> text=Behavior');\n    this.tabEnvironment = page.locator('.tab >> text=Environment');\n    this.tabGeneral = page.locator('.tab >> text=General');\n    this.administrativeAccess = page.locator('[data-test=\"administrativeAccess\"]');\n    this.automaticUpdates = page.locator('[data-test=\"automaticUpdates\"]');\n    this.automaticUpdatesCheckbox = page.locator('[data-test=\"automaticUpdatesCheckbox\"]');\n    this.statistics = page.locator('[data-test=\"statistics\"]');\n    this.autoStart = page.locator('[data-test=\"autoStart\"]');\n    this.background = page.locator('[data-test=\"background\"]');\n    this.notificationIcon = page.locator('[data-test=\"notificationIcon\"]');\n    this.pathManagement = page.locator('[data-test=\"pathManagement\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/containerEngine.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class ContainerEngineNav {\n  readonly page:                  Page;\n  readonly nav:                   Locator;\n  readonly tabGeneral:            Locator;\n  readonly tabAllowedImages:      Locator;\n  readonly containerEngine:       Locator;\n  readonly allowedImages:         Locator;\n  readonly allowedImagesCheckbox: Locator;\n  readonly enabledLockedField:    Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.nav = page.locator('[data-test=\"nav-container-engine\"]');\n    this.tabGeneral = page.locator('.tab >> text=General');\n    this.tabAllowedImages = page.locator('.tab >> text=Allowed Images');\n    this.containerEngine = page.locator('[data-test=\"containerEngine\"]');\n    this.allowedImages = page.locator('[data-test=\"allowedImages\"]');\n    this.allowedImagesCheckbox = page.getByTestId('allowedImagesCheckbox');\n    this.enabledLockedField = this.allowedImagesCheckbox.locator('.icon-lock');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/index.ts",
    "content": "import { ApplicationNav } from './application';\nimport { ContainerEngineNav } from './containerEngine';\nimport { KubernetesNav } from './kubernetes';\nimport { VirtualMachineNav } from './virtualMachine';\nimport { WslNav } from './wsl';\n\nimport type { Page } from '@playwright/test';\n\nexport class PreferencesPage {\n  readonly page:            Page;\n  readonly application:     ApplicationNav;\n  readonly virtualMachine:  VirtualMachineNav;\n  readonly containerEngine: ContainerEngineNav;\n  readonly kubernetes:      KubernetesNav;\n  readonly wsl:             WslNav;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.application = new ApplicationNav(page);\n    this.virtualMachine = new VirtualMachineNav(page);\n    this.containerEngine = new ContainerEngineNav(page);\n    this.kubernetes = new KubernetesNav(page);\n    this.wsl = new WslNav(page);\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/kubernetes.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class KubernetesNav {\n  readonly page:                          Page;\n  readonly nav:                           Locator;\n  readonly kubernetesToggle:              Locator;\n  readonly kubernetesVersion:             Locator;\n  readonly kubernetesPort:                Locator;\n  readonly kubernetesOptions:             Locator;\n  readonly kubernetesVersionLockedFields: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.nav = page.locator('[data-test=\"nav-kubernetes\"]');\n    this.kubernetesToggle = page.locator('[data-test=\"kubernetesToggle\"]');\n    this.kubernetesVersion = page.locator('[data-test=\"kubernetesVersion\"]');\n    this.kubernetesPort = page.locator('[data-test=\"kubernetesPort\"]');\n    this.kubernetesOptions = page.locator('[data-test=\"kubernetesOptions\"]');\n    this.kubernetesVersionLockedFields = page.locator('[data-test=\"kubernetesVersion\"] > .select-k8s-version > .icon-lock');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/virtualMachine.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class VirtualMachineNav {\n  readonly page:            Page;\n  readonly nav:             Locator;\n  readonly memory:          Locator;\n  readonly cpus:            Locator;\n  readonly mountType:       Locator;\n  readonly reverseSshFs:    Locator;\n  readonly ninep:           Locator;\n  readonly virtiofs:        Locator;\n  readonly cacheMode:       Locator;\n  readonly msizeInKib:      Locator;\n  readonly protocolVersion: Locator;\n  readonly securityModel:   Locator;\n  readonly vmType:          Locator;\n  readonly qemu:            Locator;\n  readonly vz:              Locator;\n  readonly useRosetta:      Locator;\n  readonly tabHardware:     Locator;\n  readonly tabVolumes:      Locator;\n  readonly tabEmulation:    Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.nav = page.locator('[data-test=\"nav-virtual-machine\"]');\n    this.memory = page.locator('#memoryInGBWrapper');\n    this.cpus = page.locator('#numCPUWrapper');\n    this.mountType = page.locator('[data-test=\"mountType\"]');\n    this.reverseSshFs = page.locator('[data-test=\"reverse-sshfs\"]');\n    this.ninep = page.locator('[data-test=\"9p\"]');\n    this.virtiofs = page.locator('[data-test=\"virtiofs\"]');\n    this.cacheMode = page.locator('[data-test=\"cacheMode\"]');\n    this.msizeInKib = page.locator('[data-test=\"msizeInKib\"]');\n    this.protocolVersion = page.locator('[data-test=\"protocolVersion\"]');\n    this.securityModel = page.locator('[data-test=\"securityModel\"]');\n    this.vmType = page.locator('[data-test=\"vmType\"]');\n    this.qemu = page.locator('[data-test=\"QEMU\"]');\n    this.vz = page.locator('[data-test=\"VZ\"]');\n    this.useRosetta = page.locator('[data-test=\"useRosetta\"]');\n    this.tabHardware = page.getByTestId('hardware');\n    this.tabVolumes = page.getByTestId('volumes');\n    this.tabEmulation = page.getByTestId('emulation');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/preferences/wsl.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class WslNav {\n  readonly page:            Page;\n  readonly nav:             Locator;\n  readonly wslIntegrations: Locator;\n  readonly addressTitle:    Locator;\n  readonly tabIntegrations: Locator;\n  readonly tabProxy:        Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.nav = page.locator('[data-test=\"nav-wsl\"]');\n    this.tabIntegrations = page.locator('.tab >> text=Integrations');\n    this.tabProxy = page.locator('.tab >> text=Proxy');\n    this.wslIntegrations = page.locator('[data-test=\"wslIntegrations\"]');\n    this.addressTitle = page.locator('[data-test=\"addressTitle\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/snapshots-page.ts",
    "content": "import { Locator, Page } from '@playwright/test';\n\nexport class SnapshotsPage {\n  readonly page:                    Page;\n  readonly snapshotsPage:           Locator;\n  readonly createSnapshotButton:    Locator;\n  readonly createSnapshotNameInput: Locator;\n  readonly createSnapshotDescInput: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.snapshotsPage = page.locator('[data-test=\"snapshotsPage\"]');\n    this.createSnapshotButton = page.locator('[data-test=\"createSnapshotButton\"]');\n    this.createSnapshotNameInput = page.locator('[data-test=\"createSnapshotNameInput\"]');\n    this.createSnapshotDescInput = page.locator('[data-test=\"createSnapshotDescInput\"]');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/troubleshooting-page.ts",
    "content": "import type { Page, Locator } from '@playwright/test';\n\nexport class TroubleshootingPage {\n  readonly page:               Page;\n  readonly factoryResetButton: Locator;\n  readonly logsButton:         Locator;\n  readonly troubleshooting:    Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.factoryResetButton = page.locator('[data-test=\"factoryResetButton\"]');\n    this.logsButton = page.locator('[data-test=\"logsButton\"]');\n    this.troubleshooting = page.locator('.troubleshooting');\n  }\n}\n"
  },
  {
    "path": "e2e/pages/volumes-page.ts",
    "content": "import { expect } from '@playwright/test';\n\nimport type { Locator, Page } from '@playwright/test';\n\ntype ActionString = 'browse' | 'delete';\n\nconst VOLUME_CELL_TEST_IDS = {\n  name:       'volume-name-cell',\n  driver:     'volume-driver-cell',\n  mountpoint: 'volume-mountpoint-cell',\n  created:    'volume-created-cell',\n} as const;\n\nexport class VolumesPage {\n  readonly page:              Page;\n  readonly table:             Locator;\n  readonly volumes:           Locator;\n  readonly namespaceSelector: Locator;\n  readonly searchBox:         Locator;\n  readonly errorBanner:       Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.table = page.getByTestId('volumes-table');\n    this.volumes = this.table.locator(`tr.main-row[data-node-id]`);\n    this.namespaceSelector = page.getByTestId('namespace-selector');\n    this.searchBox = page.getByTestId('search-input');\n    this.errorBanner = page.getByTestId('error-banner');\n  }\n\n  getVolumeRow(volumeName: string) {\n    return this.page.locator(`tr.main-row[data-node-id=\"${ volumeName }\"]`);\n  }\n\n  async waitForVolumeToAppear(volumeName: string) {\n    const volumeRow = this.getVolumeRow(volumeName);\n    await expect(volumeRow).toBeVisible({ timeout: 15000 });\n  }\n\n  async clickVolumeAction(volumeName: string, action: ActionString) {\n    const volumeRow = this.getVolumeRow(volumeName);\n    const actionButton = volumeRow.locator('.btn.role-multi-action.actions');\n    await expect(actionButton).toBeVisible({ timeout: 5_000 });\n    await actionButton.click();\n\n    const actionText = {\n      browse: 'Browse Files',\n      delete: 'Delete',\n    }[action];\n\n    const actionMenu = this.page.getByTestId('actionmenu');\n    const actionLocator = actionMenu.getByText(actionText, { exact: true });\n    await actionLocator.click();\n  }\n\n  async browseVolumeFiles(volumeName: string) {\n    await this.clickVolumeAction(volumeName, 'browse');\n  }\n\n  async deleteVolume(volumeName: string) {\n    await this.clickVolumeAction(volumeName, 'delete');\n  }\n\n  async getVolumeCount(): Promise<number> {\n    const rows = this.page.locator('tr.main-row');\n    return await rows.count();\n  }\n\n  async waitForTableToLoad() {\n    await expect(this.table).toBeVisible();\n  }\n\n  async isVolumePresent(volumeName: string): Promise<boolean> {\n    const row = this.getVolumeRow(volumeName);\n    return await row.isVisible().catch(() => false);\n  }\n\n  async searchVolumes(searchTerm: string) {\n    await this.searchBox.fill(searchTerm);\n  }\n\n  getVolumeInfo(volumeName: string) {\n    const volumeRow = this.getVolumeRow(volumeName);\n\n    return {\n      row:        volumeRow,\n      name:       volumeRow.getByTestId(VOLUME_CELL_TEST_IDS.name),\n      driver:     volumeRow.getByTestId(VOLUME_CELL_TEST_IDS.driver),\n      mountpoint: volumeRow.getByTestId(VOLUME_CELL_TEST_IDS.mountpoint),\n      created:    volumeRow.getByTestId(VOLUME_CELL_TEST_IDS.created),\n    };\n  }\n\n  async selectBulkVolumes(volumeNames: string[]) {\n    for (const volumeName of volumeNames) {\n      const volumeRow = this.getVolumeRow(volumeName);\n      const checkbox = volumeRow.locator('.selection-checkbox');\n\n      await checkbox.click();\n      await expect(volumeRow.locator('input[type=\"checkbox\"]')).toBeChecked();\n    }\n  }\n\n  async clickBulkDelete() {\n    // Use the direct delete button that appears when items are selected\n    const deleteButton = this.page\n      .getByRole('button', { name: 'Delete' })\n      .first();\n    await deleteButton.click();\n  }\n\n  async deleteBulkVolumes(volumeNames: string[]) {\n    await this.selectBulkVolumes(volumeNames);\n    await this.clickBulkDelete();\n  }\n}\n"
  },
  {
    "path": "e2e/pages/wsl-integrations-page.ts",
    "content": "import { expect } from '@playwright/test';\n\nimport type { Page, Locator } from '@playwright/test';\n\n/**\n * CheckboxLocator handles assertions dealing with a <Checkbox> Vue component.\n */\nclass CheckboxLocator {\n  readonly locator:  Locator;\n  readonly checkbox: Locator;\n  readonly name:     Locator;\n  readonly error:    Locator;\n  constructor(locator: Locator) {\n    this.locator = locator;\n    this.checkbox = locator.locator('input[type=\"checkbox\"]');\n    this.name = locator.locator('.checkbox-label');\n    this.error = locator.locator('.checkbox-outer-container-description');\n  }\n\n  click(...args: Parameters<Locator['click']>) {\n    // The checkbox itself is not visible, so it can't be clicked.\n    return this.locator.click(...args);\n  }\n\n  async assertEnabled(options?:{ timeout?: number }) {\n    const elem = await this.locator.elementHandle();\n\n    expect(elem).toBeTruthy();\n    const result = await elem?.waitForSelector('label:not([class~=\"disabled\"])', { state: 'attached', ...options });\n\n    expect(result).toBeTruthy();\n  }\n\n  async assertDisabled(options?:{ timeout?: number }) {\n    const elem = await this.locator.elementHandle();\n\n    expect(elem).toBeTruthy();\n    const result = await elem?.waitForSelector('label[class~=\"disabled\"]', { state: 'attached', ...options });\n\n    expect(result).toBeTruthy();\n  }\n}\n\nexport class WSLIntegrationsPage {\n  readonly page:         Page;\n  readonly description:  Locator;\n  readonly mainTitle:    Locator;\n  readonly integrations: Locator;\n\n  constructor(page: Page) {\n    this.page = page;\n    this.mainTitle = page.locator('[data-test=\"mainTitle\"]');\n    this.description = page.locator('.description');\n    this.integrations = page.locator('[data-test=\"integration-list\"]');\n  }\n\n  getIntegration(distro: string): CheckboxLocator {\n    const locator = this.integrations.locator(`[data-test=\"item-${ distro }\"] .checkbox-outer-container`);\n\n    return new CheckboxLocator(locator);\n  }\n}\n"
  },
  {
    "path": "e2e/preferences.e2e.spec.ts",
    "content": "import os from 'os';\n\nimport { test, expect, _electron } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { PreferencesPage } from './pages/preferences';\nimport { createDefaultSettings, startRancherDesktop, teardown, tool } from './utils/TestUtils';\n\nimport { reopenLogs } from '@pkg/utils/logging';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\nlet page: Page;\n\n/**\n * Using test.describe.serial make the test execute step by step, as described on each `test()` order\n * Playwright executes test in parallel by default and it will not work for our app backend loading process.\n * */\ntest.describe.serial('Main App Test', () => {\n  let electronApp: ElectronApplication;\n  let preferencesWindow: Page;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    createDefaultSettings();\n\n    electronApp = await startRancherDesktop(testInfo);\n\n    page = await electronApp.firstWindow();\n    await new NavPage(page).preferencesButton.click();\n    preferencesWindow = await electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    await teardown(electronApp, testInfo);\n    await tool('rdctl', 'reset', '--factory', '--verbose');\n    reopenLogs();\n  });\n\n  test('should open preferences modal', async() => {\n    expect(preferencesWindow).toBeDefined();\n\n    // Wait for the window to actually load (i.e. transition from\n    // app://index.html/#/preferences to app://index.html/#/Preferences#general)\n    await preferencesWindow.waitForURL(/Preferences#/i);\n  });\n\n  test('should show application page and render general tab', async() => {\n    const { application } = new PreferencesPage(preferencesWindow);\n\n    await expect(application.nav).toHaveClass('preferences-nav-item active');\n\n    if (!os.platform().startsWith('win')) {\n      await expect(application.tabEnvironment).toBeVisible();\n    } else {\n      await expect(application.tabEnvironment).not.toBeVisible();\n    }\n\n    await expect(application.tabGeneral).toHaveText('General');\n    await expect(application.tabBehavior).toBeVisible();\n\n    await expect(application.automaticUpdates).toBeVisible();\n    await expect(application.statistics).toBeVisible();\n    await expect(application.autoStart).not.toBeVisible();\n    await expect(application.pathManagement).not.toBeVisible();\n  });\n\n  test('should render behavior tab', async() => {\n    const { application } = new PreferencesPage(preferencesWindow);\n\n    await application.tabBehavior.click();\n\n    await expect(application.autoStart).toBeVisible();\n    await expect(application.background).toBeVisible();\n    await expect(application.notificationIcon).toBeVisible();\n    await expect(application.administrativeAccess).not.toBeVisible();\n    await expect(application.pathManagement).not.toBeVisible();\n  });\n\n  test('should render environment tab', async() => {\n    test.skip(os.platform() === 'win32', 'Environment tab not available on Windows');\n    const { application } = new PreferencesPage(preferencesWindow);\n\n    await application.tabEnvironment.click();\n\n    await expect(application.administrativeAccess).not.toBeVisible();\n    await expect(application.automaticUpdates).not.toBeVisible();\n    await expect(application.statistics).not.toBeVisible();\n    await expect(application.pathManagement).toBeVisible();\n  });\n\n  test('should navigate to virtual machine and render hardware tab', async() => {\n    test.skip(os.platform() === 'win32', 'Virtual Machine not available on Windows');\n    const { virtualMachine, application } = new PreferencesPage(preferencesWindow);\n\n    await virtualMachine.nav.click();\n\n    await expect(application.nav).toHaveClass('preferences-nav-item');\n    await expect(virtualMachine.nav).toHaveClass('preferences-nav-item active');\n\n    await expect(virtualMachine.tabHardware).toHaveText('Hardware');\n    await expect(virtualMachine.tabVolumes).toBeVisible();\n    await expect(virtualMachine.tabVolumes).toHaveText('Volumes');\n\n    if (os.platform() === 'darwin') {\n      await expect(virtualMachine.tabEmulation).toBeVisible();\n      await expect(virtualMachine.tabEmulation).toHaveText('Emulation');\n    } else {\n      await expect(virtualMachine.tabEmulation).not.toBeVisible();\n    }\n\n    await expect(virtualMachine.memory).toBeVisible();\n    await expect(virtualMachine.cpus).toBeVisible();\n  });\n\n  test('should render volumes tab', async() => {\n    test.skip(os.platform() === 'win32', 'Virtual Machine not available on Windows');\n    const { virtualMachine } = new PreferencesPage(preferencesWindow);\n\n    await virtualMachine.tabVolumes.click();\n\n    await expect(virtualMachine.mountType).toBeVisible();\n    await expect(virtualMachine.reverseSshFs).toBeVisible();\n    await expect(virtualMachine.ninep).toBeVisible();\n    await expect(virtualMachine.virtiofs).toBeVisible();\n\n    if (os.platform() === 'darwin') {\n      if (parseInt(os.release()) < 22) {\n        await expect(virtualMachine.virtiofs).toBeDisabled();\n      } else {\n        await expect(virtualMachine.virtiofs).not.toBeDisabled();\n      }\n    }\n\n    if (os.platform() === 'darwin' && parseInt(os.release()) >= 23) {\n      await expect(virtualMachine.virtiofs).toBeChecked();\n    } else {\n      await expect(virtualMachine.reverseSshFs).toBeChecked();\n    }\n\n    await virtualMachine.ninep.click();\n    await expect(virtualMachine.cacheMode).toBeVisible();\n    await expect(virtualMachine.msizeInKib).toBeVisible();\n    await expect(virtualMachine.protocolVersion).toBeVisible();\n    await expect(virtualMachine.securityModel).toBeVisible();\n  });\n\n  test('should render emulation tab on macOS', async() => {\n    test.skip(os.platform() !== 'darwin', 'Emulation tab only available on macOS');\n\n    const { virtualMachine } = new PreferencesPage(preferencesWindow);\n\n    await virtualMachine.tabEmulation.click();\n    await expect(virtualMachine.vmType).toBeVisible();\n    await expect(virtualMachine.qemu).toBeVisible();\n    await expect(virtualMachine.vz).toBeVisible();\n\n    if (parseInt(os.release()) < 22) {\n      await expect(virtualMachine.vz).toBeDisabled();\n    } else {\n      await expect(virtualMachine.vz).not.toBeDisabled();\n      await virtualMachine.vz.click({ position: { x: 10, y: 10 } });\n      await expect(virtualMachine.useRosetta).toBeVisible();\n\n      if (os.arch() === 'arm64') {\n        await expect(virtualMachine.useRosetta).not.toBeDisabled();\n      } else {\n        await expect(virtualMachine.useRosetta).toBeDisabled();\n      }\n    }\n  });\n\n  test('should navigate to container engine', async() => {\n    const { containerEngine } = new PreferencesPage(preferencesWindow);\n\n    await containerEngine.nav.click();\n\n    await expect(containerEngine.nav).toHaveClass('preferences-nav-item active');\n    await expect(containerEngine.containerEngine).toBeVisible();\n\n    await expect(containerEngine.tabGeneral).toBeVisible();\n    await expect(containerEngine.tabAllowedImages).toBeVisible();\n  });\n\n  test('should render allowed images tab after click on allowed images tab', async() => {\n    const { containerEngine } = new PreferencesPage(preferencesWindow);\n\n    await containerEngine.tabAllowedImages.click();\n\n    await expect(containerEngine.allowedImages).toBeVisible();\n    await expect(containerEngine.containerEngine).not.toBeVisible();\n  });\n\n  test('should navigate to kubernetes', async() => {\n    const { kubernetes, containerEngine } = new PreferencesPage(preferencesWindow);\n\n    await kubernetes.nav.click();\n\n    await expect(containerEngine.nav).toHaveClass('preferences-nav-item');\n    await expect(kubernetes.nav).toHaveClass('preferences-nav-item active');\n    await expect(kubernetes.kubernetesToggle).toBeVisible();\n    await expect(kubernetes.kubernetesVersion).toBeVisible();\n    await expect(kubernetes.kubernetesPort).toBeVisible();\n    await expect(kubernetes.kubernetesOptions).toBeVisible();\n  });\n\n  test('should navigate to WSL and render integrations tab', async() => {\n    test.skip(os.platform() !== 'win32', 'WSL nav item not available on macOS & Linux');\n    const { wsl } = new PreferencesPage(preferencesWindow);\n\n    await wsl.nav.click();\n\n    await expect(wsl.nav).toHaveClass('preferences-nav-item active');\n\n    await wsl.tabIntegrations.click();\n    await expect(wsl.wslIntegrations).toBeVisible();\n  });\n\n  test('should not render WSL nav item on macOS and Linux', async() => {\n    test.skip(os.platform() === 'win32', 'WSL nav item is only available on Windows');\n    const { wsl } = new PreferencesPage(preferencesWindow);\n\n    await expect(wsl.nav).not.toBeVisible();\n  });\n\n  test.describe.serial('Preferences State', () => {\n    test.beforeAll(async() => {\n      const { application } = new PreferencesPage(preferencesWindow);\n\n      // Start this collection of tests on the environment tab\n      await application.nav.click();\n      if (os.platform() === 'win32') {\n        await application.tabGeneral.click();\n      } else {\n        await application.tabEnvironment.click();\n      }\n\n      // This collection of tests is about making sure that we persist state\n      // in the preferences window, so we close the preferences window before\n      // beginning this test collection.\n      if (preferencesWindow) {\n        await preferencesWindow.close();\n      }\n    });\n\n    test.beforeEach(async() => {\n      await new NavPage(page).preferencesButton.click();\n      preferencesWindow = await electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n    });\n\n    test.afterEach(async() => {\n      if (preferencesWindow) {\n        await preferencesWindow.close();\n      }\n    });\n\n    test('should render environment tab after close and reopen preferences modal', async() => {\n      test.skip(os.platform() === 'win32', 'Environment tab not available on Windows');\n\n      expect(preferencesWindow).toBeDefined();\n\n      const { application, containerEngine } = new PreferencesPage(preferencesWindow);\n\n      await application.tabEnvironment.click();\n\n      await expect(application.nav).toHaveClass('preferences-nav-item active');\n      await expect(application.tabBehavior).toBeVisible();\n      await expect(application.tabEnvironment).toBeVisible();\n      await expect(application.administrativeAccess).not.toBeVisible();\n      await expect(application.automaticUpdates).not.toBeVisible();\n      await expect(application.statistics).not.toBeVisible();\n      await expect(application.pathManagement).toBeVisible();\n\n      // Move onto the container engine before starting the next test\n      await containerEngine.nav.click();\n      await containerEngine.tabGeneral.click();\n    });\n\n    test('should render container engine page after close and reopen preferences modal', async() => {\n      expect(preferencesWindow).toBeDefined();\n      // Wait for the window to actually load (i.e. transition from\n      // app://index.html/#/preferences to app://index.html/#/Preferences#general)\n      await preferencesWindow.waitForURL(/Preferences#/i);\n      const { containerEngine } = new PreferencesPage(preferencesWindow);\n\n      if (os.platform() === 'win32') {\n        // We didn't run the previous test which landed on `tabGeneral`, so run that here.\n        await containerEngine.nav.click();\n        await containerEngine.tabGeneral.click();\n      }\n      await expect(containerEngine.nav).toHaveClass('preferences-nav-item active');\n      await expect(containerEngine.containerEngine).toBeVisible();\n\n      await expect(containerEngine.tabGeneral).toBeVisible();\n      await expect(containerEngine.tabAllowedImages).toBeVisible();\n\n      // Move onto the allowed images tab before the next test\n      await containerEngine.tabAllowedImages.click();\n    });\n\n    test('should render allowed image tab in container engine page after close and reopen preferences modal', async() => {\n      expect(preferencesWindow).toBeDefined();\n      // Wait for the window to actually load (i.e. transition from\n      // app://index.html/#/preferences to app://index.html/#/Preferences#general)\n      await preferencesWindow.waitForURL(/Preferences#/i);\n      const { containerEngine } = new PreferencesPage(preferencesWindow);\n\n      await expect(containerEngine.nav).toHaveClass('preferences-nav-item active');\n      await expect(containerEngine.allowedImages).toBeVisible();\n      await expect(containerEngine.containerEngine).not.toBeVisible();\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/quit-on-close.e2e.spec.ts",
    "content": "import { test, expect, ElectronApplication } from '@playwright/test';\n\nimport { createDefaultSettings, reportAsset, startRancherDesktop, teardown } from './utils/TestUtils';\n\n/**\n * Using test.describe.serial make the test execute step by step, as described on each `test()` order\n * Playwright executes test in parallel by default and it will not work for our app backend loading process.\n * */\ntest.describe.serial('quitOnClose setting', () => {\n  test('should quit when quitOnClose is true and window is closed', async({ colorScheme }, testInfo) => {\n    createDefaultSettings({ application: { window: { quitOnClose: true } } });\n    const electronApp = await startRancherDesktop(testInfo, { logVariant: 'quitOnCloseTrue' });\n\n    await electronApp.firstWindow();\n\n    await expect(closeWindowsAndCheckQuit(electronApp)).resolves.toBe(true);\n    // Don't call teardown[App] here, because the app already exited.\n    await electronApp.context().tracing.stop({ path: reportAsset(testInfo, 'trace') });\n  });\n\n  test('should not quit when quitOnClose is false and window is closed', async({ colorScheme }, testInfo) => {\n    createDefaultSettings({ application: { window: { quitOnClose: false } } });\n    const electronApp = await startRancherDesktop(testInfo, { logVariant: 'quitOnCloseFalse' });\n\n    try {\n      await electronApp.firstWindow();\n      await expect(closeWindowsAndCheckQuit(electronApp)).resolves.toBe(false);\n    } finally {\n      try {\n        await teardown(electronApp, testInfo);\n      } catch { }\n    }\n  });\n});\n\n/**\n * Closes all of the windows in a running app. Returns a promise that\n * resolves to true when the app has quit within a certain period of time,\n * or that resolves to false when the app does not quit within that period\n * of time.\n * */\nfunction closeWindowsAndCheckQuit(electronApp: ElectronApplication): Promise<boolean> {\n  return electronApp.evaluate(async({ app, BrowserWindow }) => {\n    const quitReady = new Promise<boolean>((resolve) => {\n      app.on('will-quit', () => resolve(true));\n      app.on('window-all-closed', () => {\n        setTimeout(() => resolve(false), 3_000);\n      });\n    });\n\n    await Promise.all(BrowserWindow.getAllWindows().map((window) => {\n      return new Promise<void>((resolve) => {\n        window.on('closed', resolve);\n        window.close();\n      });\n    }));\n\n    return await quitReady;\n  });\n}\n"
  },
  {
    "path": "e2e/rdctl.e2e.spec.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This file includes end-to-end testing for the HTTP control interface\n */\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { expect, test } from '@playwright/test';\nimport _ from 'lodash';\nimport yaml from 'yaml';\n\nimport { NavPage } from './pages/nav-page';\nimport {\n  getAlternateSetting, kubectl, retry, startSlowerDesktop, teardown,\n} from './utils/TestUtils';\n\nimport {\n  CacheMode,\n  ContainerEngine,\n  CURRENT_SETTINGS_VERSION,\n  defaultSettings,\n  MountType,\n  ProtocolVersion,\n  SecurityModel,\n  Settings,\n  VMType,\n} from '@pkg/config/settings';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\ntest.describe('Command server', () => {\n  let electronApp: ElectronApplication;\n  let serverState: ServerState;\n  let page: Page;\n  const ENOENTMessage = os.platform() === 'win32' ? 'The system cannot find the file specified' : 'no such file or directory';\n  const appPath = path.dirname(import.meta.dirname);\n\n  async function doRequest(path: string, body = '', method = 'GET') {\n    const url = `http://127.0.0.1:${ serverState.port }/${ path.replace(/^\\/*/, '') }`;\n    const auth = `${ serverState.user }:${ serverState.password }`;\n    const init: RequestInit = {\n      method,\n      headers: {\n        Authorization: `Basic ${ Buffer.from(auth)\n          .toString('base64') }`,\n      },\n    };\n\n    if (body) {\n      init.body = body;\n    }\n\n    return await fetch(url, init);\n  }\n\n  function rdctlPath() {\n    return path.join(appPath, 'resources', os.platform(), 'bin', os.platform() === 'win32' ? 'rdctl.exe' : 'rdctl');\n  }\n\n  async function rdctl(commandArgs: string[]): Promise< { stdout: string, stderr: string, error?: any }> {\n    try {\n      return await spawnFile(rdctlPath(), commandArgs, { stdio: 'pipe' });\n    } catch (err: any) {\n      return {\n        stdout: err?.stdout ?? '', stderr: err?.stderr ?? '', error: err,\n      };\n    }\n  }\n\n  async function rdctlWithStdin(inputFile: string, commandArgs: string[]): Promise< { stdout: string, stderr: string, error?: any }> {\n    let stream: fs.ReadStream | null = null;\n\n    try {\n      const fd = await fs.promises.open(inputFile, 'r');\n\n      stream = fd.createReadStream();\n\n      return await spawnFile(rdctlPath(), commandArgs, { stdio: [stream, 'pipe', 'pipe'] });\n    } catch (err: any) {\n      return {\n        stdout: err?.stdout ?? '', stderr: err?.stderr ?? '', error: err,\n      };\n    } finally {\n      stream?.close();\n    }\n  }\n\n  function verifySettingsKeys(settings: Record<string, any>) {\n    expect(new Set(Object.keys(defaultSettings)))\n      .toEqual(new Set(Object.keys(settings)));\n  }\n\n  test.describe.configure({ mode: 'serial' });\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [electronApp, page] = await startSlowerDesktop(testInfo, { kubernetes: { enabled: true } });\n  });\n\n  test.afterAll(({ colorScheme }, testInfo) => teardown(electronApp, testInfo));\n\n  test('should load Kubernetes API', async() => {\n    const navPage = new NavPage(page);\n\n    await navPage.progressBecomesReady();\n\n    expect(await retry(() => kubectl('cluster-info'))).toContain('is running at');\n  });\n\n  test('should emit connection information', async() => {\n    const dataPath = path.join(paths.appHome, 'rd-engine.json');\n    const dataRaw = await fs.promises.readFile(dataPath, 'utf-8');\n\n    serverState = JSON.parse(dataRaw);\n    expect(serverState).toEqual(expect.objectContaining({\n      user:     expect.any(String),\n      password: expect.any(String),\n      port:     expect.any(Number),\n      pid:      expect.any(Number),\n    }));\n  });\n\n  test('should require authentication, settings request', async() => {\n    const url = `http://127.0.0.1:${ serverState.port }/v1/settings`;\n    const resp = await fetch(url);\n\n    expect(resp).toEqual(expect.objectContaining({\n      ok:     false,\n      status: 401,\n    }));\n  });\n\n  test('should emit CORS headers, settings request', async() => {\n    const resp = await doRequest('/v1/settings', '', 'OPTIONS');\n\n    expect({\n      ...resp,\n      ok:      !!resp.ok,\n      headers: Object.fromEntries(resp.headers.entries()),\n    }).toEqual(expect.objectContaining({\n      ok:      true,\n      headers: expect.objectContaining({\n        'access-control-allow-headers': 'Authorization',\n        'access-control-allow-methods': 'GET, PUT, DELETE',\n        'access-control-allow-origin':  '*',\n      }),\n    }));\n  });\n\n  test('should be able to get settings', async() => {\n    const resp = await doRequest('/v1/settings');\n\n    expect({\n      ...resp,\n      ok:      !!resp.ok,\n      headers: Object.fromEntries(resp.headers.entries()),\n    }).toEqual(expect.objectContaining({\n      ok:      true,\n      headers: expect.objectContaining({\n        'access-control-allow-headers': 'Authorization',\n        'access-control-allow-methods': 'GET, PUT, DELETE',\n        'access-control-allow-origin':  '*',\n      }),\n    }));\n    expect(await resp.json()).toHaveProperty('kubernetes');\n  });\n\n  test('setting existing settings should be a no-op', async() => {\n    let resp = await doRequest('/v1/settings');\n    const rawSettings = await resp.text();\n\n    resp = await doRequest('/v1/settings', rawSettings, 'PUT');\n    expect({\n      ok:     resp.ok,\n      status: resp.status,\n      body:   await resp.text(),\n    }).toEqual({\n      ok:     true,\n      status: 202,\n      body:   expect.stringContaining('no changes necessary'),\n    });\n  });\n\n  test('should not update values when the /settings payload has errors', async() => {\n    let resp = await doRequest('/v1/settings');\n    const settings = await resp.json();\n    const desiredEnabled = !settings.kubernetes.enabled;\n    const desiredEngine = 'flip';\n    const desiredVersion = /1.29.4/.test(settings.kubernetes.version) ? 'v1.19.1' : 'v1.29.4';\n    const requestedSettings = _.merge({}, settings, {\n      version:         CURRENT_SETTINGS_VERSION,\n      containerEngine: {\n        name:          { desiredEngine },\n        allowedImages: { enabled: !settings.containerEngine.allowedImages.enabled },\n      },\n      kubernetes: {\n        enabled: desiredEnabled,\n        version: desiredVersion,\n      },\n    });\n    const resp2 = await doRequest('/v1/settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(settings);\n  });\n\n  test('should return multiple error messages, settings request', async() => {\n    const newSettings: Record<string, any> = {\n      version:     CURRENT_SETTINGS_VERSION,\n      application: {\n        stoinks:   'yikes!', // should be ignored\n        telemetry: { enabled: { oops: 15 } },\n      },\n      containerEngine: { name: { status: 'should be a scalar' } },\n      virtualMachine:  { memoryInGB: 'carl' },\n      WSL:             { integrations: \"ceci n'est pas un objet\" },\n      portForwarding:  'bob',\n    };\n    const resp2 = await doRequest('/v1/settings', JSON.stringify(newSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n    const body = await resp2.text();\n    const expectedWSL = {\n      win32: `Proposed field \"WSL.integrations\" should be an object, got <ceci n'est pas un objet>.`,\n      lima:  `Changing field \"WSL.integrations\" via the API isn't supported.`,\n    }[os.platform() === 'win32' ? 'win32' : 'lima'];\n    const expectedMemory = {\n      win32: `Changing field \"virtualMachine.memoryInGB\" via the API isn't supported.`,\n      lima:  `Invalid value for \"virtualMachine.memoryInGB\": <\"carl\">`,\n    }[os.platform() === 'win32' ? 'win32' : 'lima'];\n    const expectedLines = [\n      'errors in attempt to update settings:',\n      expectedWSL,\n      expectedMemory,\n      `Invalid value for \"containerEngine.name\": <{\\\"status\\\":\\\"should be a scalar\\\"}>; must be one of [\"containerd\",\"moby\",\"docker\"]`,\n      'Setting \"portForwarding\" should wrap an inner object, but got <bob>.',\n      'Invalid value for \"application.telemetry.enabled\": <{\"oops\":15}>',\n    ];\n\n    expect(body.split(/\\r?\\n/g).sort()).toEqual(expect.arrayContaining(expectedLines.sort()));\n  });\n\n  test('should reject invalid JSON, settings request', async() => {\n    const resp = await doRequest('/v1/settings', '{\"missing\": \"close-brace\"', 'PUT');\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(400);\n    await expect(resp.text()).resolves.toContain('error processing JSON request block');\n  });\n\n  test('should reject empty payload, settings request', async() => {\n    const resp = await doRequest('/v1/settings', '', 'PUT');\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(400);\n    await expect(resp.text()).resolves.toContain('no settings specified in the request');\n  });\n\n  test('version-only path of a nonexistent version should 404', async() => {\n    const resp = await doRequest('/v99-bottles-of-beer-on-the-wall');\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(404);\n    await expect(resp.text()).resolves.toContain('Unknown command: GET /v99-bottles-of-beer-on-the-wall');\n  });\n\n  test('should not restart on unrelated changes', async() => {\n    let resp = await doRequest('/v1/settings');\n\n    expect(resp.ok).toBeTruthy();\n    const telemetry = (await resp.json() as Settings).application.telemetry.enabled;\n\n    resp = await doRequest('/v1/settings', JSON.stringify({ version: CURRENT_SETTINGS_VERSION, application: { telemetry: { enabled: !telemetry } } }), 'PUT');\n    expect(resp.ok).toBeTruthy();\n    await expect(resp.text()).resolves.toContain('no restart required');\n  });\n\n  test('should complain about a missing version field', async() => {\n    let resp = await doRequest('/v1/settings');\n\n    expect(resp.ok).toBeTruthy();\n\n    const body: RecursivePartial<Settings> = await resp.json();\n\n    delete body.version;\n    if (body?.application?.telemetry) {\n      body.application.telemetry.enabled = !body.application.telemetry.enabled;\n    }\n    resp = await doRequest('/v1/settings', JSON.stringify(body), 'PUT');\n    expect(resp.ok).toBeFalsy();\n    await expect(resp.text()).resolves.toContain(`updating settings requires specifying an API version, but no version was specified`);\n  });\n\n  test('should complain about an invalid version field', async() => {\n    let resp = await doRequest('/v1/settings');\n\n    expect(resp.ok).toBeTruthy();\n\n    const body: RecursivePartial<Settings> = await resp.json();\n    const badVersion = 'not a number';\n\n    // Override typescript's checking so we can verify that the server rejects the\n    // invalid value for the `version` field.\n    body.version = badVersion as any;\n    if (body?.application?.telemetry) {\n      body.application.telemetry.enabled = !body.application.telemetry.enabled;\n    }\n    resp = await doRequest('/v1/settings', JSON.stringify(body), 'PUT');\n    expect(resp.ok).toBeFalsy();\n    await expect(resp.text()).resolves.toContain(`updating settings requires specifying an API version, but \"${ badVersion }\" is not a proper config version`);\n  });\n\n  test('should require authentication, transient settings request', async() => {\n    const url = `http://127.0.0.1:${ serverState.port }/v1/transient_settings`;\n    const resp = await fetch(url);\n\n    expect(resp).toEqual(expect.objectContaining({\n      ok:     false,\n      status: 401,\n    }));\n  });\n\n  test('should emit CORS headers, transient settings request', async() => {\n    const resp = await doRequest('/v1/transient_settings', '', 'OPTIONS');\n\n    expect({\n      ...resp,\n      ok:      !!resp.ok,\n      headers: Object.fromEntries(resp.headers.entries()),\n    }).toEqual(expect.objectContaining({\n      ok:      true,\n      headers: expect.objectContaining({\n        'access-control-allow-headers': 'Authorization',\n        'access-control-allow-methods': 'GET, PUT, DELETE',\n        'access-control-allow-origin':  '*',\n      }),\n    }));\n  });\n\n  test('should be able to get transient settings', async() => {\n    const resp = await doRequest('/v1/transient_settings');\n\n    expect({\n      ...resp,\n      ok:      !!resp.ok,\n      headers: Object.fromEntries(resp.headers.entries()),\n    }).toEqual(expect.objectContaining({\n      ok:      true,\n      headers: expect.objectContaining({\n        'access-control-allow-headers': 'Authorization',\n        'access-control-allow-methods': 'GET, PUT, DELETE',\n        'access-control-allow-origin':  '*',\n      }),\n    }));\n    expect(await resp.json()).toHaveProperty('noModalDialogs');\n  });\n\n  test('setting existing transient settings should be a no-op', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const rawSettings = await resp.text();\n\n    resp = await doRequest('/v1/transient_settings', rawSettings, 'PUT');\n    expect({\n      ok:     resp.ok,\n      status: resp.status,\n      body:   await resp.text(),\n    }).toEqual({\n      ok:     true,\n      status: 202,\n      body:   expect.stringContaining('No changes necessary'),\n    });\n  });\n\n  test('should not update values when the /transient_settings navItem payload is invalid', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge({}, transientSettings, { preferences: { navItem: { current: 'foo', bar: 'bar' } } });\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should not update values when the /transient_settings payload has invalid current navItem name', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge({}, transientSettings, { preferences: { navItem: { current: 'foo' } } });\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should not update values when the /transient_settings payload has invalid sub-tabs for Application preference page', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge({}, transientSettings, { preferences: { navItem: { current: 'Application', currentTabs: { Application: 'foo' } } } });\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should not update values when the /transient_settings payload has invalid sub-tabs for Container Engine preference page', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge(\n      {},\n      transientSettings,\n      { preferences: { navItem: { currentTabs: { 'Container Engine': 'behavior' } } } },\n    );\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should not update values when the /transient_settings payload contains sub-tabs for a page not supporting sub-tabs: WSL / Virtual Machine', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge(\n      {},\n      transientSettings,\n      { preferences: { navItem: { currentTabs: { [process.platform === 'win32' ? 'WSL' : 'Virtual Machine']: 'behavior' } } } },\n    );\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should not update values when the /transient_settings payload contains sub-tabs for a page not supporting sub-tabs: Kubernetes', async() => {\n    let resp = await doRequest('/v1/transient_settings');\n    const transientSettings = await resp.json();\n\n    const requestedSettings = _.merge(\n      {},\n      transientSettings,\n      { preferences: { navItem: { currentTabs: { Kubernetes: 'behavior' } } } },\n    );\n    const resp2 = await doRequest('/v1/transient_settings', JSON.stringify(requestedSettings), 'PUT');\n\n    expect(resp2.ok).toBeFalsy();\n    expect(resp2.status).toEqual(400);\n\n    // Now verify that the specified values did not get updated.\n    resp = await doRequest('/v1/transient_settings');\n    const refreshedSettings = await resp.json();\n\n    expect(refreshedSettings).toEqual(transientSettings);\n  });\n\n  test('should reject invalid JSON, transient settings request', async() => {\n    const resp = await doRequest('/v1/transient_settings', '{\"missing\": \"close-brace\"', 'PUT');\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(400);\n    await expect(resp.text()).resolves.toContain('error processing JSON request block');\n  });\n\n  test('should reject empty payload, transient settings request', async() => {\n    const resp = await doRequest('/v1/transient_settings', '', 'PUT');\n\n    expect(resp.ok).toBeFalsy();\n    expect(resp.status).toEqual(400);\n    await expect(resp.text()).resolves.toContain('no settings specified in the request');\n  });\n\n  test.describe('v0 API', () => {\n    const endpoints = {\n      GET:  ['diagnostic_categories', 'diagnostic_checks', 'diagnostic_ids', 'settings', 'transient_settings'],\n      PUT:  ['factory_reset', 'propose_settings', 'settings', 'shutdown', 'transient_settings'],\n      POST: ['diagnostic_checks'],\n    };\n\n    test('should no longer work', async() => {\n      for (const method in endpoints) {\n        for (const endpoint of endpoints[method as 'GET' | 'PUT']) {\n          const resp = await doRequest(`/v0/${ endpoint }`, '', method);\n\n          expect({\n            ok:     resp.ok,\n            status: resp.status,\n            body:   await resp.text(),\n          }).toEqual({\n            ok:     false,\n            status: 400,\n            body:   `Invalid version \"/v0\" for endpoint \"${ method } /v0/${ endpoint }\" - use \"/v1/${ endpoint }\"`,\n          });\n        }\n      }\n    });\n  });\n\n  test.describe('rdctl', () => {\n    test.describe('config-file and parameters', () => {\n      test.describe(\"when the config-file doesn't exist\", () => {\n        let parameters: string[];\n        const configFilePath = path.join(paths.appHome, 'rd-engine.json');\n        const backupPath = path.join(paths.appHome, 'rd-engine.json.bak');\n\n        test.beforeAll(async() => {\n          const dataRaw = await fs.promises.readFile(configFilePath, 'utf-8');\n\n          serverState = JSON.parse(dataRaw);\n          parameters = [`--password=${ serverState.password }`,\n            `--port=${ serverState.port }`,\n            `--user=${ serverState.user }`,\n          ];\n          await expect(fs.promises.rename(configFilePath, backupPath)).resolves.toBeUndefined();\n        });\n        test.afterAll(async() => {\n          await expect(fs.promises.rename(backupPath, configFilePath)).resolves.toBeUndefined();\n        });\n\n        test('it complains with no parameters,', async() => {\n          const { stdout, stderr, error } = await rdctl(['list-settings']);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  expect.any(Error),\n            stderr: expect.stringContaining(`Error: failed to get connection info: open ${ configFilePath }: ${ ENOENTMessage }`),\n            stdout: '',\n          });\n          expect(stderr).not.toContain('Usage:');\n        });\n\n        test('it works with all parameters,', async() => {\n          const { stdout, stderr, error } = await rdctl(parameters.concat(['list-settings']));\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  undefined,\n            stderr: '',\n            stdout: expect.stringContaining('\"kubernetes\":'),\n          });\n          verifySettingsKeys(JSON.parse(stdout));\n        });\n        test(\"it complains when some parameters aren't specified\", async() => {\n          for (let idx = 0; idx < parameters.length; idx += 1) {\n            const partialParameters = parameters.slice(0, idx).concat(parameters.slice(idx + 1));\n            const { stdout, stderr, error } = await rdctl(partialParameters.concat(['list-settings']));\n\n            expect({\n              stdout, stderr, error,\n            }).toEqual({\n              error:  expect.any(Error),\n              stderr: expect.stringContaining(`Error: failed to get connection info: open ${ configFilePath }: ${ ENOENTMessage }`),\n              stdout: '',\n            });\n            expect(stderr).not.toContain('Usage:');\n          }\n        });\n        test.describe('when a nonexistent config file is specified', () => {\n          test('it fails even when all parameters are specified', async() => {\n            const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-fake-docker'));\n\n            try {\n              const configFile = path.join(tmpDir, 'config.json');\n              // Do not actually create configFile\n              const { stdout, stderr, error } = await rdctl(parameters.concat(['list-settings', '--config-path', configFile]));\n\n              expect({\n                stdout, stderr, error,\n              }).toEqual({\n                error:  expect.any(Error),\n                stderr: expect.stringContaining(`Error: failed to get connection info: open ${ configFile }: ${ ENOENTMessage }`),\n                stdout: '',\n              });\n              expect(stderr).not.toContain('Usage:');\n            } finally {\n              await fs.promises.rm(tmpDir, { recursive: true });\n            }\n          });\n        });\n      });\n    });\n    test('should show settings and nil-update settings', async() => {\n      const { stdout, stderr, error } = await rdctl(['list-settings']);\n\n      expect({\n        stdout, stderr, error,\n      }).toEqual({\n        error:  undefined,\n        stderr: '',\n        stdout: expect.stringContaining('\"kubernetes\":'),\n      });\n      const settings = JSON.parse(stdout);\n\n      verifySettingsKeys(settings);\n\n      const args = ['set',\n        '--container-engine', settings.containerEngine.name,\n        `--kubernetes-enabled=${ !!settings.kubernetes.enabled }`,\n        '--kubernetes-version', settings.kubernetes.version];\n      const result = await rdctl(args);\n\n      expect(result).toMatchObject({\n        stderr: '',\n        stdout: expect.stringContaining('Status: no changes necessary.'),\n      });\n    });\n\n    test.describe('set', () => {\n      const unsupportedPrefsByPlatform: Partial<Record<NodeJS.Platform, [string, any][]>> = {\n        win32: [\n          ['application.admin-access', true],\n          ['application.path-management-strategy', 'rcfiles'],\n          ['experimental.virtual-machine.mount.9p.cache-mode', CacheMode.MMAP],\n          ['experimental.virtual-machine.mount.9p.msize-in-kib', 128],\n          ['experimental.virtual-machine.mount.9p.protocol-version', ProtocolVersion.NINEP2000_L],\n          ['experimental.virtual-machine.mount.9p.security-model', SecurityModel.NONE],\n          ['virtual-machine.memory-in-gb', 10],\n          ['virtual-machine.mount.type', MountType.NINEP],\n          ['virtual-machine.number-cpus', 10],\n          ['virtual-machine.type', VMType.VZ],\n          ['virtual-machine.use-rosetta', true],\n        ],\n        darwin: [\n          ['kubernetes.ingress.localhost-only', true],\n        ],\n        linux: [\n          ['experimental.virtual-machine.proxy.enabled', true],\n          ['virtual-machine.type', VMType.VZ],\n          ['virtual-machine.use-rosetta', true],\n        ],\n      };\n      const unsupportedOptions = unsupportedPrefsByPlatform[os.platform()] ?? [];\n      const commonOptions = [\n        'container-engine.name',\n        'container-engine.allowed-images.enabled',\n        'kubernetes.version',\n        'kubernetes.port',\n        'kubernetes.options.traefik',\n        'port-forwarding.include-kubernetes-services',\n      ];\n\n      test('complains when no args are given', async() => {\n        const { stdout, stderr, error } = await rdctl(['set']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining('Error: set command: no settings to change were given'),\n          stdout: '',\n        });\n        expect(stderr).toContain('Usage:');\n        const options = stderr.split(/\\n/)\n          .filter(line => /^\\s+--/.test(line))\n          .map(line => (/\\s+--([-.\\w]+)\\s/.exec(line) || [])[1])\n          .filter(line => line);\n\n        // This part is a bit subtle\n        // Require that the received options contain at least all the common options\n        expect(options).toEqual(expect.arrayContaining(commonOptions));\n        // We can't use `not.toEqual.arrayContaining` for the unsupported options because if the received\n        // list contains some but not all of the unsupported options the not-test will still succeed\n        for (const option of unsupportedOptions) {\n          expect(options).not.toContain(option[0]);\n        }\n      });\n\n      test('complains when option value missing', async() => {\n        const { stdout, stderr, error } = await rdctl(['set', '--container-engine']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining('Error: flag needs an argument: --container-engine'),\n          stdout: '',\n        });\n        expect(stderr).toContain('Usage:');\n      });\n\n      test('complains when non-boolean option value specified', async() => {\n        const { stdout, stderr, error } = await rdctl(['set', '--kubernetes-enabled=gorb']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining('Error: invalid argument \"gorb\" for \"--kubernetes-enabled\" flag: strconv.ParseBool: parsing \"gorb\": invalid syntax'),\n          stdout: '',\n        });\n        expect(stderr).toContain('Usage:');\n      });\n\n      test('complains when invalid engine specified', async() => {\n        const myEngine = 'giblets';\n        const { stdout, stderr, error } = await rdctl(['set', `--container-engine=${ myEngine }`]);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining(`Error: invalid value for option --container-engine: \"${ myEngine }\"; must be 'containerd', 'docker', or 'moby'`),\n          stdout: '',\n        });\n        expect(stderr).not.toContain('Error: errors in attempt to update settings:');\n        expect(stderr).not.toContain('Usage:');\n      });\n\n      test('complains when server rejects a proposed version', async() => {\n        const { stdout, stderr, error } = await rdctl(['set', '--kubernetes-version=karl']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringMatching(/Error: errors in attempt to update settings:\\s+Kubernetes version \"karl\" not found./),\n          stdout: '',\n        });\n        expect(stderr).not.toContain('Usage:');\n      });\n\n      test.describe('settings v5 migration', () => {\n        /**\n         * Note issue https://github.com/rancher-sandbox/rancher-desktop/issues/3829\n         * calls for removing unrecognized fields in the existing settings.json file\n         * Currently we're ignoring unrecognized fields in the PUT payload -- to complain about\n         * them calls for another issue.\n         */\n        test('rejects old settings', async() => {\n          const oldSettings: Settings = JSON.parse((await rdctl(['list-settings'])).stdout);\n          const body: any = {\n            // type 'any' because as far as the current configuration code is concerned,\n            // it's an object with random fields and values\n            version:    CURRENT_SETTINGS_VERSION,\n            kubernetes: {\n              memoryInGB:      oldSettings.virtualMachine.memoryInGB + 1,\n              numberCPUs:      oldSettings.virtualMachine.numberCPUs + 1,\n              containerEngine: getAlternateSetting(oldSettings, 'containerEngine.name', ContainerEngine.CONTAINERD, ContainerEngine.MOBY),\n              suppressSudo:    oldSettings.application.adminAccess,\n            },\n            telemetry: !oldSettings.application.telemetry.enabled,\n            updater:   !oldSettings.application.updater.enabled,\n            debug:     !oldSettings.application.debug,\n          };\n          const addPathManagementStrategy = (oldSettings: Settings, body: any) => {\n            body.pathManagementStrategy = getAlternateSetting(oldSettings,\n              'application.pathManagementStrategy',\n              PathManagementStrategy.Manual,\n              PathManagementStrategy.RcFiles);\n          };\n\n          switch (os.platform()) {\n          case 'darwin':\n            body.kubernetes.experimental ??= {};\n            addPathManagementStrategy(oldSettings, body);\n            break;\n          case 'linux':\n            addPathManagementStrategy(oldSettings, body);\n            break;\n          case 'win32':\n            body.kubernetes.WSLIntegrations ??= {};\n            body.kubernetes.WSLIntegrations.bosco = true;\n          }\n          const { stdout, stderr, error } = await rdctl(['api', '/v1/settings', '-X', 'PUT', '-b', JSON.stringify(body)]);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            stdout: expect.stringContaining('no changes necessary'),\n            stderr: '',\n            error:  undefined,\n          });\n          const newSettings: Settings = JSON.parse((await rdctl(['list-settings'])).stdout);\n\n          expect(newSettings).toEqual(oldSettings);\n        });\n\n        test('accepts new settings', async() => {\n          const oldSettings: Settings = JSON.parse((await rdctl(['list-settings'])).stdout);\n          const body: RecursivePartial<Settings> = {\n            ...(os.platform() === 'win32'\n              ? {}\n              : {\n                virtualMachine: {\n                  memoryInGB: oldSettings.virtualMachine.memoryInGB + 1,\n                  numberCPUs: oldSettings.virtualMachine.numberCPUs + 1,\n                },\n              }),\n            version:     CURRENT_SETTINGS_VERSION,\n            application: {\n              // XXX: Can't change adminAccess until we can process the sudo-request dialog (and decline it)\n              // adminAccess: !oldSettings.application.adminAccess,\n              telemetry: { enabled: !oldSettings.application.telemetry.enabled },\n              updater:   { enabled: !oldSettings.application.updater.enabled },\n              debug:     !oldSettings.application.debug,\n            },\n            // This field is to force a restart\n            kubernetes: { port: oldSettings.kubernetes.port + 1 },\n          };\n\n          if (process.platform !== 'win32' && body.application !== undefined) {\n            body.application.pathManagementStrategy = getAlternateSetting(oldSettings,\n              'application.pathManagementStrategy',\n              PathManagementStrategy.Manual,\n              PathManagementStrategy.RcFiles);\n          }\n          const { stdout, stderr, error } = await rdctl(['api', '/v1/settings', '-X', 'PUT', '-b', JSON.stringify(body)]);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            stdout: expect.stringContaining('reconfiguring Rancher Desktop to apply changes'),\n            stderr: '',\n            error:  undefined,\n          });\n          const newSettings: Settings = JSON.parse((await rdctl(['list-settings'])).stdout);\n\n          expect(newSettings).toEqual(_.merge(oldSettings, body));\n\n          // And now reinstate the old prefs so other tests that count on them will pass.\n          const result = await rdctl(['api', '/v1/settings', '-X', 'PUT', '-b', JSON.stringify(oldSettings)]);\n\n          expect(result.stderr).toEqual('');\n          const navPage = new NavPage(page);\n\n          await navPage.progressBecomesReady();\n        });\n      });\n\n      test('complains about options not intended for current platform', async() => {\n        // playwright doesn't support test.each\n        // See https://github.com/microsoft/playwright/issues/7036 for the discussion\n\n        for (const [option, newValue] of unsupportedOptions) {\n          await expect(rdctl(['set', `--${ option }=${ newValue }`])).resolves\n            .toMatchObject({ stderr: expect.stringContaining(`Error: option --${ option } is not available on`) });\n        }\n      });\n    });\n\n    test.describe('all server commands', () => {\n      test.describe('complains about unrecognized/extra arguments', () => {\n        const badArgs = ['string', 'brucebean'];\n\n        for (const cmd of ['set', 'list-settings', 'shutdown']) {\n          const args = [cmd, ...badArgs];\n\n          test(args.join(' '), async() => {\n            const { stdout, stderr, error } = await rdctl(args);\n\n            expect({\n              stdout, stderr, error,\n            }).toEqual({\n              error:  expect.any(Error),\n              stderr: expect.stringContaining(`Error: unknown command \"string\" for \"rdctl ${ cmd }\"`),\n              stdout: '',\n            });\n            expect(stderr).toContain('Usage:');\n          });\n        }\n      });\n\n      test.describe('complains when unrecognized options are given', () => {\n        for (const cmd of ['set', 'list-settings', 'shutdown']) {\n          const args = [cmd, '--Awop-bop-a-loo-mop', 'zips', '--alop-bom-bom=cows'];\n\n          test(args.join(' '), async() => {\n            const { stdout, stderr, error } = await rdctl(args);\n\n            expect({\n              stdout, stderr, error,\n            }).toEqual({\n              error:  expect.any(Error),\n              stderr: expect.stringContaining(`Error: unknown flag: ${ args[1] }`),\n              stdout: '',\n            });\n            expect(stderr).toContain('Usage:');\n          });\n        }\n      });\n    });\n\n    test.describe('api', () => {\n      test.describe('all subcommands', () => {\n        test('complains when no args are given', async() => {\n          const { stdout, stderr, error } = await rdctl(['api']);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  expect.any(Error),\n            stderr: expect.stringContaining('Error: api command: no endpoint specified'),\n            stdout: '',\n          });\n          expect(stderr).toContain('Usage:');\n        });\n\n        test('empty string endpoint should give an error message', async() => {\n          const { stdout, stderr, error } = await rdctl(['api', '']);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  expect.any(Error),\n            stderr: expect.stringContaining('Error: api command: no endpoint specified'),\n            stdout: '',\n          });\n          expect(stderr).toContain('Usage:');\n        });\n\n        test('complains when more than one endpoint is given', async() => {\n          const endpoints = ['settings', '/v1/settings'];\n          const { stdout, stderr, error } = await rdctl(['api', ...endpoints]);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  expect.any(Error),\n            stderr: expect.stringContaining(`Error: api command: too many endpoints specified ([${ endpoints.join(' ') }]); exactly one must be specified`),\n            stdout: '',\n          });\n          expect(stderr).toContain('Usage:');\n        });\n      });\n\n      test.describe('settings', () => {\n        test.describe('options:', () => {\n          test.describe('GET', () => {\n            for (const endpoint of ['settings', '/v1/settings']) {\n              for (const methodSpecs of [[], ['-X', 'GET'], ['--method', 'GET']]) {\n                const args = ['api', endpoint, ...methodSpecs];\n\n                test(args.join(' '), async() => {\n                  const { stdout, stderr, error } = await rdctl(args);\n\n                  expect({\n                    stdout, stderr, error,\n                  }).toEqual({\n                    error:  undefined,\n                    stderr: '',\n                    stdout: expect.stringMatching(/{.+}/s),\n                  });\n                  verifySettingsKeys(JSON.parse(stdout));\n                });\n              }\n            }\n          });\n\n          test.describe('PUT', () => {\n            test.describe('from stdin', () => {\n              const settingsFile = path.join(paths.config, 'settings.json');\n\n              for (const endpoint of ['settings', '/v1/settings']) {\n                for (const methodSpec of ['-X', '--method']) {\n                  for (const inputSpec of [['--input', '-'], ['--input=-']]) {\n                    const args = ['api', endpoint, methodSpec, 'PUT', ...inputSpec];\n\n                    test(args.join(' '), async() => {\n                      const { stdout, stderr, error } = await rdctlWithStdin(settingsFile, args);\n\n                      expect({\n                        stdout, stderr, error,\n                      }).toEqual({\n                        error:  undefined,\n                        stderr: '',\n                        stdout: expect.not.stringContaining('apply'),\n                      });\n                    });\n                  }\n                }\n              }\n            });\n            test.describe('--input', () => {\n              const settingsFile = path.join(paths.config, 'settings.json');\n\n              for (const endpoint of ['settings', '/v1/settings']) {\n                for (const methodSpecs of [['-X', 'PUT'], ['--method', 'PUT'], []]) {\n                  for (const inputSource of [['--input', settingsFile], [`--input=${ settingsFile }`]]) {\n                    const args = ['api', endpoint, ...methodSpecs, ...inputSource];\n\n                    test(args.join(' '), async() => {\n                      const { stdout, stderr, error } = await rdctl(args);\n\n                      expect({\n                        stdout, stderr, error,\n                      }).toEqual({\n                        error:  undefined,\n                        stderr: '',\n                        stdout: expect.stringContaining('no changes necessary'),\n                      });\n                    });\n                  }\n                }\n              }\n            });\n\n            test('should complain about a \"--input-\" flag', async() => {\n              const { stdout, stderr, error } = await rdctl(['api', '/settings', '-X', 'PUT', '--input-']);\n\n              expect({\n                stdout, stderr, error,\n              }).toEqual({\n                error:  expect.any(Error),\n                stderr: expect.stringContaining('Error: unknown flag: --input-'),\n                stdout: '',\n              });\n              expect(stderr).toContain('Usage:');\n            });\n\n            test.describe('from body', () => {\n              const settingsFile = path.join(paths.config, 'settings.json');\n\n              for (const endpoint of ['settings', '/v1/settings']) {\n                for (const methodSpecs of [[], ['-X', 'PUT'], ['--method', 'PUT']]) {\n                  for (const inputOption of ['--body', '-b']) {\n                    const args = ['api', endpoint, ...methodSpecs, inputOption];\n\n                    test(args.join(' '), async() => {\n                      const settingsBody = await fs.promises.readFile(settingsFile, { encoding: 'utf-8' });\n                      const { stdout, stderr, error } = await rdctl(args.concat(settingsBody));\n\n                      expect({\n                        stdout, stderr, error,\n                      }).toEqual({\n                        error:  undefined,\n                        stderr: '',\n                        stdout: expect.stringContaining('no changes necessary'),\n                      });\n                    });\n                  }\n                }\n              }\n            });\n\n            test.describe('complains when body and input are both specified', () => {\n              for (const bodyOption of ['--body', '-b']) {\n                const args = ['api', 'settings', bodyOption, '{ \"doctor\": { \"wu\" : \"tang\" }}', '--input', 'mabels.farm'];\n\n                test(args.join(' '), async() => {\n                  const { stdout, stderr, error } = await rdctl(args);\n\n                  expect({\n                    stdout, stderr, error,\n                  }).toEqual({\n                    error:  expect.any(Error),\n                    stderr: expect.stringContaining('Error: api command: --body and --input options cannot both be specified'),\n                    stdout: '',\n                  });\n                  expect(stderr).toContain('Usage:');\n                });\n              }\n            });\n\n            test('complains when no body is provided', async() => {\n              const { stdout, stderr, error } = await rdctl(['api', 'settings', '-X', 'PUT']);\n\n              expect({\n                stdout, stderr, error,\n              }).toEqual({\n                error:  expect.any(Error),\n                stderr: expect.stringContaining('no settings specified in the request'),\n                stdout: expect.stringMatching(/{.*}/s),\n              });\n              expect(JSON.parse(stdout)).toEqual({ message: '400 Bad Request' } );\n              expect(stderr).not.toContain('Usage:');\n            });\n\n            test('invalid setting is specified', async() => {\n              const newSettings = { version: CURRENT_SETTINGS_VERSION, containerEngine: { name: 'beefalo' } };\n              const { stdout, stderr, error } = await rdctl(['api', 'settings', '-b', JSON.stringify(newSettings)]);\n\n              expect({\n                stdout, stderr, error,\n              }).toEqual({\n                error:  expect.any(Error),\n                stderr: expect.stringMatching(/errors in attempt to update settings:\\s+Invalid value for \"containerEngine.name\": <\"beefalo\">; must be one of \\[\"containerd\",\"moby\",\"docker\"\\]/),\n                stdout: expect.stringMatching(/{.*}/s),\n              });\n              expect(stderr).not.toContain('Usage:');\n              expect(JSON.parse(stdout)).toEqual({ message: '400 Bad Request' } );\n            });\n          });\n        });\n      });\n\n      test('complains on invalid endpoint', async() => {\n        const endpoint = '/v99/no/such/endpoint';\n        const { stdout, stderr, error } = await rdctl(['api', endpoint]);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining(`Unknown command: GET ${ endpoint }`),\n          stdout: expect.stringMatching(/{.*}/s),\n        });\n        expect(JSON.parse(stdout)).toEqual({ message: '404 Not Found' });\n        expect(stderr).not.toContain('Usage:');\n      });\n\n      test('complains on invalid unversioned endpoint', async() => {\n        const endpoint = '/v1/shazbat';\n        const { stdout, stderr, error } = await rdctl(['api', endpoint]);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  expect.any(Error),\n          stderr: expect.stringContaining(`Unknown command: GET ${ endpoint }`),\n          stdout: expect.stringMatching(/{\".+?\":\".+\"}/),\n        });\n        expect(JSON.parse(stdout)).toEqual({ message: '404 Not Found' });\n        expect(stderr).not.toContain('Usage:');\n      });\n\n      test.describe('getting endpoints', () => {\n        async function getEndpoints() {\n          const apiSpecPath = path.join(import.meta.dirname, '../pkg/rancher-desktop/assets/specs/command-api.yaml');\n          const apiSpec = await fs.promises.readFile(apiSpecPath, 'utf-8');\n          const specPaths = yaml.parse(apiSpec).paths;\n\n          return Object.entries<Record<string, unknown>>(specPaths)\n            .flatMap(([path, data]) => Object.keys(data).map(method => [path, method]))\n            .sort();\n        }\n\n        test('no paths should return all supported endpoints', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/']);\n          const endpoints = (await getEndpoints())\n            .map(([path, method]) => `${ method.toUpperCase() } ${ path }`);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout).sort()).toEqual(endpoints.sort());\n        });\n\n        test('version-only path for v0 should return only itself', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v0']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toEqual([\n            'GET /v0',\n          ]);\n        });\n\n        test('version-only path for v1 should return all endpoints in that version only', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1']);\n          const endpoints = (await getEndpoints())\n            .filter(([path]) => path.startsWith('/v1'))\n            .map(([path, method]) => `${ method.toUpperCase() } ${ path }`);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout).sort()).toEqual(endpoints.sort());\n        });\n        test('/v2 should fail', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v2']);\n\n          expect({ stdout: JSON.parse(stdout), stderr: stderr.trim() }).toMatchObject({ stdout: { message: '404 Not Found' }, stderr: 'Unknown command: GET /v2' });\n        });\n      });\n\n      test.describe('diagnostics', () => {\n        let categories: string[];\n\n        test('categories', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_categories']);\n\n          expect(stderr).toEqual('');\n          categories = JSON.parse(stdout);\n          expect(categories).toEqual(expect.arrayContaining(['Networking']));\n        });\n        test.skip('it finds the IDs for Utilities', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_ids?category=Utilities']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toEqual(expect.arrayContaining(['RD_BIN_IN_BASH_PATH', 'RD_BIN_SYMLINKS']));\n        });\n        test('it finds the IDs for Networking', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_ids?category=Networking']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toEqual(expect.arrayContaining(['CONNECTED_TO_INTERNET']));\n        });\n        test('it 404s for a nonexistent category', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_ids?category=cecinestpasuncategory']);\n\n          expect({ stdout: JSON.parse(stdout), stderr: stderr.trim() }).toMatchObject({ stdout: { message: '404 Not Found' }, stderr: 'No diagnostic checks found in category cecinestpasuncategory' });\n        });\n        test('it finds a diagnostic check', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?category=Networking&id=CONNECTED_TO_INTERNET']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toMatchObject({\n            checks: [{\n              id:          'CONNECTED_TO_INTERNET',\n              description: expect.stringMatching(/^The application/),\n              mute:        false,\n              passed:      expect.any(Boolean),\n            }],\n          });\n        });\n        test('it finds all diagnostic checks', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toMatchObject({\n            checks: expect.arrayContaining([\n              {\n                category:    'Networking',\n                id:          'CONNECTED_TO_INTERNET',\n                description: expect.stringMatching(/^The application/),\n                mute:        false,\n                fixes:       [],\n                passed:      expect.any(Boolean),\n              },\n            ]),\n          });\n        });\n        test.skip('it finds all diagnostic checks for a category', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?category=Utilities']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toEqual({\n            checks: [\n              {\n                category:    'Utilities',\n                id:          'RD_BIN_IN_BASH_PATH',\n                description: 'The ~/.rd/bin directory has not been added to the PATH, so command-line utilities are not configured in your bash shell.',\n                mute:        false,\n                fixes:       [\n                  { description: 'You have selected manual PATH configuration. You can let Rancher Desktop automatically configure it.' },\n                ],\n              },\n              {\n                category:    'Utilities',\n                id:          'RD_BIN_SYMLINKS',\n                description: 'Are the files under ~/.docker/cli-plugins symlinks to ~/.rd/bin?',\n                mute:        false,\n                fixes:       [\n                  { description: 'Replace existing files in ~/.rd/bin with symlinks to the application\\'s internal utility directory.' },\n                ],\n              },\n            ],\n          });\n        });\n        test('it finds a diagnostic check by checkID', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?id=CONNECTED_TO_INTERNET']);\n\n          expect(stderr).toEqual('');\n          expect(JSON.parse(stdout)).toMatchObject({\n            checks: [\n              {\n                category:    'Networking',\n                id:          'CONNECTED_TO_INTERNET',\n                description: expect.stringMatching(/^The application/),\n                mute:        false,\n              },\n            ],\n          });\n        });\n        test('it returns an empty array for a nonexistent category', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?category=not*a*category']);\n\n          expect({ stdout: JSON.parse(stdout), stderr } ).toMatchObject({ stdout: { checks: [] }, stderr: '' });\n        });\n        test('it returns an empty array for a nonexistent category with a valid ID', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?category=not*a*category&id=CONNECTED_TO_INTERNET']);\n\n          expect({ stdout: JSON.parse(stdout), stderr } ).toMatchObject({ stdout: { checks: [] }, stderr: '' });\n        });\n        test('it returns an empty array for a nonexistent checkID with a valid category', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?category=Utilities&id=CONNECTED_TO_INTERNET']);\n\n          expect({ stdout: JSON.parse(stdout), stderr } ).toMatchObject({ stdout: { checks: [] }, stderr: '' });\n        });\n        test('it returns an empty array for a nonexistent checkID when no category is specified', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/diagnostic_checks?&id=blip']);\n\n          expect({ stdout: JSON.parse(stdout), stderr } ).toMatchObject({ stdout: { checks: [] }, stderr: '' });\n        });\n      });\n\n      test.describe('other endpoints', () => {\n        test('it can find the about text', async() => {\n          const { stdout, stderr } = await rdctl(['api', '/v1/about']);\n\n          expect(stderr).toEqual('');\n          expect(stdout).toMatch(/\\w+/);\n        });\n      });\n    });\n\n    test.describe('shell', () => {\n      test('can run echo', async() => {\n        const { stdout, stderr, error } = await rdctl(['shell', 'echo', 'abc', 'def']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  undefined,\n          stderr: '',\n          stdout: expect.stringContaining('abc def'),\n        });\n      });\n      test('can run a command with a dash-option', async() => {\n        const { stdout, stderr, error } = await rdctl(['shell', 'uname', '-a']);\n\n        expect({\n          stdout, stderr, error,\n        }).toEqual({\n          error:  undefined,\n          stderr: '',\n          stdout: expect.stringMatching(/\\S/),\n        });\n      });\n      test('can run a shell', async() => {\n        const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rdctl-shell-input'));\n        const inputPath = path.join(tmpDir, 'echo.txt');\n\n        try {\n          await fs.promises.writeFile(inputPath, 'echo orate linds chump\\n');\n          const { stdout, stderr, error } = await rdctlWithStdin(inputPath, ['shell']);\n\n          expect({\n            stdout, stderr, error,\n          }).toEqual({\n            error:  undefined,\n            stderr: '',\n            stdout: expect.stringContaining('orate linds chump'),\n          });\n        } finally {\n          await fs.promises.rm(tmpDir, { recursive: true, force: true });\n        }\n      });\n    });\n  });\n\n  // Where is the test that pushes a supported update, you may be wondering?\n  // The problem with a positive test is that it needs to restart the backend. The UI disappears\n  // but the various back-end processes, as well as playwright, are still running.\n  // This kind of test would be better done as a standalone BAT-type test that can monitor\n  // the processes. Meanwhile, the unit tests verify that a valid payload should lead to an update.\n\n  // There's also no test checking for oversize-payload detection because when I try to create a\n  // payload > 2000 characters I get this error:\n  // FetchError: request to http://127.0.0.1:6107/v1/set failed, reason: socket hang up\n});\n"
  },
  {
    "path": "e2e/start-in-background.e2e.spec.ts",
    "content": "import { test, expect, ElectronApplication } from '@playwright/test';\n\nimport { createDefaultSettings, startRancherDesktop, teardown, tool } from './utils/TestUtils';\n\n/**\n * Using test.describe.serial make the test execute step by step, as described on each `test()` order\n * Playwright executes test in parallel by default and it will not work for our app backend loading process.\n * */\ntest.describe.serial('startInBackground setting', () => {\n  test('window should appear when startInBackground is false', async({ colorScheme }, testInfo) => {\n    createDefaultSettings({ application: { startInBackground: false } });\n    const logVariant = `startInBackgroundFalse`;\n    const electronApp = await startRancherDesktop(testInfo, { logVariant });\n\n    await expect(checkWindowOpened(electronApp)).resolves.toBe(true);\n    await teardown(electronApp, testInfo);\n  });\n\n  test('window should not appear when startInBackground is true', async({ colorScheme }, testInfo) => {\n    createDefaultSettings({ application: { startInBackground: true } });\n    const logVariant = `startInBackgroundTrue`;\n    const electronApp = await startRancherDesktop(testInfo, { logVariant });\n\n    await expect(checkWindowOpened(electronApp)).resolves.toBe(false);\n    await tool('rdctl', 'set', '--application.start-in-background=false');\n    await teardown(electronApp, testInfo);\n  });\n});\n\nfunction checkWindowOpened(electronApp: ElectronApplication): Promise<boolean> {\n  const promise = new Promise<boolean>((resolve) => {\n    electronApp.on('window', () => resolve(true));\n    setTimeout(() => resolve(false), 10_000);\n  });\n\n  // Check for any windows that may have been created since defining the\n  // 'window' handler on electronApp\n  for (const window of electronApp.windows()) {\n    if (window.url().startsWith('app://')) {\n      return Promise.resolve(true);\n    }\n  }\n\n  return promise;\n}\n"
  },
  {
    "path": "e2e/startup-profiles.e2e.spec.ts",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { expect, test } from '@playwright/test';\nimport _ from 'lodash';\n\nimport {\n  clearSettings,\n  clearUserProfile,\n  testForFirstRunWindow,\n  testForNoFirstRunWindow,\n  testWaitForLogfile,\n  verifyNoSystemProfile,\n  verifySettings,\n  verifySystemProfile,\n  verifyUserProfile,\n} from './utils/ProfileUtils';\nimport { setUserProfile, reportAsset } from './utils/TestUtils';\n\nimport { CURRENT_SETTINGS_VERSION, Settings } from '@pkg/config/settings';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nasync function createInvalidDarwinUserProfile(contents: string) {\n  const userProfilePath = path.join(paths.deploymentProfileUser, 'io.rancherdesktop.profile.defaults.plist');\n\n  await fs.promises.writeFile(userProfilePath, contents);\n}\n\nasync function createInvalidLinuxUserProfile(contents: string) {\n  const userProfilePath = path.join(paths.deploymentProfileUser, 'rancher-desktop.defaults.json');\n\n  await fs.promises.writeFile(userProfilePath, contents);\n}\n\nasync function addRegistryEntry(path: string, name: string, valueType: string, value: string) {\n  await childProcess.spawnFile('reg',\n    ['add', path, '/v', name, '/f', '/t', valueType, '/d', value],\n    { stdio: ['ignore', 'pipe', 'pipe'] });\n}\n\nasync function createDefaultUserRegistryProfileWithNonexistentFields() {\n  let base = 'HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Defaults';\n\n  await addRegistryEntry(base, 'version', 'REG_DWORD', '10');\n\n  base += '\\\\fruits';\n  await addRegistryEntry(base, 'oranges', 'REG_DWORD', '5');\n  await addRegistryEntry(base, 'mangoes', 'REG_DWORD', '1');\n  await addRegistryEntry(base, 'citrus', 'REG_SZ', 'lemons');\n}\n\nasync function createDefaultUserRegistryProfileWithIncorrectTypes() {\n  let base = 'HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Defaults';\n\n  await addRegistryEntry(base, 'version', 'REG_DWORD', '10');\n\n  base += '\\\\kubernetes';\n  await addRegistryEntry(base, 'version', 'REG_MULTI_SZ', 'strawberries\\\\0limes');\n}\n\nasync function createDefaultUserRegistryProfileWithValidDataButNoVersion() {\n  const base = 'HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Defaults\\\\kubernetes';\n\n  await addRegistryEntry(base, 'version', 'REG_SZ', '1.29.0');\n}\n\nasync function createLockedUserRegistryProfileWithValidDataButNoVersion() {\n  const base = 'HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Locked\\\\kubernetes';\n\n  await addRegistryEntry(base, 'version', 'REG_SZ', '1.29.0');\n}\n\ntest.describe.serial('starting up with profiles', () => {\n  test.afterAll(async() => {\n    await clearUserProfile();\n    await clearSettings();\n  });\n  test.describe.serial('profile combinations', () => {\n    // First time we want to verify there *is* a first-run window.\n    // There should never be a first-run window after that.\n    let runFunc = testForFirstRunWindow;\n    let i = 0;\n    let numSkipped = 0;\n\n    for (const settingsFunc of [clearSettings, verifySettings]) {\n      for (const userProfileFunc of [clearUserProfile, verifyUserProfile]) {\n        for (const systemProfileFunc of [verifyNoSystemProfile, verifySystemProfile]) {\n          test(`Standard test ${ i }: ${ settingsFunc.name } / ${ userProfileFunc.name } / ${ systemProfileFunc.name }`, async({ colorScheme }, testInfo) => {\n            const skipReasons = await systemProfileFunc();\n\n            if (skipReasons.length > 0) {\n              console.log(`Skipping test (${ systemProfileFunc.name })`);\n              numSkipped += 1;\n            } else {\n              await settingsFunc();\n              await userProfileFunc();\n              await runFunc(testInfo, { logVariant: `${ i }` });\n              runFunc = testForNoFirstRunWindow;\n            }\n          });\n          i++;\n        }\n      }\n    }\n    test('check for correct number of tests', () => {\n      // Half the tests require a system profile, half require no system-profile, so we should always skip half of them.\n      expect(numSkipped).toEqual(4);\n    });\n  });\n\n  test.describe('problematic user profiles', () => {\n    let skipReasons: string[];\n\n    test.beforeEach(async() => {\n      await clearSettings();\n      await clearUserProfile();\n      skipReasons = await verifyNoSystemProfile();\n    });\n\n    test('nonexistent settings act like an empty default profile', async({ colorScheme }, testInfo) => {\n      test.skip(skipReasons.length > 0, `Profile requirements for this test: ${ skipReasons.join(', ') }`);\n      if (process.platform === 'win32') {\n        await createDefaultUserRegistryProfileWithNonexistentFields();\n      } else {\n        const s1 = {\n          version: 10,\n          fruits:  {\n            oranges: 5, mangoes: true, citrus: 'lemons',\n          },\n        } as unknown as RecursivePartial<Settings>;\n\n        await setUserProfile(s1, null);\n      }\n      // We have a deployment with only a version field, good enough to bypass the first-run dialog.\n      await testForNoFirstRunWindow(testInfo, { logVariant: 'nonexistent-settings' });\n    });\n\n    test('invalid format', async({ colorScheme }, testInfo) => {\n      let errorMatcher: RegExp;\n      const logVariant = 'invalid-profile-format';\n      const localSkipReasons = [...skipReasons];\n\n      if (process.platform === 'win32') {\n        localSkipReasons.push(`This test doesn't make sense on Windows`);\n      }\n      test.skip(localSkipReasons.length > 0, `Profile requirements for this test: ${ localSkipReasons.join(', ') }`);\n      switch (process.platform) {\n      case 'darwin':\n        await createInvalidDarwinUserProfile(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>kubernetes</key>\n    <dict>\n      <key>version</key>\n      <array>\n        <string>str`);\n        errorMatcher = new RegExp(`Error loading plist file ${ path.join(paths.deploymentProfileUser, 'io.rancherdesktop.profile.defaults.plist') }.*Property List error: Encountered unexpected EOF`);\n        break;\n      case 'linux':\n        await createInvalidLinuxUserProfile(`{\"kubernetes\":{\"version\":[\"str`);\n        errorMatcher = new RegExp(`Error starting up: DeploymentProfileError: Error parsing deployment profile from ${ path.join(paths.deploymentProfileUser, 'rancher-desktop.defaults.json') }: SyntaxError: Unterminated string in JSON`);\n        break;\n      default:\n        throw new Error(`Not expecting to handle platform ${ process.platform }`);\n      }\n      const windowCount = await testWaitForLogfile(testInfo, { logVariant });\n      const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n      const contents = await fs.promises.readFile(logPath, { encoding: 'utf-8' });\n\n      expect(windowCount).toEqual(0);\n      expect(contents).toContain('Fatal Error:');\n      expect(contents).toMatch(errorMatcher);\n    });\n\n    test('missing version', async({ colorScheme }, testInfo) => {\n      const logVariant = 'missing-settings-version';\n      const versionLessSettings: RecursivePartial<Settings> = {\n        kubernetes:  { enabled: true },\n        application: {\n          debug:                  true,\n          pathManagementStrategy: PathManagementStrategy.Manual,\n          startInBackground:      false,\n        },\n      };\n      const settingsFullPath = path.join(paths.config, 'settings.json');\n\n      await fs.promises.mkdir(paths.config, { recursive: true });\n      await fs.promises.writeFile(settingsFullPath, JSON.stringify(versionLessSettings));\n      const windowCount = await testWaitForLogfile(testInfo, { logVariant });\n      const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n      const contents = await fs.promises.readFile(logPath, { encoding: 'utf-8' });\n\n      expect(windowCount).toEqual(0);\n      const msg = `No version specified in ${ settingsFullPath }`;\n\n      expect(contents).toMatch(new RegExp(`Fatal Error:.*${ _.escapeRegExp(msg) }`, 's'));\n    });\n\n    test('wrong datatype in profile', async({ colorScheme }, testInfo) => {\n      const logVariant = 'wrong-datatype-in-profile';\n\n      test.skip(skipReasons.length > 0, `Profile requirements for this test: ${ skipReasons.join(', ') }`);\n      if (process.platform === 'win32') {\n        await createDefaultUserRegistryProfileWithIncorrectTypes();\n      } else {\n        const s1 = { version: 10, kubernetes: { version: ['strawberries', 'limes'] } } as unknown as RecursivePartial<Settings>;\n\n        await setUserProfile(s1, null);\n      }\n      const windowCount = await testWaitForLogfile(testInfo, { logVariant });\n      const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n      const contents = await fs.promises.readFile(logPath, { encoding: 'utf-8' });\n\n      expect(windowCount).toEqual(0);\n      expect(contents).toContain('Fatal Error:');\n      if (process.platform === 'win32') {\n        expect(contents).toContain(`Error for field 'HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Defaults\\\\kubernetes\\\\version'`);\n        expect(contents).toContain(`expecting value of type string, got an array '[\"strawberries\",\"limes\"]'`);\n      } else {\n        expect(contents).toMatch(new RegExp(`Error in deployment file.*${ paths.deploymentProfileUser }.*defaults`));\n        expect(contents).toContain(`Error for field 'kubernetes.version':`);\n        expect(contents).toContain(`expecting value of type string, got an array [\"strawberries\",\"limes\"]`);\n      }\n    });\n\n    test('missing version in defaults deployment profile', async({ colorScheme }, testInfo) => {\n      const logVariant = `missing-version-in-defaults-profile`;\n\n      test.skip(skipReasons.length > 0, `Profile requirements for this test: ${ skipReasons.join(', ') }`);\n      if (process.platform === 'win32') {\n        await createDefaultUserRegistryProfileWithValidDataButNoVersion();\n      } else {\n        await setUserProfile({ kubernetes: { enabled: false } }, null);\n      }\n      const windowCount = await testWaitForLogfile(testInfo, { logVariant });\n      const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n      const contents = await fs.promises.readFile(logPath, { encoding: 'utf-8' });\n\n      expect(windowCount).toEqual(0);\n      expect(contents).toContain('Fatal Error:');\n      if (process.platform === 'win32') {\n        expect(contents).toContain('Invalid default-deployment: no version specified at HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Defaults.');\n        expect(contents).toContain(`You'll need to add a version field to make it valid (current version is ${ CURRENT_SETTINGS_VERSION }).`);\n      } else {\n        expect(contents).toContain('Failed to load the deployment profile');\n        expect(contents).toMatch(/Invalid deployment file.*defaults.*: no version specified. You'll need to add a version field to make it valid/);\n      }\n    });\n\n    test('missing version in locked deployment profile', async({ colorScheme }, testInfo) => {\n      const logVariant = 'missing-version-in-locked-profile';\n\n      test.skip(skipReasons.length > 0, `Profile requirements for this test: ${ skipReasons.join(', ') }`);\n      if (process.platform === 'win32') {\n        await createLockedUserRegistryProfileWithValidDataButNoVersion();\n      } else {\n        await setUserProfile(null, { kubernetes: { enabled: false } });\n      }\n      const windowCount = await testWaitForLogfile(testInfo, { logVariant });\n      const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n      const contents = await fs.promises.readFile(logPath, { encoding: 'utf-8' });\n\n      expect(windowCount).toEqual(0);\n      expect(contents).toContain('Fatal Error:');\n      if (process.platform === 'win32') {\n        expect(contents).toContain('Invalid locked-deployment: no version specified at HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\Locked.');\n        expect(contents).toContain(`You'll need to add a version field to make it valid (current version is ${ CURRENT_SETTINGS_VERSION }).`);\n      } else {\n        expect(contents).toContain('Failed to load the deployment profile');\n        expect(contents).toMatch(/Invalid deployment file.*locked.*: no version specified. You'll need to add a version field to make it valid/);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "e2e/utils/ProfileUtils.ts",
    "content": "// Deployment-profile-related utilities\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\n\nimport { expect, Page, TestInfo } from '@playwright/test';\n\nimport {\n  createDefaultSettings,\n  setUserProfile,\n  startRancherDesktop,\n  retry,\n  teardown,\n  reportAsset,\n  startRancherDesktopOptions,\n} from './TestUtils';\nimport { NavPage } from '../pages/nav-page';\n\nimport { CURRENT_SETTINGS_VERSION } from '@pkg/config/settings';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\n\nexport async function clearSettings(): Promise<void> {\n  const fullPath = path.join(paths.config, 'settings.json');\n\n  await fs.promises.rm(fullPath, { force: true });\n}\n\nexport async function clearUserProfile(): Promise<void> {\n  const platform = os.platform() as 'win32' | 'darwin' | 'linux';\n\n  if (platform === 'win32') {\n    return await verifyNoRegistrySubtree('HKCU');\n  }\n  const profilePaths = getDeploymentPaths(platform, paths.deploymentProfileUser);\n\n  for (const fullPath of profilePaths) {\n    await fs.promises.rm(fullPath, { force: true });\n  }\n}\n\nasync function fileExists(fullPath: string): Promise<boolean> {\n  try {\n    await fs.promises.access(fullPath);\n\n    return true;\n  } catch { }\n\n  return false;\n}\n\nfunction getDeploymentBaseNames(platform: 'linux' | 'darwin'): string[] {\n  if (platform === 'linux') {\n    return ['rancher-desktop.defaults.json', 'rancher-desktop.locked.json'];\n  } else if (platform === 'darwin') {\n    return ['io.rancherdesktop.profile.defaults.plist', 'io.rancherdesktop.profile.locked.plist'];\n  } else {\n    throw new Error(`Unexpected platform ${ platform }`);\n  }\n}\n\nfunction getDeploymentPaths(platform: 'linux' | 'darwin', profileDir: string): string[] {\n  let baseNames = getDeploymentBaseNames(platform);\n\n  if (platform === 'linux' && profileDir !== paths.deploymentProfileUser) {\n    // macOS system profiles live in a shared directory and include the application name;\n    // linux ones are in their own directory, and we need to remove the prefix.\n    baseNames = baseNames.map(s => s.replace('rancher-desktop.', ''));\n  }\n\n  return baseNames.map(baseName => path.join(profileDir, baseName));\n}\n\nasync function hasSystemRegistrySubtree(): Promise<boolean> {\n  for (const profileType of ['defaults', 'locked']) {\n    for (const variant of ['Policies\\\\Rancher Desktop', 'Rancher Desktop\\\\Profile']) {\n      try {\n        const { stdout } = await childProcess.spawnFile('reg',\n          ['query', `HKLM\\\\SOFTWARE\\\\${ variant }\\\\${ profileType }`],\n          { stdio: ['ignore', 'pipe', 'pipe'] });\n\n        if (stdout.length > 0) {\n          return true;\n        }\n      } catch { }\n    }\n  }\n\n  return false;\n}\n\nexport async function verifySystemRegistrySubtree(): Promise<string[]> {\n  if (await hasSystemRegistrySubtree()) {\n    return [];\n  } else {\n    return [`Need to add registry subtree \"HKLM\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\<defaults or locked>\"`];\n  }\n}\n\nexport async function verifySettings(): Promise<void> {\n  const fullPath = path.join(paths.config, 'settings.json');\n\n  if (!await fileExists(fullPath)) {\n    createDefaultSettings();\n  }\n}\n\nexport async function verifyNoRegistrySubtree(hive: string): Promise<void> {\n  for (const variant of ['Policies\\\\Rancher Desktop', 'Rancher Desktop\\\\Profile']) {\n    const registryPath = `${ hive }\\\\SOFTWARE\\\\${ variant }`;\n\n    try {\n      const { stdout } = await childProcess.spawnFile('reg',\n        ['query', registryPath],\n        { stdio: ['ignore', 'pipe', 'pipe'] });\n\n      if (stdout.length === 0) {\n        continue;\n      }\n    } catch {\n      continue;\n    }\n    try {\n      await childProcess.spawnFile('reg', ['delete', registryPath, '/f'], { stdio: ['ignore', 'pipe', 'pipe'] });\n    } catch (cause: any) {\n      throw new Error(`Need to remove registry hive \"${ registryPath }\" (tried, got error ${ cause })`, { cause });\n    }\n  }\n}\n\nexport async function verifyUserProfile(): Promise<void> {\n  await clearUserProfile();\n  await setUserProfile({ version: 10 as typeof CURRENT_SETTINGS_VERSION, containerEngine: { allowedImages: { enabled: true } } }, null);\n}\n\nexport async function verifyNoSystemProfile(): Promise<string[]> {\n  const platform = os.platform() as 'win32' | 'darwin' | 'linux';\n\n  if (platform === 'win32') {\n    try {\n      await verifyNoRegistrySubtree('HKLM');\n\n      return [];\n    } catch (ex: any) {\n      return [ex.message];\n    }\n  }\n  const existingProfiles = [];\n  const profilePaths = [\n    ...getDeploymentPaths(platform, paths.deploymentProfileSystem),\n    ...getDeploymentPaths(platform, paths.altDeploymentProfileSystem),\n  ];\n\n  for (const profilePath of profilePaths) {\n    if (await fileExists(profilePath)) {\n      existingProfiles.push(`Need to delete system profile ${ profilePath }`);\n    }\n  }\n\n  return existingProfiles;\n}\n\nexport async function verifySystemProfile(): Promise<string[]> {\n  const platform = os.platform() as 'win32' | 'darwin' | 'linux';\n\n  if (platform === 'win32') {\n    return await verifySystemRegistrySubtree();\n  }\n\n  const profilePaths = [\n    ...getDeploymentPaths(platform, paths.deploymentProfileSystem),\n    ...getDeploymentPaths(platform, paths.altDeploymentProfileSystem),\n  ];\n\n  for (const profilePath of profilePaths) {\n    if (await fileExists(profilePath)) {\n      return [];\n    }\n  }\n\n  return [`Need to create system profile file ${ profilePaths.join(' and/or ') }`];\n}\n\n/**\n * Start Rancher Desktop, expecting a first run window to show up; accept it,\n * then wait for the main window to open.\n */\nexport async function testForFirstRunWindow(testInfo: TestInfo, options: startRancherDesktopOptions) {\n  let page: Page | undefined;\n  let navPage: NavPage;\n  let windowCount = 0;\n  let windowCountForMainPage = 0;\n  const electronApp = await startRancherDesktop(testInfo, {\n    ...options, mock: false, noModalDialogs: false, timeout: 60_000,\n  });\n\n  electronApp.on('window', async(openedPage: Page) => {\n    windowCount += 1;\n    if (windowCount === 1) {\n      await retry(async() => {\n        const button = openedPage.getByText('OK');\n\n        if (button) {\n          await button.click({ timeout: 10_000 });\n        }\n      }, { delay: 100, tries: 50 });\n\n      return;\n    }\n    navPage = new NavPage(openedPage);\n\n    try {\n      await retry(async() => {\n        await expect(navPage.mainTitle).toHaveText('Welcome to Rancher Desktop by SUSE');\n      });\n      page = openedPage;\n      windowCountForMainPage = windowCount;\n    } catch (ex: any) {\n      console.log(`Ignoring failed title-test: ${ ex.toString().substring(0, 10000) }`);\n    }\n  });\n  try {\n    let iter = 0;\n    const start = new Date().valueOf();\n    const limit = 900 * 1_000 + start;\n\n    // eslint-disable-next-line no-unmodified-loop-condition\n    while (page === undefined) {\n      const now = new Date().valueOf();\n\n      iter += 1;\n      if (iter % 100 === 0) {\n        console.log(`waiting for main window, iter ${ iter }...`);\n      }\n      if (now > limit) {\n        throw new Error(`timed out waiting for ${ limit / 1000 } seconds`);\n      }\n      await util.promisify(setTimeout)(100);\n    }\n    expect(windowCountForMainPage).toEqual(2);\n  } finally {\n    await teardown(electronApp, testInfo);\n  }\n}\n\n/**\n * Start Rancher Desktop, checking that there was no first run window (and that\n * the first window to appear is the main window).\n */\nexport async function testForNoFirstRunWindow(testInfo: TestInfo, options: startRancherDesktopOptions) {\n  let page: Page | undefined;\n  let navPage: NavPage;\n  let windowCount = 0;\n  let windowCountForMainPage = 0;\n  const electronApp = await startRancherDesktop(testInfo, {\n    ...options, mock: false, noModalDialogs: false, timeout: 60_000,\n  });\n\n  electronApp.on('window', async(openedPage: Page) => {\n    windowCount += 1;\n    navPage = new NavPage(openedPage);\n\n    await expect(async() => {\n      await expect(navPage.mainTitle).toHaveText('Welcome to Rancher Desktop by SUSE');\n    }).toPass({ timeout: 60_000 });\n    page = openedPage;\n    windowCountForMainPage = windowCount;\n  });\n  try {\n    let iter = 0;\n    const start = new Date().valueOf();\n    const limit = 900 * 1_000 + start;\n\n    // eslint-disable-next-line no-unmodified-loop-condition\n    while (page === undefined) {\n      const now = new Date().valueOf();\n\n      iter += 1;\n      if (iter % 100 === 0) {\n        console.log(`waiting for main window, iter ${ iter }...`);\n      }\n      if (now > limit) {\n        throw new Error(`timed out waiting for ${ limit / 1000 } seconds`);\n      }\n      await util.promisify(setTimeout)(100);\n    }\n    expect(windowCountForMainPage).toEqual(1);\n  } finally {\n    await teardown(electronApp, testInfo);\n  }\n}\n\n/**\n * Start Rancher Desktop, and wait for background.log file to be populated; there\n * should be no windows visible.\n */\nexport async function testWaitForLogfile(testInfo: TestInfo, options: startRancherDesktopOptions) {\n  let windowCount = 0;\n  const electronApp = await startRancherDesktop(testInfo, {\n    ...options, mock: false, noModalDialogs: true, timeout: 60_000,\n  });\n  const logPath = path.join(reportAsset(testInfo, 'log'), 'background.log');\n\n  electronApp.on('window', () => {\n    windowCount += 1;\n    console.log('There should be no windows for this test.');\n  });\n  try {\n    let iter = 0;\n    const start = new Date().valueOf();\n    const limit = 900 * 1_000 + start;\n\n    while (true) {\n      const now = new Date().valueOf();\n\n      iter += 1;\n      if (iter % 100 === 0) {\n        console.log(`waiting for logs, iter ${ iter }...`);\n      }\n      try {\n        const statInfo = await fs.promises.lstat(logPath);\n\n        if (statInfo && statInfo.size > 160) {\n          break;\n        }\n      } catch {}\n      if (now > limit) {\n        throw new Error(`timed out waiting for ${ limit / 1000 } seconds`);\n      }\n      if (windowCount > 0) {\n        break;\n      }\n      await util.promisify(setTimeout)(100);\n    }\n  } finally {\n    try {\n      // Race condition: the app might have already shut down due to the fatal profile error.\n      await teardown(electronApp, testInfo);\n    } catch {\n    }\n  }\n\n  return windowCount;\n}\n"
  },
  {
    "path": "e2e/utils/TestUtils.ts",
    "content": "/**\n * TestUtils exports functions required for the E2E test specs.\n */\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\n\nimport { expect, _electron, ElectronApplication, TestInfo } from '@playwright/test';\nimport _, { GetFieldType } from 'lodash';\nimport { Page } from 'playwright-core';\nimport plist from 'plist';\n\nimport { defaultSettings, LockedSettingsType, Settings } from '@pkg/config/settings';\nimport { getDefaultMemory } from '@pkg/config/settingsImpl';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial, RecursiveTypes } from '@pkg/utils/typeUtils';\n\nlet currentTest: undefined | {\n  file:      string,\n  startTime: number,\n  options:   startRancherDesktopOptions,\n};\n\n/**\n * Remove any existing user profiles, and set it to the given settings.  If\n * either is `null`, then it is not re-added.\n */\nexport async function setUserProfile(userProfile: RecursivePartial<Settings> | null, lockedFields:LockedSettingsType | null) {\n  const platform = os.platform() as 'win32' | 'darwin' | 'linux';\n\n  if (platform === 'win32') {\n    return await setWindowsUserLegacyProfile(userProfile, lockedFields);\n  } else if (platform === 'linux') {\n    return await setLinuxUserProfile(userProfile, lockedFields);\n  } else {\n    return await setDarwinUserProfile(userProfile, lockedFields);\n  }\n}\n\nasync function setLinuxUserProfile(userProfile: RecursivePartial<Settings> | null, lockedFields:LockedSettingsType | null) {\n  const userProfilePath = path.join(paths.deploymentProfileUser, 'rancher-desktop.defaults.json');\n  const userLocksPath = path.join(paths.deploymentProfileUser, 'rancher-desktop.locked.json');\n\n  if (userProfile && Object.keys(userProfile).length > 0) {\n    await fs.promises.writeFile(userProfilePath, JSON.stringify(userProfile, undefined, 2));\n  } else {\n    await fs.promises.rm(userProfilePath, { force: true });\n  }\n  if (lockedFields && Object.keys(lockedFields).length > 0) {\n    await fs.promises.writeFile(userLocksPath, JSON.stringify(lockedFields, undefined, 2));\n  } else {\n    await fs.promises.rm(userLocksPath, { force: true });\n  }\n}\n\nfunction convertToRegistryLegacy(s: string) {\n  return s.replace(/Policies\\\\Rancher Desktop/g, 'Rancher Desktop\\\\Profile')\n    .replace('SOFTWARE\\\\Policies]', 'SOFTWARE\\\\Rancher Desktop]');\n}\n\nasync function setWindowsUserLegacyProfile(userProfile: RecursivePartial<Settings> | null, lockedFields:LockedSettingsType | null) {\n  const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-test-profiles'));\n\n  try {\n    for (const [registryType, settings] of [['defaults', userProfile], ['locked', lockedFields]] as const) {\n      // Always remove existing profiles, since we never want to merge any\n      // existing profiles with the new ones.\n      try {\n        const keyPath = `HKCU\\\\SOFTWARE\\\\Rancher Desktop\\\\Profile\\\\${ registryType }`;\n\n        await childProcess.spawnFile('reg.exe', ['DELETE', keyPath, '/f'], { stdio: 'pipe' });\n      } catch (cause: any) {\n        if (!/unable to find/.test(Object(cause).stderr ?? '')) {\n          throw new Error(`Error trying to delete a user registry hive: ${ cause }`, { cause });\n        }\n      }\n\n      if (settings && Object.keys(settings).length > 0) {\n        const genResult = convertToRegistryLegacy(await tool('rdctl', 'create-profile', '--body', JSON.stringify(settings),\n          '--output=reg', '--hive=hkcu', `--type=${ registryType }`));\n        const regFile = path.join(workdir, 'test.reg');\n\n        try {\n          await fs.promises.writeFile(regFile, genResult);\n          await childProcess.spawnFile('reg.exe', ['IMPORT', regFile], { stdio: 'ignore' });\n        } catch (cause: any) {\n          throw new Error(`Error trying to create a user registry hive: ${ cause }`, { cause });\n        }\n      }\n    }\n  } finally {\n    await fs.promises.rm(workdir, { recursive: true, force: true });\n  }\n}\n\nasync function setDarwinUserProfile(userProfile: RecursivePartial<Settings> | null, lockedFields:LockedSettingsType | null) {\n  const userProfilePath = path.join(paths.deploymentProfileUser, 'io.rancherdesktop.profile.defaults.plist');\n  const userLocksPath = path.join(paths.deploymentProfileUser, 'io.rancherdesktop.profile.locked.plist');\n\n  if (userProfile && Object.keys(userProfile).length > 0) {\n    // plist.build() seems to have issues with RecursivePartial<Record<string, string>>, hence cast.\n    await fs.promises.writeFile(userProfilePath, plist.build(userProfile as any));\n  } else {\n    await fs.promises.rm(userProfilePath, { force: true });\n  }\n  if (lockedFields && Object.keys(lockedFields).length > 0) {\n    await fs.promises.writeFile(userLocksPath, plist.build(lockedFields));\n  } else {\n    await fs.promises.rm(userLocksPath, { force: true });\n  }\n}\n\n/**\n * Create empty default settings to bypass gracefully\n * FirstPage window.\n */\nexport function createDefaultSettings(overrides: RecursivePartial<Settings> = {}) {\n  const defaultOverrides: RecursivePartial<Settings> = {\n    kubernetes:  { enabled: true },\n    application: {\n      debug:                  true,\n      pathManagementStrategy: PathManagementStrategy.Manual,\n      startInBackground:      false,\n    },\n    virtualMachine: { memoryInGB: getDefaultMemory() },\n  };\n  const settingsData: Settings = _.merge({}, defaultSettings, defaultOverrides, overrides);\n\n  const settingsJson = JSON.stringify(settingsData);\n  const fileSettingsName = 'settings.json';\n  const settingsFullPath = path.join(paths.config, fileSettingsName);\n\n  if (!fs.existsSync(settingsFullPath)) {\n    fs.mkdirSync(paths.config, { recursive: true });\n    fs.writeFileSync(path.join(paths.config, fileSettingsName), settingsJson);\n    console.log(`Default settings file successfully created at ${ paths.config }/${ fileSettingsName }`);\n  } else {\n    try {\n      const contents = fs.readFileSync(settingsFullPath, { encoding: 'utf-8' });\n      const settings: Settings = JSON.parse(contents.toString());\n      const desiredSettings: Settings = _.merge({}, settings, defaultOverrides, overrides);\n\n      if (!_.eq(settings, desiredSettings)) {\n        fs.writeFileSync(settingsFullPath, JSON.stringify(desiredSettings), { encoding: 'utf-8' });\n      }\n    } catch (err) {\n      console.log(`Failed to process ${ settingsFullPath }: ${ err }`);\n    }\n  }\n}\n\n/**\n * getAlternateSetting returns the setting that isn't the same as the existing setting.\n */\nexport function getAlternateSetting<K extends keyof RecursiveTypes<Settings>>(currentSettings: Settings, setting: K, altOne: GetFieldType<Settings, K>, altTwo: GetFieldType<Settings, K>) {\n  return _.get(currentSettings, setting) === altOne ? altTwo : altOne;\n}\n\n/**\n * Calculate the path of an asset that should be attached to a test run.\n * @param type What kind of asset this is; defaults to `trace`.\n */\nexport function reportAsset(testInfo: TestInfo, type: 'trace' | 'log' = 'trace') {\n  const testName = testInfo.file;\n  let name = `${ path.basename(testName).replace(/(?:\\.e2e)(?:\\.spec)(?:\\.ts)$/, '') }-`;\n\n  if (currentTest?.options?.logVariant) {\n    name += `${ currentTest.options.logVariant }-`;\n  }\n  if (testInfo.retry) {\n    name += `try-${ testInfo.retry }-`;\n  }\n  name += {\n    trace: 'pw-trace.zip',\n    log:   'logs',\n  }[type];\n\n  return path.join(import.meta.dirname, '..', 'reports', name);\n}\n\n/**\n * Tear down the application, without managing logging.  This should only be\n * used when doing atypical tests that need to restart the application within\n * the test.  This is normally used instead of `app.close()`.\n *\n * @note teardown() should be used where possible.\n */\nexport async function teardownApp(app: ElectronApplication) {\n  const proc = app.process();\n  const pid = proc.pid;\n\n  try {\n    // Allow one minute for shutdown\n    await Promise.race([\n      app.close(),\n      util.promisify(setTimeout)(60 * 1000),\n    ]);\n    await tool('rdctl', 'shutdown');\n  } finally {\n    if (proc.kill('SIGTERM') || proc.kill('SIGKILL')) {\n      console.log(`Manually stopped process ${ pid }`);\n    }\n    // Try to do platform-specific killing based on process groups\n    if (process.platform === 'darwin' || process.platform === 'linux') {\n      // Send SIGTERM to the process group, wait three seconds, then send\n      // SIGKILL and wait for one more second.\n      for (const [signal, timeout] of [['TERM', 3_000], ['KILL', 1_000]] as const) {\n        let pids: string[];\n\n        try {\n          const args = ['-o', 'pid=', process.platform === 'darwin' ? '-g' : '--sid', `${ pid }`];\n          const { stdout } = await childProcess.spawnFile('ps', args, { stdio: ['ignore', 'pipe', 'inherit'] });\n\n          pids = stdout.trim().split(/\\s+/);\n        } catch (ex) {\n          console.log(`Did not find processes in process group ${ pid }, ignoring.`);\n          break;\n        }\n\n        try {\n          if (pids.length > 0) {\n            console.log(`Manually killing group processes ${ pids.join(' ') }`);\n            await childProcess.spawnFile('kill', ['-s', signal, ...pids]);\n          }\n        } catch (ex) {\n          console.log(`Failed to process group: ${ ex } (retrying)`);\n        }\n        await util.promisify(setTimeout)(timeout);\n      }\n    }\n  }\n}\n\nexport async function teardown(app: ElectronApplication, testInfo: TestInfo) {\n  const context = app.context();\n  const { file: filename } = testInfo;\n\n  await context.tracing.stop({ path: reportAsset(testInfo) });\n  await teardownApp(app);\n\n  if (currentTest?.file === filename) {\n    const delta = (Date.now() - currentTest.startTime) / 1_000;\n    const min = Math.floor(delta / 60);\n    const sec = Math.round(delta % 60);\n    const string = min ? `${ min } min ${ sec } sec` : `${ sec } seconds`;\n\n    console.log(`Test ${ path.basename(filename) } took ${ string }.`);\n  } else {\n    console.log(`Test ${ path.basename(filename) } did not have a start time.`);\n  }\n}\n\nexport function getResourceBinDir(): string {\n  const srcDir = path.dirname(import.meta.dirname);\n\n  return path.join(srcDir, '..', 'resources', os.platform(), 'bin');\n}\n\nexport function getFullPathForTool(tool: string): string {\n  const filename = os.platform().startsWith('win') ? `${ tool }.exe` : tool;\n\n  return path.join(getResourceBinDir(), filename);\n}\n\n/**\n * Run the given tool with the given arguments, returning its standard output.\n */\nexport async function tool(tool: string, ...args: string[]): Promise<string> {\n  const exe = getFullPathForTool(tool);\n\n  try {\n    const { stdout } = await childProcess.spawnFile(exe, args, {\n      env: {\n        ...process.env,\n        PATH: `${ process.env.PATH }${ path.delimiter }${ getResourceBinDir() }`,\n      },\n      stdio: ['ignore', 'pipe', 'pipe'],\n    });\n\n    return stdout;\n  } catch (ex:any) {\n    console.error(`Error running ${ tool } ${ args.join(' ') }`);\n    console.error(`stdout: ${ ex.stdout }`);\n    console.error(`stderr: ${ ex.stderr }`);\n    // This expect(...).toBeUndefined() will always fail; we just want to make\n    // playwright print out the stdout and stderr along with the message.\n    // Normally, it would just print out `ex.toString()`, which mostly just says\n    // \"<command> exited with code 1\" and doesn't explain _why_ that happened.\n    expect({\n      stdout: ex.stdout, stderr: ex.stderr, message: ex.toString(),\n    }).toBeUndefined();\n    throw ex;\n  }\n}\n\n/**\n * Run `kubectl` with given arguments.\n * @returns standard output of the command.\n * @example await kubectl('version')\n */\nexport async function kubectl(...args: string[] ): Promise<string> {\n  return await tool('kubectl', '--context', 'rancher-desktop', ...args);\n}\n\n/**\n * Run `helm` with given arguments.\n * @returns standard output of the command.\n * @example await helm('version')\n */\nexport async function helm(...args: string[] ): Promise<string> {\n  return await tool('helm', '--kube-context', 'rancher-desktop', ...args);\n}\n\nexport async function retry<T>(proc: () => Promise<T>, options?: { delay?: number, tries?: number }): Promise<T> {\n  const delay = options?.delay ?? 500;\n  const tries = options?.tries ?? 30;\n\n  for (let i = 1; ; ++i) {\n    try {\n      return await proc();\n    } catch (ex) {\n      if (i >= tries) {\n        console.log(`${ tries } tries exceeding, failing.`);\n        throw ex;\n      }\n      console.error(`${ ex }, retrying... (${ i }/${ tries })`);\n      await util.promisify(setTimeout)(delay);\n    }\n  }\n}\n\nexport interface startRancherDesktopOptions {\n  /** Whether to use the mock backend; defaults to true. */\n  mock?:           boolean;\n  /** The environment to use. */\n  env?:            Record<string, string>;\n  /** Set to false if we want to see the first-run dialog (defaults to true). */\n  noModalDialogs?: boolean;\n  /** Maximum time in milliseconds to wait for the app to launch. */\n  timeout?:        number;\n  /** A suffix to be added to the log file, for variants. */\n  logVariant?:     string;\n}\n\n/**\n * Run Rancher Desktop; return promise that resolves to commonly-used\n * playwright objects when it has started.\n * @param testPath The path to the test file.\n * @param options Additional options; see type definition for details.\n */\nexport async function startRancherDesktop(testInfo: TestInfo, options: startRancherDesktopOptions = {}): Promise<ElectronApplication> {\n  currentTest = {\n    file: testInfo.file, options, startTime: Date.now(),\n  };\n  const { default: packageMeta } = await import('../../package.json', { with: { type: 'json' } });\n  const args = [\n    path.join(import.meta.dirname, '../..', packageMeta.main),\n    '--disable-gpu',\n    '--whitelisted-ips=',\n    // See pkg/rancher-desktop/utils/commandLine.ts before changing the next item as the final option.\n    '--disable-dev-shm-usage',\n  ];\n  const logsDir = reportAsset(testInfo, 'log');\n\n  await fs.promises.rm(logsDir, {\n    recursive: true, force: true, maxRetries: 3,\n  });\n  const launchOptions: Parameters<typeof _electron.launch>[0] = {\n    args,\n    env: {\n      ...process.env,\n      ...options?.env ?? {},\n      RD_LOGS_DIR: logsDir,\n      ...options?.mock ?? true ? { RD_MOCK_BACKEND: '1' } : {},\n    },\n  };\n\n  if (options?.noModalDialogs ?? true) {\n    args.push('--no-modal-dialogs');\n  }\n  if (options?.timeout) {\n    launchOptions.timeout = options?.timeout;\n  }\n  const electronApp = await _electron.launch(launchOptions);\n\n  await electronApp.context().tracing.start({ screenshots: true, snapshots: true });\n\n  return electronApp;\n}\n\nexport async function startSlowerDesktop(testInfo: TestInfo, defaultSettings: RecursivePartial<Settings> = {}): Promise<[ElectronApplication, Page]> {\n  const launchOptions: startRancherDesktopOptions = { mock: false };\n\n  createDefaultSettings(defaultSettings);\n  if (process.env.CI) {\n    launchOptions.timeout = 120_000; // default is 30_000 msec but the CI is very slow\n  }\n  const electronApp = await startRancherDesktop(testInfo, launchOptions);\n  const page = await electronApp.firstWindow();\n\n  return [electronApp, page];\n}\n"
  },
  {
    "path": "e2e/volumes.e2e.spec.ts",
    "content": "import { ElectronApplication, Page, expect, test } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { VolumesPage } from './pages/volumes-page';\nimport { startSlowerDesktop, teardown, tool } from './utils/TestUtils';\n\nimport { ContainerEngine } from '@pkg/config/settings';\n\nlet page: Page;\n\ntest.describe.serial('Volumes Tests', () => {\n  let electronApp: ElectronApplication;\n  let testVolumeName: string;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    [electronApp, page] = await startSlowerDesktop(testInfo, {\n      kubernetes:      { enabled: false },\n      containerEngine: { name: ContainerEngine.MOBY, allowedImages: { enabled: false } },\n    });\n\n    const navPage = new NavPage(page);\n    await navPage.progressBecomesReady();\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    if (testVolumeName) {\n      try {\n        await tool('docker', 'volume', 'rm', testVolumeName);\n      } catch (error) {}\n    }\n    await teardown(electronApp, testInfo);\n  });\n\n  test('should navigate to volumes page', async() => {\n    const navPage = new NavPage(page);\n    const volumesPage = await navPage.navigateTo('Volumes');\n\n    await expect(navPage.mainTitle).toHaveText('Volumes');\n    await volumesPage.waitForTableToLoad();\n  });\n\n  test('should display volume in the list', async() => {\n    const volumesPage = new VolumesPage(page);\n\n    testVolumeName = `test-volume-${ Date.now() }`;\n\n    try {\n      await tool('docker', 'volume', 'create', testVolumeName);\n    } catch (error) {\n      console.error('Failed to create test volume:', error);\n      throw error;\n    }\n\n    await page.reload();\n    await volumesPage.waitForTableToLoad();\n\n    await volumesPage.waitForVolumeToAppear(testVolumeName);\n  });\n\n  test('should show volume information', async() => {\n    const volumesPage = new VolumesPage(page);\n\n    await volumesPage.waitForVolumeToAppear(testVolumeName);\n\n    const volumeInfo = volumesPage.getVolumeInfo(testVolumeName);\n\n    await expect(volumeInfo.name).not.toBeEmpty();\n    await expect(volumeInfo.driver).not.toBeEmpty();\n    await expect(volumeInfo.mountpoint).not.toBeEmpty();\n  });\n\n  test('should browse volume files', async() => {\n    const volumesPage = new VolumesPage(page);\n\n    await volumesPage.browseVolumeFiles(testVolumeName);\n\n    await page.waitForURL(`**/volumes/files/${ testVolumeName }`, {\n      timeout: 10_000,\n    });\n\n    await page.goBack();\n    await volumesPage.waitForTableToLoad();\n  });\n\n  test('should delete volume', async() => {\n    const volumesPage = new VolumesPage(page);\n\n    await volumesPage.waitForVolumeToAppear(testVolumeName);\n    await expect(volumesPage.errorBanner).toBeHidden();\n    await page.waitForFunction(async() => {\n      return (await window.ddClient.docker.listContainers({ all: true })).length === 0;\n    });\n    await volumesPage.deleteVolume(testVolumeName);\n    await expect(volumesPage.errorBanner).toBeHidden();\n    await expect(volumesPage.getVolumeRow(testVolumeName)).toBeHidden({\n      timeout: 20_000,\n    });\n\n    testVolumeName = '';\n  });\n\n  test('should create multiple volumes for bulk operations', async() => {\n    const volumeNames = [\n      `test-bulk-volume-1-${ Date.now() }`,\n      `test-bulk-volume-2-${ Date.now() }`,\n      `test-bulk-volume-3-${ Date.now() }`,\n    ];\n\n    try {\n      for (const volumeName of volumeNames) {\n        await tool('docker', 'volume', 'create', volumeName);\n      }\n\n      await page.reload();\n      const volumesPage = new VolumesPage(page);\n      await volumesPage.waitForTableToLoad();\n      await expect(volumesPage.errorBanner).toBeHidden();\n\n      for (const volumeName of volumeNames) {\n        await volumesPage.waitForVolumeToAppear(volumeName);\n      }\n\n      await volumesPage.deleteBulkVolumes(volumeNames);\n      await expect(volumesPage.errorBanner).toBeHidden();\n\n      for (const volumeName of volumeNames) {\n        await expect(volumesPage.getVolumeRow(volumeName)).toBeHidden({\n          timeout: 10_000,\n        });\n      }\n\n      await page.reload();\n      await volumesPage.waitForTableToLoad();\n\n      for (const volumeName of volumeNames) {\n        await expect(volumesPage.getVolumeRow(volumeName)).toBeHidden();\n      }\n      await expect(volumesPage.errorBanner).toBeHidden();\n    } catch (error) {\n      for (const volumeName of volumeNames) {\n        try {\n          await tool('docker', 'volume', 'rm', volumeName);\n        } catch (cleanupError) {}\n      }\n      throw error;\n    }\n  });\n\n  test('should handle search functionality', async() => {\n    const volumesPage = new VolumesPage(page);\n\n    const searchVolumeName = `search-test-volume-${ Date.now() }`;\n\n    try {\n      await tool('docker', 'volume', 'create', searchVolumeName);\n\n      await page.reload();\n      await volumesPage.waitForTableToLoad();\n      await volumesPage.waitForVolumeToAppear(searchVolumeName);\n\n      await volumesPage.searchVolumes('search-test');\n\n      await expect(volumesPage.getVolumeRow(searchVolumeName)).toBeVisible();\n\n      const isPresent = await volumesPage.isVolumePresent(searchVolumeName);\n      expect(isPresent).toBe(true);\n\n      await volumesPage.searchVolumes('');\n    } finally {\n      try {\n        await tool('docker', 'volume', 'rm', searchVolumeName);\n      } catch (cleanupError) {}\n    }\n  });\n\n  test('should display error message in banner', async() => {\n    const volumesPage = new VolumesPage(page);\n    const volumeName = `test-volume-in-use-${ Date.now() }`;\n    const containerName = `test-container-${ Date.now() }`;\n\n    try {\n      await tool('docker', 'volume', 'create', volumeName);\n\n      // Create container that uses volume above\n      await tool(\n        'docker',\n        'run',\n        '--detach',\n        '--name',\n        containerName,\n        '-v',\n        `${ volumeName }:/data`,\n        'alpine',\n        'sleep',\n        'inf',\n      );\n\n      await page.reload();\n      await volumesPage.waitForTableToLoad();\n      await volumesPage.waitForVolumeToAppear(volumeName);\n\n      // Try to delete volume, results in error\n      await volumesPage.deleteVolume(volumeName);\n\n      await expect(volumesPage.errorBanner).toBeVisible();\n\n      await expect(volumesPage.errorBanner).toContainText(/volume is in use/i);\n\n      await expect(volumesPage.getVolumeRow(volumeName)).toBeVisible();\n    } finally {\n      try {\n        await tool('docker', 'rm', '-f', containerName);\n        await tool('docker', 'volume', 'rm', volumeName);\n      } catch (cleanupError) {}\n    }\n  });\n\n  test('should auto-refresh volumes list', async() => {\n    const volumesPage = new VolumesPage(page);\n    const autoRefreshVolumeName = `auto-refresh-test-${ Date.now() }`;\n\n    try {\n      await volumesPage.waitForTableToLoad();\n\n      // Remove all existing volumes to ensure clean state\n      try {\n        const existingVolumes = await tool('docker', 'volume', 'ls', '--quiet');\n        const volumeNames = existingVolumes.trim().split(/\\s+/);\n\n        if (volumeNames.length > 0) {\n          await tool('docker', 'volume', 'rm', '--force', ...volumeNames);\n        }\n      } catch {}\n      await expect(volumesPage.volumes).toHaveCount(0);\n\n      await tool('docker', 'volume', 'create', autoRefreshVolumeName);\n\n      await expect(volumesPage.getVolumeRow(autoRefreshVolumeName)).toBeVisible();\n\n      const volumeInfo = volumesPage.getVolumeInfo(autoRefreshVolumeName);\n      await expect(volumeInfo.name).not.toBeEmpty();\n      await expect(volumeInfo.driver).not.toBeEmpty();\n\n      await tool('docker', 'volume', 'rm', autoRefreshVolumeName);\n\n      await expect(\n        volumesPage.getVolumeRow(autoRefreshVolumeName),\n      ).toBeHidden();\n    } finally {\n      try {\n        await tool('docker', 'volume', 'rm', autoRefreshVolumeName);\n      } catch {}\n    }\n  });\n});\n"
  },
  {
    "path": "e2e/wsl-integrations.e2e.spec.ts",
    "content": "/**\n * This tests WSL integrations; it is a Windows-only test.\n */\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { expect, test } from '@playwright/test';\n\nimport { NavPage } from './pages/nav-page';\nimport { PreferencesPage } from './pages/preferences';\nimport { createDefaultSettings, retry, startRancherDesktop, teardown } from './utils/TestUtils';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\ntest.describe('WSL Integrations', () => {\n  test.describe.configure({ mode: 'serial' });\n  if (os.platform() !== 'win32') {\n    test.skip();\n  }\n\n  /** The directory containing our mock wsl.exe */\n  let workdir = '';\n  /** The environment variables, before our tests. */\n  let electronApp: ElectronApplication;\n  let page: Page;\n  let preferencesWindow: Page;\n\n  test.beforeAll(async() => {\n    const stubDir = path.resolve(import.meta.dirname, '..', 'src', 'go', 'mock-wsl');\n\n    workdir = await fs.promises.mkdtemp(\n      path.join(os.tmpdir(), 'rd-test-wsl-integration-'));\n    await fs.promises.mkdir(path.join(workdir, 'system32'));\n    await spawnFile('go',\n      ['build', '-o', path.join(workdir, 'system32', 'wsl.exe'), '.'], {\n        stdio: 'inherit',\n        cwd:   stubDir,\n        env:   {\n          ...process.env,\n          CGO_ENABLED: '1',\n        },\n      });\n  });\n\n  const writeConfig = async(opts?: Partial<Record<'alpha' | 'beta' | 'gamma', boolean | string>>) => {\n    const config: {\n      commands: {\n        args:     string[],\n        mode?:    string,\n        stdout?:  string,\n        stderr?:  string,\n        utf16le?: boolean,\n      }[]\n    } = {\n      commands: [\n        {\n          args:    ['--list', '--quiet'],\n          mode:    'repeated',\n          stdout:  ['alpha', 'beta', 'gamma'].join('\\n'),\n          utf16le: true,\n        },\n        {\n          args:   ['--list', '--verbose'],\n          mode:   'repeated',\n          stdout: [\n            '  NAME   STATE    VERSION',\n            '  alpha  Stopped  2',\n            '  beta   Stopped  2',\n            '  gamma  Stopped  2',\n            '',\n          ].join('\\n'),\n          utf16le: true,\n        },\n        ...['alpha', 'beta', 'gamma'].flatMap(distro => [\n          ...[['bin', 'docker-compose'], ['internal', 'wsl-helper']].flatMap(tool => ([\n            {\n              args:   ['--distribution', distro, '--exec', '/bin/wslpath', '-a', '-u', path.join(process.cwd(), 'resources', 'linux', ...tool)],\n              mode:   'repeated',\n              stdout: `/${ distro }/${ tool.join('/') }`,\n            }])),\n          ...[['bin', 'docker-buildx'], ['internal', 'wsl-helper']].flatMap(tool => ([\n            {\n              args:   ['--distribution', distro, '--exec', '/bin/wslpath', '-a', '-u', path.join(process.cwd(), 'resources', 'linux', ...tool)],\n              mode:   'repeated',\n              stdout: `/${ distro }/${ tool.join('/') }`,\n            }])),\n          ...[\n            [`/${ distro }/internal/wsl-helper`, 'kubeconfig', '--enable=false'],\n            [`/${ distro }/internal/wsl-helper`, 'kubeconfig', '--enable=true'],\n            ['/bin/sh', '-c', 'mkdir -p \"$HOME/.docker/cli-plugins\"'],\n            ['/bin/sh', '-c',\n              `if [ ! -e \"$HOME/.docker/cli-plugins/docker-compose\" -a ! -L \"$HOME/.docker/cli-plugins/docker-compose\" ] ; then\n                ln -s \"/${ distro }/bin/docker-compose\" \"$HOME/.docker/cli-plugins/docker-compose\" ;\n              fi`.replace(/\\s+/g, ' ')],\n            ['/bin/sh', '-c', 'mkdir -p \"$HOME/.docker/cli-plugins\"'],\n          ].map(cmd => ({\n            args: ['--distribution', distro, '--exec', ...cmd],\n            mode: 'repeated',\n          })),\n          {\n            args:   ['--distribution', distro, '--exec', '/bin/sh', '-c', 'readlink -f \"$HOME/.docker/cli-plugins/docker-buildx\"'],\n            mode:   'repeated',\n            stdout: '/dev/null',\n          },\n          {\n            args:   ['--distribution', distro, '--exec', '/bin/sh', '-c', 'readlink -f \"$HOME/.docker/cli-plugins/docker-compose\"'],\n            mode:   'repeated',\n            stdout: '/dev/null',\n          },\n          {\n            args:   ['--distribution', distro, '--user', 'root', '--exec', `/${ distro }/internal/wsl-helper`, 'docker-proxy', 'serve', '--verbose'],\n            mode:   'repeated',\n            stdout: '/dev/null',\n          },\n          {\n            args:   ['--distribution', distro, '--user', 'root', '--exec', `/${ distro }/internal/wsl-helper`, 'docker-proxy', 'kill', '--verbose'],\n            mode:   'repeated',\n            stdout: '/dev/null',\n          },\n        ]),\n        {\n          args:   ['--distribution', 'alpha', '--exec', '/alpha/internal/wsl-helper', 'kubeconfig', '--show'],\n          mode:   'repeated',\n          stdout: (opts?.alpha ?? false).toString(),\n        },\n        {\n          args:   ['--distribution', 'beta', '--exec', '/beta/internal/wsl-helper', 'kubeconfig', '--show'],\n          mode:   'repeated',\n          stdout: (opts?.beta ?? true).toString(),\n        },\n        {\n          args:   ['--distribution', 'gamma', '--exec', '/gamma/internal/wsl-helper', 'kubeconfig', '--show'],\n          mode:   'repeated',\n          stdout: (opts?.gamma ?? 'some error').toString(),\n        },\n        {\n          args: ['--distribution', 'rancher-desktop', '--exec', '/usr/local/bin/nerdctl', '--address',\n            '/run/k3s/containerd/containerd.sock', 'namespace', 'list', '--quiet'],\n          mode:   'repeated',\n          stdout: 'default',\n        },\n      ],\n    };\n\n    // Sometimes trying to update this file triggers an EBUSY error, so retry it.\n    await retry(() => {\n      return fs.promises.writeFile(path.join(workdir, 'config.json'), JSON.stringify(config, undefined, 2));\n    }, { delay: 500, tries: 20 });\n  };\n\n  // We need the beforeAll to allow initial Electron startup.\n  test.beforeAll(async() => await writeConfig());\n  test.beforeEach(async() => await writeConfig());\n  test.afterAll(async() => {\n    if (workdir) {\n      await fs.promises.rm(workdir, {\n        recursive:  true,\n        maxRetries: 5,\n      });\n    }\n  });\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    createDefaultSettings();\n\n    electronApp = await startRancherDesktop(testInfo, {\n      env: {\n        PATH:             path.join(workdir, 'system32') + path.delimiter + process.env.PATH,\n        RD_TEST_WSL_EXE:  path.join(workdir, 'system32', 'wsl.exe'),\n        RD_MOCK_WSL_DATA: path.join(workdir, 'config.json'),\n      },\n    });\n\n    const prefWindowPromise = electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n\n    page = await electronApp.firstWindow();\n    await new NavPage(page).preferencesButton.click();\n    preferencesWindow = await prefWindowPromise;\n  });\n  test.afterAll(({ colorScheme }, testInfo) => teardown(electronApp, testInfo));\n\n  test('should open preferences modal', async() => {\n    expect(preferencesWindow).toBeDefined();\n\n    // Wait for the window to actually load (i.e. transition from\n    // app://index.html/#/preferences to app://index.html/#/Preferences#general)\n    await preferencesWindow.waitForURL(/Preferences#/i);\n  });\n\n  test('should navigate to WSL and render integrations tab', async() => {\n    const { wsl } = new PreferencesPage(preferencesWindow);\n\n    await wsl.nav.click();\n\n    await expect(wsl.nav).toHaveClass('preferences-nav-item active');\n    await expect(wsl.tabIntegrations).toBeVisible();\n  });\n\n  test('should list integrations', async() => {\n    const { wsl: wslPage } = new PreferencesPage(preferencesWindow);\n\n    await wslPage.tabIntegrations.click();\n    await expect(wslPage.wslIntegrations).toBeVisible();\n\n    await expect(wslPage.wslIntegrations).toHaveCount(1, { timeout: 10_000 });\n    const wslIntegrationList = wslPage.tabIntegrations.getByTestId('wsl-integration-list');\n\n    expect(wslIntegrationList.getByText('alpha')).not.toBeNull();\n    expect(wslIntegrationList.getByText('beta')).not.toBeNull();\n    expect(wslIntegrationList.getByText('gamma')).not.toBeNull();\n  });\n\n  /*\n  test('should show checkbox states', (async() => {\n    const integrations = wslPage.wslIntegrations;\n    const alpha = integrations.find(item => item.name === 'alpha');\n    const beta = wslPage.getIntegration('beta');\n    const gamma = wslPage.getIntegration('gamma');\n\n    await expect(alpha.locator).toHaveCount(1);\n    await expect(alpha.checkbox).not.toBeChecked();\n    await expect(alpha.name).toHaveText('alpha');\n    await expect(alpha.error).not.toBeVisible();\n\n    await expect(beta.locator).toHaveCount(1);\n    await expect(beta.checkbox).toBeChecked();\n    await expect(beta.name).toHaveText('beta');\n    await expect(beta.error).not.toBeVisible();\n\n    await expect(gamma.locator).toHaveCount(1);\n    await expect(gamma.checkbox).not.toBeChecked();\n    await expect(gamma.name).toHaveText('gamma');\n    await expect(gamma.error).toHaveText('some error');\n  });\n\n  test('should allow enabling integration', async() => {\n    const { wsl: wslPage } = new PreferencesPage(preferencesWindow);\n    await wslPage.reload();\n    const integrations = wslPage.integrations;\n\n    await expect(integrations).toHaveCount(1, { timeout: 10_000 });\n\n    const alpha = wslPage.getIntegration('alpha');\n\n    await expect(alpha.checkbox).not.toBeChecked();\n    await alpha.assertEnabled();\n    await alpha.click();\n    await alpha.assertDisabled();\n    await writeConfig({ alpha: true });\n    await alpha.assertEnabled();\n    await expect(alpha.checkbox).toBeChecked();\n  });\n\n  test('should allow disabling integration', async() => {\n    await wslPage.reload();\n    const integrations = wslPage.integrations;\n\n    await expect(integrations).toHaveCount(1, { timeout: 10_000 });\n\n    const beta = wslPage.getIntegration('beta');\n\n    await expect(beta.checkbox).toBeChecked();\n    await beta.assertEnabled();\n    await beta.click();\n    await beta.assertDisabled();\n    await writeConfig({ beta: false });\n    await beta.assertEnabled();\n    await expect(beta.checkbox).not.toBeChecked();\n  });\n\n  test('should update invalid reason', async() => {\n    await wslPage.reload();\n    const integrations = wslPage.integrations;\n\n    await expect(integrations).toHaveCount(1, { timeout: 10_000 });\n\n    const gamma = wslPage.getIntegration('gamma');\n\n    await gamma.assertDisabled();\n    await expect(gamma.error).toHaveText('some error');\n    await writeConfig({ gamma: 'some other error' });\n\n    await page.reload();\n    const newGamma = (await navPage.navigateTo('WSLIntegrations')).getIntegration('gamma');\n\n    await expect(newGamma.error).toHaveText('some other error');\n    await newGamma.assertDisabled();\n  });\n */\n});\n"
  },
  {
    "path": "eslint.config.mts",
    "content": "import path from 'path';\n\nimport { includeIgnoreFile } from '@eslint/compat';\nimport eslint from '@eslint/js';\nimport { standardTypeChecked } from '@vue/eslint-config-standard-with-typescript';\nimport { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript';\nimport pluginVue from 'eslint-plugin-vue';\nimport globals from 'globals';\n\nexport default defineConfigWithVueTs(\n  eslint.configs.recommended,\n  pluginVue.configs['flat/recommended'],\n  standardTypeChecked.map(entry => {\n    // Avoid issues with redefining plugins:\n    // `Config \"typescript-eslint/base\": Key \"plugins\": Cannot redefine plugin \"@typescript-eslint\".`\n    if (entry.plugins) {\n      delete entry.plugins['@typescript-eslint'];\n    }\n    return entry;\n  }),\n  vueTsConfigs.recommendedTypeChecked,\n  vueTsConfigs.stylisticTypeChecked,\n  includeIgnoreFile(path.resolve('.gitignore')),\n  {\n    name:            'rancher-desktop',\n    languageOptions: {\n      sourceType: 'commonjs',\n    },\n    rules: {\n      '@stylistic/comma-dangle': ['error', 'always-multiline'],\n      '@stylistic/indent':       ['warn', 2, { SwitchCase: 0 }],\n      '@stylistic/key-spacing':  ['warn', {\n        align: {\n          beforeColon: false,\n          afterColon:  true,\n          on:          'value',\n          mode:        'minimum',\n        },\n        multiLine: {\n          beforeColon: false,\n          afterColon:  true,\n        },\n      }],\n      '@stylistic/no-multi-spaces': ['error', { ignoreEOLComments: true, exceptions: { Property: true, ImportAttribute: true, TSTypeAnnotation: true } }],\n      '@stylistic/quotes':          ['warn', 'single', { avoidEscape: true, allowTemplateLiterals: true }],\n      '@stylistic/semi':            ['error', 'always', {\n        omitLastInOneLineBlock:     true,\n        omitLastInOneLineClassBody: true,\n      }],\n      '@stylistic/space-before-function-paren': ['error', 'never'],\n      '@stylistic/space-in-parens':             'off',\n      '@stylistic/template-curly-spacing':      ['error', 'always'],\n      '@typescript-eslint/dot-notation':        'off',\n      '@typescript-eslint/no-base-to-string':   'off',\n      '@typescript-eslint/no-deprecated':       'error',\n      '@typescript-eslint/no-explicit-any':     'off', // We do need `any` sometimes\n      '@typescript-eslint/no-unused-vars':      ['warn', {\n        args: 'none', caughtErrors: 'none', ignoreRestSiblings: true, varsIgnorePattern: '^_.',\n      }],\n      '@typescript-eslint/only-throw-error':               'off',\n      '@typescript-eslint/prefer-nullish-coalescing':      'off',\n      '@typescript-eslint/prefer-promise-reject-errors':   'off',\n      '@typescript-eslint/no-redundant-type-constituents': 'off',\n      '@typescript-eslint/restrict-template-expressions':  'off',\n      '@typescript-eslint/unbound-method':                 'off',\n      'import-x/order':                                    ['error', {\n        alphabetize:                   { order: 'asc' },\n        groups:                        ['builtin', 'external', ['parent', 'sibling', 'index'], 'internal', 'object', 'type'],\n        'newlines-between':            'always',\n        pathGroupsExcludedImportTypes: ['builtin', 'object'],\n        pathGroups:                    [\n          {\n            pattern: '@pkg/**',\n            group:   'internal',\n          },\n        ],\n      }],\n      'new-cap':               'off',\n      // This one assumes all callbacks have errors in the first argument, which isn't likely.\n      'n/no-callback-literal': 'off',\n      'no-global-assign':      ['error', { exceptions: ['console'] }],\n      'vue/comma-dangle':      ['error', 'always-multiline'],\n    },\n  },\n  {\n    name:            'rancher-desktop-vue',\n    files:           ['**/*.vue'],\n    languageOptions: {\n      sourceType: 'module',\n    },\n  },\n  {\n    // The `no-useless-assignment` rule is catching `export const getters = ...` for some reason.\n    name:  'rancher-desktop-useless-assignment-in-store',\n    files: ['pkg/rancher-desktop/store/*.ts'],\n    rules: { 'no-useless-assignment': 'off' },\n  },\n  {\n    // Disable TypeScript-specific rules in JavaScript files.\n    name:  'rancher-desktop-js',\n    files: ['**/*.js', '**/*.cjs'],\n    rules: {\n      '@typescript-eslint/no-require-imports': ['off'],\n    },\n  },\n  {\n    // Disable lints not needed in tests (mostly global imports).\n    name:            'rancher-desktop-spec',\n    files:           ['**/*.spec.js', '**/*.spec.ts'],\n    languageOptions: {\n      globals: globals.jest,\n    },\n    rules: {\n      '@typescript-eslint/no-require-imports': 'off',\n      'import-x/first':                        'off', // Often needed for mocks\n    },\n  },\n  {\n    // Files we imported from Rancher Dashboard.\n    name:  'rancher-dashboard-imports',\n    files: [\n      'pkg/rancher-desktop/plugins/*.js',\n      'pkg/rancher-desktop/utils/*.js',\n    ],\n    languageOptions: {\n      globals: globals.browser,\n    },\n  },\n  {\n    // Files we imported from Rancher Dashboard.\n    name:  'rancher-dashboard-useless-assignments',\n    files: [\n      'pkg/rancher-desktop/components/SortableTable/*',\n      'pkg/rancher-desktop/store/*.js',\n      'pkg/rancher-desktop/utils/*.js',\n    ],\n    rules: {\n      'no-useless-assignment': 'off',\n    },\n  },\n  {\n    // Compatibility: disable lints during the ESLint transition.\n    name:    'rancher-desktop-compatibility',\n    extends: [\n      {\n        // Files we imported from Rancher Dashboard.\n        name:  'rancher-dashboard-imports',\n        files: [\n          'pkg/rancher-desktop/components/SortableTable/**',\n        ],\n        rules: {\n          'vue/eqeqeq': 'off',\n        },\n      },\n      {\n        // Files in workflows were previously excluded from linting\n        name:    'rancher-desktop-workflows',\n        ignores: ['.github/workflows/**'],\n      },\n    ],\n    rules: {\n      '@typescript-eslint/class-literal-property-style': 'off',\n      '@typescript-eslint/no-empty-function':            'off',\n      '@typescript-eslint/no-floating-promises':         'off',\n      '@typescript-eslint/no-misused-promises':          'off',\n      '@typescript-eslint/no-require-imports':           'off',\n      '@typescript-eslint/no-unsafe-enum-comparison':    'off',\n      '@typescript-eslint/no-unsafe-function-type':      'off',\n      '@typescript-eslint/no-unused-expressions':        'off',\n      '@typescript-eslint/no-unused-vars':               'off',\n      '@typescript-eslint/prefer-find':                  'off',\n      '@typescript-eslint/prefer-for-of':                'off',\n      'array-callback-return':                           'off',\n      'no-constant-binary-expression':                   'off',\n      'no-unreachable-loop':                             'off',\n      'no-use-before-define':                            'off',\n      'no-useless-escape':                               'off',\n      'prefer-rest-params':                              'off',\n      'prefer-spread':                                   'off',\n      'vue/block-lang':                                  'off',\n      'vue/component-definition-name-casing':            'off',\n      'vue/multi-word-component-names':                  'off',\n      'vue/no-deprecated-delete-set':                    'off',\n      'vue/no-reserved-component-names':                 'off',\n      'vue/no-side-effects-in-computed-properties':      'off',\n      'vue/no-v-for-template-key-on-child':              'off',\n      'vue/no-v-html':                                   'off',\n      'vue/order-in-components':                         'off',\n      'vue/require-explicit-emits':                      'off',\n    },\n  },\n);\n"
  },
  {
    "path": "go.work",
    "content": "go 1.25.0\n\nuse (\n\t./scripts\n\t./src/go/docker-credential-none\n\t./src/go/extension-proxy\n\t./src/go/guestagent\n\t./src/go/mock-wsl\n\t./src/go/nerdctl-stub\n\t./src/go/nerdctl-stub/generate\n\t./src/go/networking\n\t./src/go/rdctl\n\t./src/go/spin-stub\n\t./src/go/startup-profile\n\t./src/go/wsl-helper\n)\n"
  },
  {
    "path": "jest.config.js",
    "content": "// @ts-check\nimport { TS_EXT_TO_TREAT_AS_ESM, ESM_TS_TRANSFORM_PATTERN } from 'ts-jest';\n\n/** @type {import('jest').Config} */\nexport default {\n  transform: {\n    [ESM_TS_TRANSFORM_PATTERN]: ['ts-jest', { useESM: true }],\n    '^.+\\\\.vue$':               './pkg/rancher-desktop/utils/testUtils/vue-jest.js',\n  },\n  transformIgnorePatterns: [],\n  extensionsToTreatAsEsm:  [...TS_EXT_TO_TREAT_AS_ESM, '.vue'],\n  moduleFileExtensions:    [\n    'js',\n    'json',\n    'node', // For native modules, e.g. @napi-rs/xattr\n    'ts',\n    'vue',\n  ],\n  modulePathIgnorePatterns: [\n    '<rootDir>/dist',\n    '<rootDir>/pkg/rancher-desktop/dist',\n    '<rootDir>/.git',\n    '<rootDir>/e2e',\n    '<rootDir>/screenshots',\n  ],\n  moduleNameMapper: {\n    '\\\\.css$':       '<rootDir>/pkg/rancher-desktop/config/emptyStubForJSLinter.js',\n    '^@pkg/assets/': '<rootDir>/pkg/rancher-desktop/config/emptyStubForJSLinter.js',\n    '^@pkg/(.*)$':   '<rootDir>/pkg/rancher-desktop/$1',\n  },\n  setupFiles: [\n    '<rootDir>/pkg/rancher-desktop/utils/testUtils/setupVue.ts',\n  ],\n  testEnvironment:        'jsdom',\n  testEnvironmentOptions: {\n    customExportConditions: [\n      'node',\n      'node-addons',\n    ],\n  },\n  testPathIgnorePatterns: [\n    '<rootDir>/node_modules/',\n    '<rootDir>/pkg/rancher-desktop/sudo-prompt/',\n  ],\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rancher-desktop\",\n  \"productName\": \"Rancher Desktop\",\n  \"license\": \"Apache-2.0\",\n  \"version\": \"1.22.0\",\n  \"author\": {\n    \"name\": \"SUSE\",\n    \"email\": \"containers@suse.com\"\n  },\n  \"engines\": {\n    \"node\": \"^22.14.0\"\n  },\n  \"type\": \"module\",\n  \"packageManager\": \"yarn@4.9.4\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/rancher-sandbox/rancher-desktop.git\"\n  },\n  \"scripts\": {\n    \"dev\": \"node scripts/ts-wrapper.js scripts/dev.ts\",\n    \"lint\": \"yarn lint:fix\",\n    \"lint:fix\": \"yarn lint:typescript:fix && yarn lint:go:fix && yarn lint:spelling\",\n    \"lint:nofix\": \"yarn lint:typescript:nofix && yarn lint:go:nofix && yarn lint:spelling\",\n    \"lint:typescript:fix\": \"yarn lint:typescript:nofix --fix\",\n    \"lint:typescript:nofix\": \"node scripts/ts-wrapper.js scripts/lint-typescript.ts\",\n    \"lint:go:fix\": \"node scripts/ts-wrapper.js scripts/lint-go.ts --fix\",\n    \"lint:go:nofix\": \"node scripts/ts-wrapper.js scripts/lint-go.ts\",\n    \"lint:spelling\": \"bash scripts/spelling.sh\",\n    \"generate:nerdctl-stub\": \"powershell -ExecutionPolicy RemoteSigned scripts/windows/generate-nerdctl-stub.ps1\",\n    \"generate:extension-data\": \"node scripts/ts-wrapper.js scripts/extension-data.ts\",\n    \"build\": \"node scripts/ts-wrapper.js scripts/build.ts\",\n    \"package\": \"node scripts/ts-wrapper.js scripts/package.ts\",\n    \"sign\": \"node scripts/ts-wrapper.js scripts/sign.ts\",\n    \"wix\": \"node scripts/ts-wrapper.js scripts/wix.ts\",\n    \"test\": \"yarn lint:nofix && yarn test:unit && yarn test:extra\",\n    \"test:unit\": \"yarn test:unit:jest && yarn test:unit:nerdctl-stub && yarn test:unit:wsl-helper && yarn test:unit:rdctl\",\n    \"test:unit:jest\": \"cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 node --experimental-vm-modules node_modules/jest/bin/jest.js\",\n    \"test:unit:watch\": \"yarn test:unit -- --watch\",\n    \"test:unit:nerdctl-stub\": \"cd ./src/go/nerdctl-stub/ && go test ./...\",\n    \"test:unit:rdctl\": \"cd ./src/go/rdctl/ && go test ./...\",\n    \"test:unit:wsl-helper\": \"cd ./src/go/wsl-helper/ && go generate ./... && go test ./...\",\n    \"test:extra\": \"yarn test:extra:api-schema\",\n    \"test:extra:api-schema\": \"node scripts/ts-wrapper.js scripts/check-api-schema.ts\",\n    \"test:e2e\": \"node scripts/ts-wrapper.js scripts/e2e.ts\",\n    \"test:e2e:screenshots\": \"node scripts/ts-wrapper.js scripts/e2e.ts --config=screenshots/playwright-config.ts\",\n    \"postinstall\": \"node scripts/ts-wrapper.js scripts/postinstall.ts\",\n    \"postuninstall\": \"cross-env BROWSERSLIST_IGNORE_OLD_DATA=1 electron-builder install-app-deps\",\n    \"rddepman\": \"node scripts/ts-wrapper.js scripts/rddepman.ts\",\n    \"ucmonitor\": \"node scripts/ts-wrapper.js scripts/unreleased-change-monitor.ts\",\n    \"dcmonitor\": \"node scripts/ts-wrapper.js scripts/docker-cli-monitor.ts\",\n    \"screenshots\": \"yarn screenshots:light && yarn screenshots:dark\",\n    \"screenshots:dark\": \"cross-env THEME=dark yarn test:e2e:screenshots\",\n    \"screenshots:light\": \"cross-env THEME=light yarn test:e2e:screenshots\"\n  },\n  \"main\": \"dist/app/background.js\",\n  \"dependencies\": {\n    \"@docker/extension-api-client-types\": \"0.4.2\",\n    \"@kubernetes/client-node\": \"1.4.0\",\n    \"@napi-rs/xattr\": \"^1.0.3\",\n    \"@rancher/components\": \"0.3.0-alpha.1\",\n    \"@xterm/addon-fit\": \"0.11.0\",\n    \"@xterm/addon-search\": \"0.16.0\",\n    \"@xterm/addon-web-links\": \"0.12.0\",\n    \"@xterm/xterm\": \"6.0.0\",\n    \"async-mutex\": \"^0.5.0\",\n    \"cookie-universal\": \"2.2.2\",\n    \"cross-spawn\": \"7.0.6\",\n    \"dayjs\": \"1.11.20\",\n    \"dompurify\": \"3.3.3\",\n    \"electron-updater\": \"6.8.3\",\n    \"express\": \"5.2.1\",\n    \"floating-vue\": \"5.2.2\",\n    \"fs-extra\": \"11.3.4\",\n    \"http-proxy-middleware\": \"3.0.5\",\n    \"intl-messageformat\": \"11.1.2\",\n    \"jquery\": \"4.0.0\",\n    \"jsonpath-plus\": \"10.4.0\",\n    \"lodash\": \"4.17.23\",\n    \"marked\": \"17.0.4\",\n    \"native-reg\": \"1.1.1\",\n    \"node-forge\": \"1.3.3\",\n    \"proxy-agent\": \"^6.5.0\",\n    \"rancher-icons\": \"rancher/icons#v2.0.21\",\n    \"semver\": \"7.7.4\",\n    \"tar-stream\": \"3.1.8\",\n    \"vue\": \"3.5.30\",\n    \"vue-3-slider-component\": \"1.0.2\",\n    \"vue-router\": \"5.0.3\",\n    \"vue-select\": \"3.20.4\",\n    \"vuex\": \"4.1.0\",\n    \"which\": \"6.0.1\",\n    \"yaml\": \"2.8.2\"\n  },\n  \"devDependencies\": {\n    \"@babel/eslint-parser\": \"7.28.6\",\n    \"@babel/plugin-proposal-class-properties\": \"7.18.6\",\n    \"@babel/plugin-proposal-nullish-coalescing-operator\": \"7.18.6\",\n    \"@babel/plugin-proposal-optional-chaining\": \"7.21.0\",\n    \"@babel/plugin-proposal-private-methods\": \"7.18.6\",\n    \"@babel/plugin-proposal-private-property-in-object\": \"7.21.11\",\n    \"@electron/asar\": \"4.1.0\",\n    \"@electron/fuses\": \"^2.1.0\",\n    \"@electron/notarize\": \"3.1.1\",\n    \"@eslint/compat\": \"2.0.3\",\n    \"@eslint/js\": \"10.0.1\",\n    \"@playwright/test\": \"1.58.2\",\n    \"@types/cross-spawn\": \"6.0.6\",\n    \"@types/dompurify\": \"3.2.0\",\n    \"@types/ejs\": \"3.1.5\",\n    \"@types/jest\": \"30.0.0\",\n    \"@types/lodash\": \"4.17.24\",\n    \"@types/mustache\": \"4.2.6\",\n    \"@types/node\": \"22.19.15\",\n    \"@types/node-forge\": \"1.3.14\",\n    \"@types/plist\": \"3.0.5\",\n    \"@types/ps-tree\": \"1.1.6\",\n    \"@types/semver\": \"7.7.1\",\n    \"@types/tar-stream\": \"3.1.4\",\n    \"@types/which\": \"3.0.4\",\n    \"@vue/cli-plugin-babel\": \"5.0.9\",\n    \"@vue/cli-plugin-router\": \"5.0.9\",\n    \"@vue/cli-plugin-vuex\": \"5.0.9\",\n    \"@vue/cli-service\": \"5.0.9\",\n    \"@vue/compiler-sfc\": \"3.5.30\",\n    \"@vue/eslint-config-standard-with-typescript\": \"9.2.0\",\n    \"@vue/eslint-config-typescript\": \"14.7.0\",\n    \"@vue/test-utils\": \"2.4.6\",\n    \"@yarnpkg/cli\": \"^4.12.0\",\n    \"@yarnpkg/core\": \"^4.5.0\",\n    \"@yarnpkg/types\": \"^4.0.1\",\n    \"babel-core\": \"7.0.0-bridge.0\",\n    \"babel-jest\": \"30.3.0\",\n    \"babel-loader\": \"10.1.1\",\n    \"cross-env\": \"10.1.0\",\n    \"css-loader\": \"7.1.4\",\n    \"ejs\": \"5.0.1\",\n    \"electron\": \"41.0.2\",\n    \"electron-builder\": \"26.8.1\",\n    \"eslint\": \"10.0.3\",\n    \"eslint-plugin-vue\": \"10.8.0\",\n    \"extract-zip\": \"2.0.1\",\n    \"glob\": \"^13.0.3\",\n    \"globals\": \"17.4.0\",\n    \"jest\": \"30.3.0\",\n    \"jest-environment-jsdom\": \"30.3.0\",\n    \"js-yaml-loader\": \"1.2.2\",\n    \"mustache\": \"4.2.0\",\n    \"node-addon-api\": \"8\",\n    \"node-gyp\": \"12.2.0\",\n    \"node-gyp-build\": \"4.8.4\",\n    \"node-loader\": \"^2.1.0\",\n    \"octokit\": \"5.0.5\",\n    \"plist\": \"3.1.0\",\n    \"ps-tree\": \"1.2.0\",\n    \"raw-loader\": \"4.0.2\",\n    \"sass\": \"1.98.0\",\n    \"sass-loader\": \"16.0.7\",\n    \"ts-jest\": \"29.4.6\",\n    \"ts-loader\": \"^9.5.4\",\n    \"tsconfig-paths\": \"4.2.0\",\n    \"tsx\": \"4.21.0\",\n    \"typescript\": \"5.9.3\",\n    \"webpack\": \"5.100.2\"\n  },\n  \"dependenciesMeta\": {\n    \"electron\": {\n      \"built\": true\n    },\n    \"esbuild\": {\n      \"built\": true\n    },\n    \"native-reg\": {\n      \"built\": true\n    },\n    \"unrs-resolver\": {\n      \"built\": true\n    }\n  },\n  \"resolutions\": {\n    \"string-width\": \"^4\"\n  },\n  \"optionalDependencies\": {\n    \"dmg-license\": \"1.0.11\",\n    \"posix-node\": \"0.12.0\"\n  },\n  \"browserslist\": [\n    \"node 22\",\n    \"electron >= 35\"\n  ]\n}\n"
  },
  {
    "path": "packaging/electron-builder.yml",
    "content": "# copyright needs to stay in sync with message in About panel in background.ts\ncopyright: Copyright © 2021-2026 SUSE LLC\nproductName: Rancher Desktop\nicon: ./resources/icons/logo-square-512.png\nappId: io.rancherdesktop.app\nasar: true\nasarUnpack:\n- '**/*.node'\nelectronLanguages: [ en-US ]\nextraResources:\n- resources/\n- '!resources/darwin/lima*.tgz'\n- '!resources/darwin/qemu*.tgz'\n- '!resources/linux/lima*.tgz'\n- '!resources/linux/qemu*.tgz'\n- '!resources/linux/staging/'\n- '!resources/win32/staging/'\n- '!resources/host/'\n- '!resources/**/*.js.map'\nfiles:\n- dist/app/**/*\n- '!**/node_modules/*/prebuilds/!(${platform}*)/*.node'\nmac:\n  darkModeSupport: true\n  hardenedRuntime: true\n  gatekeeperAssess: false\n  icon: ./resources/icons/mac-icon.png\n  target: [ dmg, zip ]\n  identity: ~ # We sign in a separate step\n  extraFiles:\n  - build/signing-config-mac.yaml\n  - { from: dist/electron-builder.yaml, to: electron-builder.yml }\nwin:\n  target: [ zip ]\n  signtoolOptions:\n    signingHashAlgorithms: [ sha256 ] # We only support Windows 10 + WSL2\n  requestedExecutionLevel: asInvoker # The _app_ doesn't need privileges\n  extraFiles:\n  - build/wix/*\n  - build/license.rtf\n  - build/signing-config-win.yaml\n  - { from: dist/wix-custom-action.dll, to: wix-custom-action.dll }\n  - { from: dist/electron-builder.yaml, to: electron-builder.yml }\nlinux:\n  category: Utility\n  executableName: rancher-desktop\n  artifactName: ${name}-${version}-linux.zip\n  target: [ zip ]\npublish:\n  provider: custom\n  upgradeServer: https://desktop.version.rancher.io/v1/checkupgrade\n  vPrefixedTagName: true\n"
  },
  {
    "path": "packaging/linux/appimage.yml",
    "content": "app: rancher-desktop\n\nbuild:\n  packages:\n    - unzip\n    - ImageMagick\n    - libcairo2\n\nscript:\n  - rm -rf $BUILD_APPDIR/* && mkdir -p $BUILD_APPDIR/opt/rancher-desktop $BUILD_APPDIR/usr/share/metainfo $BUILD_APPDIR/usr/bin $BUILD_APPDIR/usr/lib64\n  - unzip $BUILD_SOURCE_DIR/rancher-desktop.zip -d $BUILD_APPDIR/opt/rancher-desktop\n  - chmod 04755 $BUILD_APPDIR/opt/rancher-desktop/chrome-sandbox\n  - mv $BUILD_APPDIR/opt/rancher-desktop/resources/resources/linux/rancher-desktop.desktop $BUILD_APPDIR\n  - convert -resize 512x512 $BUILD_APPDIR/opt/rancher-desktop/resources/resources/icons/logo-square-512.png $BUILD_APPDIR/rancher-desktop.png\n  - mv $BUILD_APPDIR/opt/rancher-desktop/resources/resources/linux/lima/bin/qemu-* $BUILD_APPDIR/usr/bin\n  - mv $BUILD_APPDIR/opt/rancher-desktop/resources/resources/linux/lima/share/qemu $BUILD_APPDIR/usr/share\n  - mv $BUILD_APPDIR/opt/rancher-desktop/resources/resources/linux/lima/lib $BUILD_APPDIR/usr\n  - cp /usr/lib64/libcairo* $BUILD_APPDIR/usr/lib64/\n  - ln -s ../../opt/rancher-desktop/rancher-desktop $BUILD_APPDIR/usr/bin/rancher-desktop\n  - ln -s ../share/qemu $BUILD_APPDIR/usr/bin/pc_bios\n"
  },
  {
    "path": "packaging/linux/flatpak.yaml",
    "content": "# This file is unused and just kept for reference for plain flatpak builds\nid: io.rancherdesktop.app\nbranch: main\nruntime: org.freedesktop.Platform\nruntime-version: '21.08'\nsdk: org.freedesktop.Sdk\nbase: org.electronjs.Electron2.BaseApp\nbase-version: '21.08'\nsdk-extensions:\n  # Not really needed since we are not building the app here\n  - org.freedesktop.Sdk.Extension.node14\ncommand: electron-wrapper\nseparate-locales: false\nfinish-args:\n  - --share=ipc\n  - --socket=x11\n  - --socket=wayland\n  - --share=network\n  - --device=dri\n  - --device=kvm\n  - --filesystem=xdg-config/rancher-desktop:create\n  - --filesystem=xdg-cache/rancher-desktop:create\n  - --filesystem=xdg-data/rancher-desktop:create\n  - --filesystem=home\n  - --talk-name=org.freedesktop.Notifications\n  - --own-name=org.kde.*\nrename-desktop-file: rancher-desktop.desktop\nrename-appdata-file: rancher-desktop.appdata.xml\nmodules:\n  - name: rancher-desktop\n    buildsystem: simple\n    sources:\n      - type: dir\n        path: .\n      - type: script\n        dest-filename: electron-wrapper\n        commands:\n          - |\n            export TMPDIR=\"$XDG_RUNTIME_DIR/app/$FLATPAK_ID\"\n\n            zypak-wrapper /app/lib/io.rancherdesktop.app/rancher-desktop \"$@\"\n    build-commands:\n      # Bundle electron build after yarn build -- --linux dir \n      - mkdir -p /app/lib/io.rancherdesktop.app\n      - unzip rancher-desktop.zip -d /app/lib/io.rancherdesktop.app\n      # Remove in app qemu binaries\n      - rm /app/lib/io.rancherdesktop.app/lib /app/lib/io.rancherdesktop.app/pc-bios /app/lib/io.rancherdesktop.app/qemu-* -rf\n      # Include FreeDesktop integration files at expected locations\n      - |\n        rm -rf /app/share/metainfo /app/share/icons /app/share/applications\n        mkdir -p /app/share/metainfo /app/share/applications\n\n        icon=\"/app/lib/io.rancherdesktop.app/resources/resources/icons/logo-square-512.png\"\n        for size in 512x512 256x256 128x128 96x96 64x64 48x48 32x32 24x24 16x16; do\n          mkdir \"/app/share/icons/hicolor/${size}/apps\" -p\n          ffmpeg -i \"${icon}\" -vf scale=\"${size}\" \"/app/share/icons/hicolor/${size}/apps/io.rancherdesktop.app.png\"\n        done\n\n        mv /app/lib/io.rancherdesktop.app/resources/resources/linux/rancher-desktop.desktop /app/share/applications\n        mv /app/lib/io.rancherdesktop.app/resources/resources/linux/rancher-desktop.appdata.xml /app/share/metainfo\n      # Install app wrapper\n      - install -Dm755 -t /app/bin/ electron-wrapper\n    modules:\n    - name: qemu\n      config-opts:\n      - \"--disable-user\"\n      - \"--disable-vnc\"\n      - \"--disable-sdl\"\n      - \"--disable-gtk\"\n      - \"--disable-curses\"\n      - \"--disable-iconv\"\n      - \"--disable-gio\"\n      - \"--enable-kvm\"\n      - \"--target-list=x86_64-softmmu\"\n      sources:\n      - type: archive\n        url: https://download.qemu.org/qemu-6.1.0.tar.xz\n        sha256: eebc089db3414bbeedf1e464beda0a7515aad30f73261abc246c9b27503a3c96\n"
  },
  {
    "path": "packaging/linux/rancher-desktop.appdata.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<component type=\"desktop\">\n  <id>rancher-desktop</id>\n  <metadata_license>CC0-1.0</metadata_license>\n  <project_license>Apache-2.0</project_license>\n  <name>Rancher Desktop</name>\n  <summary>Kubernetes and container management on the desktop</summary>\n  <description>\n    <p>\n      Rancher Desktop is an open-source project to bring Kubernetes and container management to the desktop\n    </p>\n  </description>\n  <provides>\n    <binary>rancher-desktop</binary>\n  </provides>\n  <releases>\n    <release version=\"\" date=\"\"/>\n  </releases>\n  <url type=\"homepage\">https://rancherdesktop.io/</url>\n  <url type=\"bugtracker\">https://github.com/rancher-sandbox/rancher-desktop/issues</url>\n</component>\n"
  },
  {
    "path": "packaging/linux/rancher-desktop.spec",
    "content": "#\n# spec file for package rancher-desktop\n#\n# Copyright (c) 2025 SUSE LLC\n#\n# All modifications and additions to the file contributed by third parties\n# remain the property of their copyright owners, unless otherwise agreed\n# upon. The license for this file, and modifications and additions to the\n# file, is the same license as for the pristine package itself (unless the\n# license for the pristine package is not an Open Source License, in which\n# case the license is the MIT License). An \"Open Source License\" is a\n# license that conforms to the Open Source Definition (Version 1.9)\n# published by the Open Source Initiative.\n\n# Please submit bugfixes or comments via https://bugs.opensuse.org/\n#\n\n\nName:       rancher-desktop\nVersion:    0\nRelease:    0\nSummary:    Kubernetes and container management on the desktop\nLicense:    Apache-2.0\nBuildRoot:  %{_tmppath}/%{name}-%{version}-build\nGroup:      Development/Tools/Other\nSource0:    %{name}.zip\nURL:        https://github.com/rancher-sandbox/rancher-desktop#readme\n\n%if \"%{_vendor}\" == \"debbuild\"\n# Needed to set Maintainer in output debs\nPackager:       SUSE <containers@suse.com>\n%endif\n\n%if 0%{?fedora} || 0%{?rhel}\n%global debug_package %{nil}\n%endif\n\nAutoReqProv:    no\n\nBuildRequires:  unzip\n%if 0%{?debian}\nBuildRequires:  imagemagick\n%else\nBuildRequires:  ImageMagick\n%endif\n\n%if 0%{?debian}\nRequires: qemu-utils\nRequires: qemu-system-x86\nRequires: pass\nRequires: openssh-client\n# To enumerate system certificates\nRequires: gnutls-bin\nRequires: libasound2\nRequires: libatk1.0-0\nRequires: libatk-bridge2.0-0\nRequires: libatspi2.0-0\nRequires: libc6\nRequires: libcairo2\nRequires: libcups2\nRequires: libdbus-1-3\nRequires: libdrm2\nRequires: libexpat1\nRequires: libgbm1\nRequires: libgcc1\nRequires: libgdk-pixbuf-2.0-0\nRequires: libglib2.0-0\nRequires: libglib2.0-dev\nRequires: libgtk-3-0\nRequires: libnspr4\nRequires: libnss3\nRequires: libpango-1.0-0\nRequires: libx11-6\nRequires: libxcb1\nRequires: libxcomposite1\nRequires: libxdamage1\nRequires: libxext6\nRequires: libxfixes3\nRequires: libxkbcommon0\nRequires: libxrandr2\n%else\nRequires: qemu\nRequires: openssh-clients\n\n%if 0%{?fedora} || 0%{?rhel}\nRequires: (pass or libsecret)\n%else\nRequires: (password-store or libsecret-1-0)\nRequires: qemu-img\n%endif\n\nRequires: glibc\nRequires: desktop-file-utils\n\n%if 0%{?fedora} || 0%{?rhel}\nRequires: libX11\nRequires: libXcomposite\nRequires: libXdamage\nRequires: libXext\nRequires: libXfixes\nRequires: libXrandr\nRequires: alsa-lib\nRequires: atk\nRequires: at-spi2-atk\nRequires: at-spi2-core\nRequires: cairo\nRequires: cups-libs\nRequires: dbus-libs\nRequires: libdrm\nRequires: expat\nRequires: mesa-libgbm\nRequires: libgcc\nRequires: gdk-pixbuf2\nRequires: glib\n# To enumerate system certificates\nRequires: gnutls-utils\nRequires: gtk3\nRequires: pango\nRequires: libxcb\nRequires: libxkbcommon\nRequires: nspr\nRequires: nss\n%else\n# To enumerate system certificates\nRequires: gnutls\nRequires: libX11-6\nRequires: libXcomposite1\nRequires: libXdamage1\nRequires: libXext6\nRequires: libXfixes3\nRequires: libXrandr2\nRequires: libasound2\nRequires: libatk-1_0-0\nRequires: libatk-bridge-2_0-0\nRequires: libatspi0\nRequires: libcairo2\nRequires: libcups2\nRequires: libdbus-1-3\nRequires: libdrm2\nRequires: libexpat1\nRequires: libgbm1\nRequires: libgcc_s1\nRequires: libgdk_pixbuf-2_0-0\nRequires: libgio-2_0-0\nRequires: libglib-2_0-0\nRequires: libgmodule-2_0-0\nRequires: libgobject-2_0-0\nRequires: libgtk-3-0\nRequires: libpango-1_0-0\nRequires: libxcb1\nRequires: libxkbcommon0\nRequires: mozilla-nspr\nRequires: mozilla-nss\n%endif\n\n%endif\n\n%description\nRancher Desktop is an open-source project to bring Kubernetes and container management to the desktop\n\n%prep\n%setup -c %{name} -n %{name}\n\n%build\n# Generate icons\nicon=\"resources/resources/icons/logo-square-512.png\"\nfor size in 512x512 256x256 128x128 96x96 64x64 48x48 32x32 24x24 16x16; do\n  mkdir \"share/icons/hicolor/${size}/apps\" -p\n  convert -resize \"${size}\" \"${icon}\" \"share/icons/hicolor/${size}/apps/%{name}.png\"\ndone\n\n# Desktop integration files\nmkdir -p share/applications share/metainfo\nmv resources/resources/linux/rancher-desktop.desktop share/applications/rancher-desktop.desktop\nmv resources/resources/linux/rancher-desktop.appdata.xml share/metainfo/rancher-desktop.appdata.xml\n\n# Remove qemu binaries included in lima tarball\nrm -v resources/resources/linux/lima/bin/qemu-*\nrm -rvf resources/resources/linux/lima/lib\nrm -rvf resources/resources/linux/lima/share/qemu\n\n%install\nmkdir -p \"%{buildroot}%{_prefix}/bin\" \"%{buildroot}/opt/%{name}\"\n\ncp -ra ./share \"%{buildroot}%{_prefix}\"\ncp -ra ./* \"%{buildroot}/opt/%{name}\"\n\n# Link to the binary\nln -sf \"/opt/%{name}/rancher-desktop\" \"%{buildroot}%{_bindir}/rancher-desktop\"\n\n%post\n# This is needed to ensure Debian packages have proper file permissions;\n# otherwise the postinst script is not generated correctly.\ntrue\n\n%files\n%defattr(-,root,root,-)\n%dir /opt/%{name}\n/opt/%{name}*\n%attr(4755,root,root) /opt/%{name}/chrome-sandbox\n%{_bindir}/rancher-desktop\n%{_prefix}/share/applications/rancher-desktop.desktop\n%{_prefix}/share/icons/hicolor/*\n%{_prefix}/share/metainfo/rancher-desktop.appdata.xml\n\n%changelog\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/dependencies.yaml",
    "content": "lima: 1.2.1.rd2\nqemu: 9.2.0.rd2\nsocketVMNet: 1.2.2\nalpineLimaISO:\n  isoVersion: 0.2.47.rd1\n  alpineVersion: 3.23.0\nWSLDistro: \"0.94\"\nkuberlr: 0.6.1\nhelm: 4.1.3\ndockerCLI: 29.3.0\ndockerBuildx: 0.32.1\ndockerCompose: 5.1.1\ngolangci-lint: 2.11.3\ntrivy: 0.69.3\nsteve: 0.1.0-beta9.1\nrancherDashboard: 2.11.1.rd3\ndockerProvidedCredentialHelpers: 0.9.5\nECRCredentialHelper: 0.12.0\nmobyOpenAPISpec: \"1.54\"\nwix: v3.14.1\nhostSwitch: 1.2.7\nmoproxy: 0.5.1\nspinShim: 0.23.0\nspinOperator: 0.6.1\ncertManager: 1.20.0\nspinCLI: 3.6.2\nspinKubePlugin: 0.4.0\ncheck-spelling: 0.0.25\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/extension-data.yaml",
    "content": "# Data generated by running `yarn generate:extension-data`. DO NOT EDIT.\n- slug: rancher/application-collection-extension\n  version: 0.5.2\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: 0.3.4\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/rancher-logo-cow-blue.svg\n    com.docker.extension.additional-urls: '[        {\"title\":\"Product\n      page\",\"url\":\"https://www.suse.com/products/rancher/application-collection\"},        {\"title\":\"Web\n      application\",\"url\":\"https://apps.rancher.io\"},        {\"title\":\"Documentation\",\"url\":\"https://docs.apps.rancher.io\"},        {\"title\":\"Support\",\"url\":\"https://github.com/rancherlabs/application-collection-extension/discussions\"}    ]'\n    com.docker.extension.categories: kubernetes,utility-tools\n    com.docker.extension.changelog: See full <a\n      href=\"https://github.com/rancherlabs/application-collection-extension/releases/tag/0.5.2\">change\n      log</a>.\n    com.docker.extension.detailed-description: \"        Build and run cloud-native\n      applications with SUSE's trusted, curated, and continuously updated\n      application collection.        <br />        This extension helps you\n      integrating the Collection into your local development environment\n      by:        <br />        <ul>            <li>Managing the authentication:\n      docker, helm and kubernetes credentials are automatically\n      configured</li>            <li>Making apps plug&play: installs the\n      workloads with a predefined set of values ready for local\n      deployment</li>            <li>Helping you stay up-to-date: detects\n      application updates and helps you through the update\n      process</li>        </ul>        <br />        Usage:        <br\n      />        <ol>            <li>Have a target kubernetes cluster configured\n      in your context</li>            <li>Install the\n      extension</li>            <li><a\n      href=\\\"https://docs.apps.rancher.io/get-started/authentication/#create-a-\\\n      personal-access-token\\\">Generate an access token</a> and configure the\n      authentication</li>            <li>Start deploying\n      workloads</li>        </ol>        \"\n    com.docker.extension.publisher-url: https://apps.rancher.io/\n    com.docker.extension.screenshots: '[        {\"alt\":\"Collection\",\n      \"url\":\"https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/01_collection.png\"},         {\"alt\":\"Application\n      details\",\n      \"url\":\"https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/02_application-details.png\"},         {\"alt\":\"Chart\n      values form\",\n      \"url\":\"https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/03_install-form.png\"},         {\"alt\":\"Workloads\",\n      \"url\":\"https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/04_workloads.png\"},         {\"alt\":\"Workload\n      details\",\n      \"url\":\"https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/05_workload-details.png\"}    ]'\n    com.suse.apps.main-package: nodejs-24\n    com.suse.bci.micro.authors: https://github.com/SUSE/bci/discussions\n    com.suse.bci.micro.created: 2025-12-11T11:54:43.169877214Z\n    com.suse.bci.micro.description: A micro environment for containers based on the\n      SUSE Linux Enterprise Base Container Image.\n    com.suse.bci.micro.disturl: obs://build.suse.de/SUSE:SLE-15-SP7:Update:CR/containers/a53d17097cf3a907bf42b29b24882459-micro-image\n    com.suse.bci.micro.eula: sle-bci\n    com.suse.bci.micro.lifecycle-url: https://www.suse.com/lifecycle#suse-linux-enterprise-server-15\n    com.suse.bci.micro.name: 15.7-52.6\n    com.suse.bci.micro.reference: registry.suse.com/bci/bci-micro:15.7-52.6\n    com.suse.bci.micro.release-stage: released\n    com.suse.bci.micro.source: https://sources.suse.com/SUSE:SLE-15-SP7:Update:CR/micro-image/a53d17097cf3a907bf42b29b24882459/\n    com.suse.bci.micro.supportlevel: l3\n    com.suse.bci.micro.title: SLE BCI 15 SP7 Micro\n    com.suse.bci.micro.until: 2031-07-31\n    com.suse.bci.micro.url: https://www.suse.com/products/base-container-images/\n    com.suse.bci.micro.vendor: SUSE LLC\n    com.suse.bci.micro.version: 15.7-52.6\n    com.suse.eula: \"\"\n    com.suse.lifecycle-url: \"\"\n    com.suse.release-stage: \"\"\n    com.suse.supportlevel: \"\"\n    com.suse.supportlevel.until: \"\"\n    io.artifacthub.package.logo-url: \"\"\n    io.artifacthub.package.readme-url: \"\"\n    org.openbuildservice.disturl: obs://build.suse.de/Devel:Orchid:Containers/containers/103eb70d1e78f2fd8ec1baa2fc66d3c2-nodejs-24\n    org.opencontainers.image.authors: \"\"\n    org.opencontainers.image.base.digest: sha256:7d103f4317c8c7eae4d0126d34c8b7a92769b44764a526a63325f0ca24150092\n    org.opencontainers.image.base.name: registry.suse.com/bci/bci-micro:15.7-52.6\n    org.opencontainers.image.created: 2025-12-12T14:06:19.962587777Z\n    org.opencontainers.image.description: Integrate the Application Collection into your development lifecycle\n    org.opencontainers.image.ref.name: 24.12.0-5.27\n    org.opencontainers.image.source: \"\"\n    org.opencontainers.image.title: SUSE Application Collection\n    org.opencontainers.image.url: https://apps.rancher.io/applications/nodejs\n    org.opencontainers.image.vendor: SUSE LLC\n    org.opencontainers.image.version: 24.12.0\n    org.opensuse.reference: registry.suse.com/bci/bci-micro:15.7-52.6\n  title: SUSE Application Collection\n  logo: https://raw.githubusercontent.com/rancherlabs/application-collection-extension/refs/heads/main/assets/rancher-logo-cow-blue.svg\n  publisher: SUSE LLC\n  short_description: Integrate the Application Collection into your development lifecycle\n- slug: ghcr.io/rancher-sandbox/rancher-desktop-rdx-ai-workbench\n  version: 0.2.0\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: 0.3.4\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/rancher-sandbox/rancher-desktop-rdx-ai-workbench/refs/tags/v0.2.0/workbench.svg\n    com.docker.extension.additional-urls: \"\"\n    com.docker.extension.categories: \"\"\n    com.docker.extension.changelog: \"\"\n    com.docker.extension.detailed-description: \"\"\n    com.docker.extension.publisher-url: \"\"\n    com.docker.extension.screenshots: \"\"\n    org.opencontainers.image.description: AI Workbench extension\n    org.opencontainers.image.title: AI Workbench\n    org.opencontainers.image.vendor: SUSE LLC\n  title: AI Workbench\n  logo: https://raw.githubusercontent.com/rancher-sandbox/rancher-desktop-rdx-ai-workbench/refs/tags/v0.2.0/workbench.svg\n  publisher: SUSE LLC\n  short_description: AI Workbench extension\n- slug: splatform/epinio-docker-desktop\n  version: 0.1.3\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.0\"\n    com.docker.desktop.extension.icon: https://epinio.io/images/icon-epinio.svg\n    com.docker.extension.additional-urls: '[{\"title\":\"Documentation\",\"url\":\"https://docs.epinio.io/\"},{\"title\":\"Issues\",\"url\":\"https://github.com/epinio/epinio/issues\"},{\"title\":\"CLI\",\"url\":\"https://github.com/epinio/epinio/releases\"},{\"title\":\"Slack\",\"url\":\"https://rancher-users.slack.com/?redir=%2Fmessages%2Fepinio\"}]'\n    com.docker.extension.detailed-description: <h1>The Application Development\n      Engine for Kubernetes</h1><h3>Tame your developer workflow to go from Code\n      to URL in one step.</h3>Epinio installs into any Kubernetes cluster to\n      bring your application from source code to deployment and allow for\n      Developers and Operators to work better together!\n    com.docker.extension.publisher-url: https://epinio.io\n    com.docker.extension.screenshots: '[{\"alt\": \"Epinio after Installation\", \"url\":\n      \"https://epinio.io/images/epinio-docker-desktop-screenshot.png\"}]'\n    org.opencontainers.image.description: Push from source to Kubernetes in one step\n    org.opencontainers.image.title: Epinio\n    org.opencontainers.image.vendor: Epinio by Krumware and SUSE\n  title: Epinio\n  logo: https://epinio.io/images/icon-epinio.svg\n  publisher: Epinio by Krumware and SUSE\n  short_description: Push from source to Kubernetes in one step\n- slug: julianb90/tachometer\n  version: 0.1.1\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: 0.3.4\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/julian-b90/tachometer/main/speedometer.png\n    com.docker.extension.additional-urls: '[{\"title\":\"Issues\",\"url\":\"https://github.com/julian-b90/tachometer/issues\"}]'\n    com.docker.extension.categories: development,utility-tools\n    com.docker.extension.changelog: \"<p>V 0.1.1 <br /> ### Changed <ul><li>update\n      dependencies</li><li>update node to latest LTS 20.17.0</li></ul></p>\"\n    com.docker.extension.detailed-description: Extension shows real-time cpu and memory usage of containers\n    com.docker.extension.publisher-url: https://github.com/julian-b90/tachometer\n    com.docker.extension.screenshots: '[{\"alt\":\"tachometer\",\n      \"url\":\"https://raw.githubusercontent.com/julian-b90/tachometer/main/screenshot.png\"},\n      {\"alt\":\"details view\",\n      \"url\":\"https://raw.githubusercontent.com/julian-b90/tachometer/main/screenshot_2.png\"}]'\n    org.opencontainers.image.description: Extension shows real-time cpu and memory usage of containers\n    org.opencontainers.image.title: Tachometer\n    org.opencontainers.image.vendor: julian-b90\n  title: Tachometer\n  logo: https://raw.githubusercontent.com/julian-b90/tachometer/main/speedometer.png\n  publisher: julian-b90\n  short_description: Extension shows real-time cpu and memory usage of containers\n- slug: docker/logs-explorer-extension\n  version: 0.2.5\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.3\"\n    com.docker.desktop.extension.icon: https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/icon.svg\n    com.docker.extension.additional-urls: \"[]\"\n    com.docker.extension.categories: utility-tools\n    com.docker.extension.changelog: \"     <ul>     <li>Fix missing logs on Windows.</li>     </ul>\"\n    com.docker.extension.detailed-description: \"<p>Logs Explorer provides deeper\n      insight into your logs.</p>     <h2 id=-features>✨ Key\n      features</h2>     <ul>     <li><strong>Multiple filters</strong>: You can\n      browse logs by status or log type, or you can select individual\n      containers.</li>     <li>     <strong>Advanced search\n      functionality</strong>:     <ul>     <li>You can search for logs that have\n      occurred after a certain amount of time or since a given\n      date.</li>     <li>You can use regular expressions or exact matches when\n      you search.</li>     <li>You can save each search query you enter into the\n      search bar to help narrow your search with “sticky” search filters. You\n      can save more than one query at a\n      time.</li>     </ul>     </li>     <li><strong>Improved scrolling\n      experience</strong>: When containers are running, scrolling is locked to\n      the bottom by default so you see the latest logs.</li>     </ul>     \"\n    com.docker.extension.publisher-url: https://www.docker.com/\n    com.docker.extension.screenshots: '[     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/1-all-containers.png\",\n      \"alt\": \"View logs from all the containers\"},     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/2-filters.png\",     \"alt\":\n      \"View logs matching a filter\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/2-collapsed-filters.png\",     \"alt\":\n      \"View logs with filter panel collapsed\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/3-expanded-rows.png\",     \"alt\":\n      \"Expand rows\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/3-log-type-status-filter.png\",     \"alt\":\n      \"View logs depending on the log type or the container\n      status\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/4-since-search-filter.png\",     \"alt\":\n      \"View logs since a duration\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/5-from-search-filter.png\",     \"alt\":\n      \"View logs from a time\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/6-tips.png\",     \"alt\":\n      \"Learn tips to search logs\"     },     {     \"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/0.2.1/7-all-containers-dark-mode.png\",     \"alt\":\n      \"Dark mode\"     }     ]'\n    org.opencontainers.image.description: View all your container logs in one place\n      so you can debug and troubleshoot faster.\n    org.opencontainers.image.revision: 8351516879c55dae0d7324ce49214568307306da\n    org.opencontainers.image.source: https://github.com/docker/logs-explorer-extension\n    org.opencontainers.image.title: Logs Explorer\n    org.opencontainers.image.vendor: Docker Inc.\n  title: Logs Explorer\n  logo: https://docker-extension-screenshots.s3.amazonaws.com/logs-explorer-extension/icon.svg\n  publisher: Docker Inc.\n  short_description: View all your container logs in one place so you can debug\n    and troubleshoot faster.\n- slug: prakhar1989/dive-in\n  version: 0.0.8\n  containerd_compatible: false\n  labels:\n    com.docker.desktop.extension.api.version: 0.3.0\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/prakhar1989/dive-in/main/scuba.svg\n    com.docker.extension.additional-urls: '[{\"title\":\"Documentation\",\"url\":\"https://github.com/prakhar1989/dive-in\"}]'\n    com.docker.extension.categories: utility-tools\n    com.docker.extension.changelog: First version\n    com.docker.extension.detailed-description: <p><h1>Dive In</h1>Explore docker\n      images, layer contents, and discover ways to shrink the size of your\n      Docker/OCI image.</p>\n    com.docker.extension.publisher-url: https://prakhar.me\n    com.docker.extension.screenshots: '[{\"alt\":\"main page\",\n      \"url\":\"https://github.com/prakhar1989/dive-in/blob/main/screenshots/1.png?raw=true\"},\n      {\"alt\":\"start containers\",\n      \"url\":\"https://github.com/prakhar1989/dive-in/blob/main/screenshots/2.png?raw=true\"}]'\n    org.opencontainers.image.description: Explore docker images, layer contents, and\n      discover ways to shrink the size of your Docker/OCI image.\n    org.opencontainers.image.title: Dive In\n    org.opencontainers.image.vendor: Prakhar Srivastav\n  title: Dive In\n  logo: https://raw.githubusercontent.com/prakhar1989/dive-in/main/scuba.svg\n  publisher: Prakhar Srivastav\n  short_description: Explore docker images, layer contents, and discover ways to\n    shrink the size of your Docker/OCI image.\n- slug: joycelin79/newman-extension\n  version: 0.0.7\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.0.1\"\n    com.docker.desktop.extension.icon: https://voyager.postman.com/icon/icon-newman-docker-orange-postman.svg\n    com.docker.extension.additional-urls: '[{\"title\":\"GitHub\n      Repository\",\"url\":\"https://github.com/loopDelicious/docker-extension\"},\n      {\"title\":\"Feedback and\n      issues\",\"url\":\"https://github.com/loopDelicious/docker-extension/issues\"},\n      {\"title\":\"Privacy\n      Policy\",\"url\":\"https://www.postman.com/legal/privacy-policy\"},{\"title\":\"Terms\n      of Service\",\"url\":\"https://www.postman.com/legal/terms\"}]'\n    com.docker.extension.changelog: <ul><li>Added metadata to provide more\n      information about the extension.</li></ul>\n    com.docker.extension.detailed-description: <h1>Newman</h1><p>The Postman\n      extension uses Newman to run collections on Docker Desktop. View results\n      of your API tests in a staging or production environment.</p><h1>Known\n      issues</h1><p>In some cases, depending on test collections and test\n      results, some buttons (expand/collapse folder, copy/paste) might be\n      inoperative.</p>\n    com.docker.extension.publisher-url: https://www.postman.com\n    com.docker.extension.screenshots: '[{\"alt\":\"View collection run results\",\n      \"url\":\"https://user-images.githubusercontent.com/17693714/200926587-cc817844-419d-4a39-abfa-e3b9b43881ea.png\"},\n      {\"alt\":\"Add Postman API key\",\n      \"url\":\"https://user-images.githubusercontent.com/17693714/200926910-a0fdba8d-02b0-4025-b7d6-95eb556eefa7.png\"},{\"alt\":\"Select\n      Postman collection to run\",\n      \"url\":\"https://user-images.githubusercontent.com/17693714/200926775-35e8dc5a-6c44-45de-80a8-f80430c81066.png\"}]'\n    org.opencontainers.image.description: Run your Postman collections from Docker Desktop.\n    org.opencontainers.image.title: Newman\n    org.opencontainers.image.vendor: Postman\n  title: Newman\n  logo: https://voyager.postman.com/icon/icon-newman-docker-orange-postman.svg\n  publisher: Postman\n  short_description: Run your Postman collections from Docker Desktop.\n- slug: docker/resource-usage-extension\n  version: 1.0.3\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: 0.3.0\n    com.docker.desktop.extension.icon: https://docker-extension-screenshots.s3.amazonaws.com/resource-usage-extension/icon.svg\n    com.docker.extension.additional-urls: \"\"\n    com.docker.extension.categories: utility-tools\n    com.docker.extension.changelog: Fix style on grid buttons\n    com.docker.extension.detailed-description: \"<p>With Resource Usage you\n      can:</p>     <ul>     <li>Find out which containers or Docker Compose\n      projects consume the most resources.</li>     <li>Monitor the evolution of\n      resource usage by containers over time.</li>     <li>See how much CPU,\n      Memory, Network and Disk space your containers are\n      using.</li>     </ul>     \"\n    com.docker.extension.publisher-url: https://www.docker.com/\n    com.docker.extension.screenshots: '[     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/resource-usage-extension/1-processes.png\",     \"alt\":\n      \"View all docker containers resources in a table\"},     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/resource-usage-extension/2-graphs.png\",     \"alt\":\n      \"View containers resource usage as graphs\"}     ]'\n    org.opencontainers.image.description: Monitor and manage live data stream for running containers.\n    org.opencontainers.image.revision: 1.0.3\n    org.opencontainers.image.source: https://github.com/docker/resource-usage-extension\n    org.opencontainers.image.title: Resource usage\n    org.opencontainers.image.vendor: Docker Inc.\n  title: Resource usage\n  logo: https://docker-extension-screenshots.s3.amazonaws.com/resource-usage-extension/icon.svg\n  publisher: Docker Inc.\n  short_description: Monitor and manage live data stream for running containers.\n- slug: anchore/docker-desktop-extension\n  version: 0.5.1\n  containerd_compatible: false\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.3\"\n    com.docker.desktop.extension.icon: https://user-images.githubusercontent.com/590471/164125752-b83d973c-f161-4d54-889f-352dee0ec795.svg\n    com.docker.extension.additional-urls: '[{\"title\":\"Support\",\"url\":\"https://github.com/anchore/docker-desktop-extension-support\"}]'\n    com.docker.extension.screenshots: '[{\"alt\": \"image listing\", \"url\":\n      \"https://user-images.githubusercontent.com/590471/164122365-efb150a9-3c97-42d2-bb46-7ba434fc21d2.png\"},{\"alt\":\n      \"package listing\", \"url\":\n      \"https://user-images.githubusercontent.com/590471/164122366-a7b89526-29c0-498c-b23b-d96667368637.png\"},{\"alt\":\n      \"vulnerability listing\", \"url\":\n      \"https://user-images.githubusercontent.com/590471/164122368-601d1ee2-a77d-4c0f-a98a-1aa68ea79d2a.png\"}]'\n    org.opencontainers.image.description: Content and security analysis for container images\n    org.opencontainers.image.title: anchore\n    org.opencontainers.image.vendor: Anchore Inc.\n  title: anchore\n  logo: https://user-images.githubusercontent.com/590471/164125752-b83d973c-f161-4d54-889f-352dee0ec795.svg\n  publisher: Anchore Inc.\n  short_description: Content and security analysis for container images\n- slug: ignatandrei/blockly-automation\n  version: 0.0.7\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.0\"\n    com.docker.desktop.extension.icon: https://github.com/ignatandrei/BlocklyAutomation/wiki/imgs/logoBADocker.png\n    com.docker.extension.additional-urls: '[{\"title\":\"Main\n      Page\",\"url\":\"https://github.com/ignatandrei/BlocklyAutomation/wiki/DockerExtension\"}]'\n    com.docker.extension.categories: development,testing-tools\n    com.docker.extension.changelog: <ul><li>first version</li></ul>\n    com.docker.extension.detailed-description: <h1>Description</h1><p>You can\n      automate pretty much every docker command. Press Execute to see in action\n      and LoadBlocks for more examples.</p>\n    com.docker.extension.publisher-url: https://github.com/ignatandrei/blocklyautomation\n    com.docker.extension.screenshots: '[{\"alt\":\"Load Demos\",\n      \"url\":\"https://raw.githubusercontent.com/wiki/ignatandrei/BlocklyAutomation/imgs/DockerExtension/LoadBlocks.png\"},\n      {\"alt\":\"start containers\",\n      \"url\":\"https://raw.githubusercontent.com/wiki/ignatandrei/BlocklyAutomation/imgs/DockerExtension/StartContainers.png\"}]'\n    org.opencontainers.image.description: A extension that displays lowCode with Blockly for any Docker command\n    org.opencontainers.image.title: Blockly Automation\n    org.opencontainers.image.vendor: Andrei Ignat\n  title: Blockly Automation\n  logo: https://github.com/ignatandrei/BlocklyAutomation/wiki/imgs/logoBADocker.png\n  publisher: Andrei Ignat\n  short_description: A extension that displays lowCode with Blockly for any Docker command\n- slug: docker/disk-usage-extension\n  version: 0.2.8\n  containerd_compatible: false\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.3\"\n    com.docker.desktop.extension.icon: https://docker-extension-screenshots.s3.us-east-1.amazonaws.com/disk-usage-extension/hard-drive.svg\n    com.docker.extension.additional-urls: \"[]\"\n    com.docker.extension.changelog: <ul>     <li>Adding 'Select all' button for\n      reclaimable space options.</li>     </ul>\n    com.docker.extension.detailed-description: \"<p>Disk Usage displays and\n      categorizes the disk space used by Docker. It also shows you how much of\n      the disk space is reclaimable and provides an easy one-click experience to\n      reclaim space.</p>     <h3 id=who-might-find-it-useful->Who might find it\n      useful?</h3>     <ul>     <li><p>If you need more visibility about how\n      much space Docker resources (e.g. images, volumes, etc) are using and how\n      much of it is reclaimable.</p>     </li>     <li><p>If you are looking for\n      a quick way to clean up disk space used by\n      Docker.</p>     </li>     </ul>     <h3 id=how-it-works>How it\n      works</h3>     <p>You can reclaim the disk space used by Docker by\n      removing:</p>     <ul>     <li>Stopped containers</li>     <li>Unused\n      images</li>     <li>Dangling images</li>     <li>Build\n      cache</li>     <li>Unused volumes</li>     </ul>     \"\n    com.docker.extension.publisher-url: https://www.docker.com/\n    com.docker.extension.screenshots: '[     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/disk-usage-extension/1-disk-usage.png\",\n      \"alt\": \"Disk usage stats\"},     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/disk-usage-extension/2-reclaim-popup.png\",\n      \"alt\": \"Reclaim space popup\"},     {\"url\":\n      \"https://docker-extension-screenshots.s3.amazonaws.com/disk-usage-extension/3-space-reclaimed.png\",\n      \"alt\": \"Space reclaimed successfully\"}     ]'\n    org.opencontainers.image.description: Optimize your disk space by removing unused objects from Docker Desktop.\n    org.opencontainers.image.revision: 929ff4d90be404fdf4325537286603d6c7c515ae\n    org.opencontainers.image.source: https://github.com/docker/disk-usage-extension\n    org.opencontainers.image.title: Disk Usage\n    org.opencontainers.image.vendor: Docker Inc.\n  title: Disk Usage\n  logo: https://docker-extension-screenshots.s3.us-east-1.amazonaws.com/disk-usage-extension/hard-drive.svg\n  publisher: Docker Inc.\n  short_description: Optimize your disk space by removing unused objects from Docker Desktop.\n- slug: harpooncorp/harpoon-ext\n  version: 0.0.6\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">=0.2.3\"\n    com.docker.desktop.extension.icon: https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/harpoon_logo_white_whale_transparent.png\n    com.docker.extension.additional-urls: '[{\"title\":\"Documentation\",\"url\":\"https://docs.harpoon.io/en/latest/introduction.html\"},\n      {\"title\":\"Features\",\"url\":\"https://docs.harpoon.io/en/latest/features.html\"},\n      {\"title\":\"Support\",\"url\":\"https://www.harpoon.io/contact\"}, {\"title\":\"Book\n      Demo\",\"url\":\"https://www.harpoon.io/demo\"}]'\n    com.docker.extension.categories: kubernetes\n    com.docker.extension.changelog: \"\"\n    com.docker.extension.detailed-description: harpoon is a drag and drop Kubernetes\n      tool for deploying any software in seconds. Our visual Kubernetes\n      interface enables anyone to deploy production-grade software with no code.\n      Whether you're new to Kubernetes and are looking for the best way to learn\n      or a seasoned pro, harpoon has all the features you need to be successful\n      in deploying and configuring your software using the industry-leading\n      container orchestrator, all with no code.\n    com.docker.extension.publisher-url: https://harpoon.io\n    com.docker.extension.screenshots: '[{\"alt\":\"harpoon project page dark mode\",\n      \"url\":\"https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/project-dark.png\"},\n      {\"alt\":\"harpoon project page light mode\",\n      \"url\":\"https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/project-light.png\"},\n      {\"alt\":\"harpoon home page dark mode\",\n      \"url\":\"https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/home-dark.png\"},\n      {\"alt\":\"harpoon home page light mode\",\n      \"url\":\"https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/home-light.png\"}]'\n    org.opencontainers.image.description: Docker Extension for the No Code Kubernetes platform\n    org.opencontainers.image.title: harpoon\n    org.opencontainers.image.vendor: harpoon Corp\n  title: harpoon\n  logo: https://docker-desktop-screenshots.s3.us-west-2.amazonaws.com/harpoon_logo_white_whale_transparent.png\n  publisher: harpoon Corp\n  short_description: Docker Extension for the No Code Kubernetes platform\n- slug: vklokun/docker-desktop-extension\n  version: 0.1.1\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.3\"\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/cncf/artwork/ec3936fa0256c768b538247d20f130d293a9faed/projects/kubescape/stacked/color/kubescape-stacked-color.svg\n    com.docker.extension.account-info: required\n    com.docker.extension.additional-urls: \"\"\n    com.docker.extension.categories: kubernetes,security\n    com.docker.extension.changelog: <p>Extension changelog<ul> <li>Support access\n      key required by ARMO platform</li><li>Update Kubescape helm chart\n      name</li></ul></p>\n    com.docker.extension.detailed-description: <h1>Kubescape Extension for Docker\n      Desktop</h1> <p>Kubescape helps harden your Kubernetes cluster by\n      providing insight into your cluster's security posture. Some of the\n      features that help you achieve this are - regular configuration and image\n      scans, visualizing your RBAC rules and suggesting automatic fixes where\n      applicable. </p> <p> The Kubescape Extension for Docker Desktop works by\n      installing the Kubescape in-cluster components, connecting them to ARMO\n      Platform and providing insights into the Kubernetes cluster deployed by\n      Docker Desktop via the dashboard on ARMO Platform.\n    com.docker.extension.publisher-url: https://cloud.armosec.io/\n    com.docker.extension.screenshots: '[ { \"alt\": \"Kubescape Extension for Docker\n      Desktop, Select Provider screen\", \"url\":\n      \"https://raw.githubusercontent.com/kubescape/docker-desktop-extension/main/docs/screenshots/dark-01.png\"\n      }, { \"alt\": \"Kubescape Extension for Docker Desktop, Sign Up screen\",\n      \"url\":\n      \"https://raw.githubusercontent.com/kubescape/docker-desktop-extension/main/docs/screenshots/dark-02.png\"\n      }, { \"alt\": \"Kubescape Extension for Docker Desktop, Secure Your Cluster\n      screen\", \"url\":\n      \"https://raw.githubusercontent.com/kubescape/docker-desktop-extension/main/docs/screenshots/dark-03.png\"\n      }, { \"alt\": \"Kubescape Extension for Docker Desktop, Cluster Secured\n      screen\", \"url\":\n      \"https://raw.githubusercontent.com/kubescape/docker-desktop-extension/main/docs/screenshots/dark-04.png\"\n      }, { \"alt\": \"Kubescape Extension for Docker Desktop, Monitor screen\",\n      \"url\":\n      \"https://raw.githubusercontent.com/kubescape/docker-desktop-extension/main/docs/screenshots/dark-05.png\"\n      } ]'\n    org.opencontainers.image.description: Secure your Kubernetes cluster and gain\n      insight into your cluster’s security posture via an easy-to-use online\n      dashboard.\n    org.opencontainers.image.licenses: Apache-2.0\n    org.opencontainers.image.title: Kubescape\n    org.opencontainers.image.vendor: ARMO\n  title: Kubescape\n  logo: https://raw.githubusercontent.com/cncf/artwork/ec3936fa0256c768b538247d20f130d293a9faed/projects/kubescape/stacked/color/kubescape-stacked-color.svg\n  publisher: ARMO\n  short_description: Secure your Kubernetes cluster and gain insight into your\n    cluster’s security posture via an easy-to-use online dashboard.\n- slug: caretdev/intersystems-extension\n  version: 0.1.7\n  containerd_compatible: true\n  labels:\n    com.docker.desktop.extension.api.version: \">= 0.2.3\"\n    com.docker.desktop.extension.icon: https://raw.githubusercontent.com/caretdev/docker-intersystems-extension/main/intersystems.svg\n    com.docker.extension.additional-urls: '[{\"title\":\"InterSystems\",\"url\":\"https://intersystems.com/\"},{\"title\":\"Support\",\"url\":\"https://github.com/caretdev/docker-intersystems-extension/issues\"},{\"title\":\"Discord\",\"url\":\"https://discord.gg/Bt5DUwJhdt\"}]'\n    com.docker.extension.categories: image-registry\n    com.docker.extension.changelog: \"\"\n    com.docker.extension.detailed-description: '<h1>InterSystems <a\n      href=\"http://containers.intersystems.com/\">Container Registry</a></h1>\n      <p>   This provides a new distribution channel for customers to access\n      container-based releases and previews. All Community   Edition images are\n      available in a public repository with no login required. All full released\n      images (IRIS, IRIS for   Health, Health Connect, System Alerting and\n      Monitoring, InterSystems Cloud Manager) and utility images (such\n      as   arbiter, Web Gateway, and PasswordHash) require a login token,\n      generated from your WRC (Worldwide Response Center)   account credentials.\n      </p>  <h1>Why do I need this Extension</h1> <p>   This extension provides\n      integrated UI for InterSystems Container Registry, so, you can easily\n      follow any updates, and quickly find and pull any images available on\n      Container Registry there. </p> <p>   Features available <ul>   <li>Observe\n      the list of available public images</li>   <li>Observe the list of\n      available private images for users with access to WRC</li>   <li>Easy\n      pulling images</li>   <li>Delete local images</li>   <li>Copy image name\n      with tag</li>   <li>OS Support     <ul>       <li>Windows\n      x86-64</li>       <li>Linux x86-64 and ARM64</li>       <li>macOS x86-64\n      and ARM64</li>     </ul>   </li>   <li>Filter images by name and\n      tag</li>   <li>Filter for ARM64 images</li> </ul> </p>  <h1>How to\n      use</h1>  <p>   It is already usable right after installation. And all\n      public images are already available. The list of images is cached, and it\n      is possible to refresh the list manually. </p> <p>   To get access to\n      private images with your WRC account, you have to go to <a\n      href=\"https://containers.intersystems.com\">https://containers.intersystems.com</a>\n      login there, and using provided token login in docker.   <pre>     docker\n      login -u=\"wrc_username\" -p=\"your_token\"\n      containers.intersystems.com   </pre>   After successful login, return to\n      the extension and press the Refresh button in the top right corner. </p>\n      <video controls name=\"media\" width=\"100%\" autoplay loop>   <source\n      src=\"https://raw.githubusercontent.com/caretdev/docker-intersystems-extension/main/img/InterSystems-Docker-Extension.mp4\"/>\n      </video>  <h1>Additionally</h1> <h2>InterSystems <a\n      href=\"https://community.intersystems.com\">Developer Community</a></h2>\n      <p>   InterSystems Developer Community is a global network of highly\n      experienced technology experts, influencers, and   thought leaders who\n      have expertise in InterSystems technologies. It’s a multilingual platform\n      both for InterSystems   employees, customers and partners. </p> '\n    com.docker.extension.publisher-url: https://github.com/caretdev/docker-intersystems-extension\n    com.docker.extension.screenshots: '[{\"url\":\"https://raw.githubusercontent.com/caretdev/docker-intersystems-extension/main/img/screenshot1.png\",\"alt\":\"Community\n      images\"},{\"url\":\"https://raw.githubusercontent.com/caretdev/docker-intersystems-extension/main/img/screenshot2.png\",\"alt\":\"Community\n      ARM64 images\"}]'\n    org.opencontainers.image.description: Convenient way to access InterSystems\n      Container Registry, public and private images of such products as IRIS and\n      IRIS for Health and many others in one place.\n    org.opencontainers.image.title: InterSystems\n    org.opencontainers.image.vendor: CaretDev Corp.\n  title: InterSystems\n  logo: https://raw.githubusercontent.com/caretdev/docker-intersystems-extension/main/intersystems.svg\n  publisher: CaretDev Corp.\n  short_description: Convenient way to access InterSystems Container Registry,\n    public and private images of such products as IRIS and IRIS for Health and\n    many others in one place.\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/lima-config.yaml",
    "content": "# Default Lima configuration; parts will be overridden in code.\n\n# Rancher Desktop ships with a patched QEMU that supports the Apple M4 CPU\n# So override Lima 1.0.3 falling back to cortex-a72.\ncpuType:\n  aarch64: host\n\nssh:\n  loadDotSSHPubKeys: false\nfirmware:\n  legacyBIOS: false\ncontainerd:\n  system: false\n  user: false\n# Provisioning scripts run on every boot, not just initial VM provisioning.\nprovision:\n- # When the ISO image is updated, only preserve selected data from /etc but otherwise use the new files.\n  # Update files in /usr/local on the data volume from the new versions on the ISO.\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    mkdir -p /bootfs\n    mount --bind / /bootfs\n    # /bootfs/etc is empty on first boot because it has been moved to /mnt/data/etc by lima\n    if [ -f /bootfs/etc/os-release ]; then\n      # Alpine turned /etc/os-release into a symlink; we dereference it again. If we still have a symlink\n      # here, then this is an upgrade from an older version and needs to go through the migration.\n      if [ -L /etc/os-release ] || ! diff -q /etc/os-release /bootfs/etc/os-release; then\n        # When we are upgrading from an ISO we will install new packages during boot.\n        # But Lima will restore the old /etc/apk/world file and run `apk fix --no-network`\n        # to restore packages from cache that were installed manually. This will however\n        # uninstall all packages again that were not previously installed because they are\n        # not listed in the old world file. We need to install them once more from the\n        # boot media to update the world file.\n        # We are not bothering with uninstalling packages that are not part of the new release.\n        apk add --no-network --keys-dir /bootfs/etc/apk/keys --repositories-file /bootfs/etc/apk/repositories \\\n          $(cat /bootfs/etc/apk/world)\n\n        # Using a temp file just in case dereferencing a symlink onto itself has a race condition.\n        cp -L /etc/os-release /etc/os-release.tmp\n        mv /etc/os-release.tmp /etc/os-release\n\n        cp /etc/machine-id /bootfs/etc\n        cp /etc/ssh/ssh_host* /bootfs/etc/ssh/\n        mkdir -p /etc/docker /etc/rancher\n        cp -pr /etc/docker /bootfs/etc\n        cp -pr /etc/rancher /bootfs/etc\n\n        rm -rf /mnt/data/etc.prev\n        mkdir /mnt/data/etc.prev\n        mv /etc/* /mnt/data/etc.prev\n        cp -L /bootfs/etc/os-release /tmp/os-release\n        mv /bootfs/etc/* /etc\n        mv /tmp/os-release /etc\n\n        # install updated files from /usr/local, e.g. nerdctl, buildkit, cni plugins\n        cp -pr /bootfs/usr/local /usr\n\n        # Keep the lima-init.log around for debugging    \n        cp /var/log/lima-init.log /mnt/data/lima-init-upgrade.log\n\n        # lima has applied changes while the \"old\" /etc was in place; restart to apply them to the updated one.\n        reboot\n      fi\n    fi\n    umount /bootfs\n    rmdir /bootfs\n- # make sure we booted with the right cgroup mode; k3s versions before 1.20.4 only support cgroup v1\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    RC_CGROUP_MODE=unified\n    if ! grep -q -E \"^#?rc_cgroup_mode=\\\"$RC_CGROUP_MODE\\\"\" /etc/rc.conf; then\n      sed -i -E \"s/^#?rc_cgroup_mode=\\\".*\\\"/rc_cgroup_mode=\\\"$RC_CGROUP_MODE\\\"/\" /etc/rc.conf\n      # avoid reboot loop if sed failed for any reason\n      if grep -q -E \"^rc_cgroup_mode=\\\"$RC_CGROUP_MODE\\\"\" /etc/rc.conf; then\n        reboot\n      fi\n    fi\n- # return unused space from the data volume back to the host\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    fstrim /mnt/data\n- # allow more than 10 sessions over the master control path\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    sed -i -E 's/^#?MaxSessions +[0-9]+/MaxSessions 25/g' /etc/ssh/sshd_config\n    rc-service --ifstarted sshd reload\n- # Persist /root directory on data volume\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    if ! [ -d /mnt/data/root ]; then\n      mkdir -p /root\n      mv /root /mnt/data/root\n    fi\n    mkdir -p /root\n    mount --bind /mnt/data/root /root\n- # Create /etc/docker/certs.d symlink\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    mkdir -p /etc/docker\n\n    # Delete certs.d if it is a symlink (from previous boot).\n    [ -L /etc/docker/certs.d ] && rm /etc/docker/certs.d\n\n    # Create symlink if certs.d doesn't exist (user may have created a regular directory).\n    if [ ! -e /etc/docker/certs.d ]; then\n      # We don't know if the host is Linux or macOS, so we take a guess based on which mountpoint exists.\n      if [ -d \"/Users/{{.User}}\" ]; then\n        ln -s \"/Users/{{.User}}/.docker/certs.d\" /etc/docker\n      elif [ -d \"/home/{{.User}}\" ]; then\n        ln -s \"/home/{{.User}}/.docker/certs.d\" /etc/docker\n      fi\n    fi\n- # Make sure hostname doesn't change during upgrade from earlier versions\n  mode: system\n  script: |\n    #!/bin/sh\n    hostname lima-rancher-desktop\n- # Clean up filesystems\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    # During boot is the only safe time to delete old k3s versions.\n    rm -rf /var/lib/rancher/k3s/data\n    # Delete all tmp files older than 3 days.\n    find /tmp -depth -mtime +3 -delete\n- # Make mount-points shared.\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit -o nounset -o xtrace\n    for dir in / /etc /tmp /var/lib; do\n      mount --make-shared \"${dir}\"\n    done\n- # This sets up cron (used for logrotate)\n  mode: system\n  script: |\n    #!/bin/sh\n    # Move logrotate to hourly, because busybox crond only handles time jumps up\n    # to one hour; this ensures that if the machine is suspended over long\n    # periods, things will still happen often enough.  This is idempotent.\n    mv -n /etc/periodic/daily/logrotate /etc/periodic/hourly/\n    rc-update add crond default\n    rc-service crond start\n- # Ensure the user is in the docker group to access the docker socket\n  mode: system\n  script: |\n    set -o errexit -o nounset -o xtrace\n    usermod --append --groups docker \"{{.User}}\"\n- # Install mkcert and prepare default/fallback cert for localhost\n  mode: system\n  script: |\n    export CAROOT=/run/mkcert\n    mkdir -p $CAROOT\n    cd $CAROOT\n    # Remove old mkcert certificates\n    rm -f /usr/local/share/ca-certificates/mkcert_development_CA_*.crt\n    mkcert -install\n    mkcert localhost\n    chown -R nobody:nobody $CAROOT\n- # Configure HTTPS_PROXY to OpenResty\n  mode: system\n  script: |\n    set -o errexit -o nounset -o xtrace\n\n    # openresty is backgrounding itself (and writes its own pid file)\n    sed -i 's/^command_background/#command_background/' /etc/init.d/rd-openresty\n\n    # configure proxy only when allowed-images exists\n    allowed_images_conf=/usr/local/openresty/nginx/conf/allowed-images.conf\n    # Remove the reference to an obsolete image conf filename\n    obsolete_image_allow_list_conf=/usr/local/openresty/nginx/conf/image-allow-list.conf\n    setproxy=\"[ -f $allowed_images_conf ] && supervise_daemon_args=\\\"-e HTTPS_PROXY=http://127.0.0.1:3128 \\${supervise_daemon_args:-}\\\" || true\"\n    for svc in containerd docker; do\n      sed -i \"\\#-f $allowed_images_conf#d\" /etc/init.d/$svc\n      sed -i \"\\#-f $obsolete_image_allow_list_conf#d\" /etc/init.d/$svc\n      echo \"$setproxy\" >> /etc/init.d/$svc\n    done\n\n    # Make sure openresty log directory exists\n    install -d -m755 /var/log/openresty\n- # mount bpffs to allow containers to leverage bpf, and make both bpffs and\n  # cgroupfs shared mounts so the pods can mount them correctly\n  mode: system\n  script: |\n    #!/bin/sh\n    set -o errexit\n\n    mount bpffs -t bpf /sys/fs/bpf\n    mount --make-shared /sys/fs/bpf\n    mount --make-shared /sys/fs/cgroup\n- # we run trivy as root now; remove any cached databases installed into the user directory by previous version\n  # trivy.db is 600M and trivy-java.db is 1.1G\n  mode: user\n  script: |\n    rm -rf \"${HOME}/.cache/trivy\"\nportForwards:\n- guestPortRange: [1, 65535]\n  guestIPMustBeZero: true\n  hostIP: \"0.0.0.0\"\n  proto: any\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/networks-config.yaml",
    "content": "# Path to socket_vmnet executable. Because socket_vmnet is invoked via sudo, it\n# must be installed where only root can modify/replace it. This means also none\n# of the parent directories should be writable by the user.\n#\n# The varRun directory also must not be writable by the user because it will\n# include the socket_vmnet pid file. socket_vmnet will be terminated via\n# sudo, so replacing the pid file would allow killing of arbitrary privileged\n# processes. varRun however MUST be writable by the daemon user.\n#\n# None of the paths segments may be symlinks, which is why it has to be /private/var\n# instead of /var etc.\npaths:\n  socketVMNet: /opt/rancher-desktop/bin/socket_vmnet\n  varRun: /private/var/run\n  sudoers: /private/etc/sudoers.d/zzzzz-rancher-desktop-lima\ngroup: everyone\nnetworks:\n  rancher-desktop-shared:\n    mode: shared\n    gateway: 192.168.205.1\n    dhcpEnd: 192.168.205.254\n    netmask: 255.255.255.0\n  host:\n    mode: host\n    gateway: 192.168.206.1\n    dhcpEnd: 192.168.206.254\n    netmask: 255.255.255.0\n  # We will add bridged-en0 etc. networks, one for each host interface.\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/10-flannel.conflist",
    "content": "{\n  \"name\":\"cbr0\",\n  \"cniVersion\":\"0.3.1\",\n  \"plugins\":[\n    {\n      \"type\":\"flannel\",\n      \"delegate\":{\n        \"hairpinMode\":true,\n        \"forceAddress\":true,\n        \"isDefaultGateway\":true\n      }\n    },\n    {\n      \"type\":\"portmap\",\n      \"capabilities\":{\n        \"portMappings\":true\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/buildkit.confd",
    "content": "# config file for /etc/init.d/buildkit\n\n# overrides the main command executed by the supervise daemon\nbuildkitd_command=\"/usr/local/bin/buildkitd\"\n\n# any other options you want to pass to buildkitd_command\nbuildkitd_opts=\"--addr=unix:///run/buildkit/buildkitd.sock --containerd-worker=true --containerd-worker-addr=/run/k3s/containerd/containerd.sock --containerd-worker-gc --oci-worker=false\"\n\n# Settings for process limits (ulimit)\n#ulimit_opts=\"-c unlimited -n 1048576 -u unlimited\"\n\n# seconds to wait for sending SIGTERM and SIGKILL signals when stopping buildkitd\n#signal_retry=\"TERM/60/KILL/10\"\n\n# where buildkit stdout (and perhaps stderr) goes.\n#log_file=\"/var/log/buildkit.log\"\n\n# where buildkit stderr optionally goes.\n# if this is not set, the value in 'logfile' is used\n#err_file=\"/var/log/buildkit-err.log\"\n\n# mode of the log files\n#log_mode=0644\n\n# user that owns the log files (no group root on WSL)\nlog_owner=root\n\n# to override the default supervise_daemon_args\n#supervise_daemon_opts=\"\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/buildkit.initd",
    "content": "#!/sbin/openrc-run\nsupervisor=supervise-daemon\n\nname=\"BuildKit Daemon\"\ndescription=\"Standalone buildkitd\"\n\ncommand=\"${buildkitd_command:-/usr/bin/buildkitd}\"\ncommand_args=\"${buildkitd_opts:---oci-worker=false --containerd-worker=true}\"\nrc_ulimit=\"${ulimit_opts:--c unlimited -n 1048576 -u unlimited}\"\nretry=\"${signal_retry:-TERM/60/KILL/10}\"\n\nlog_file=\"${log_file:-/var/log/${RC_SVCNAME}.log}\"\nerr_file=\"${err_file:-${log_file}}\"\nlog_mode=\"${log_mode:-0644}\"\nlog_owner=\"${log_owner:-root}\"\n\nsupervise_daemon_args=\"${supervise_daemon_opts:---stderr \\\"${err_file}\\\" --stdout \\\"${log_file}\\\"}\"\n\nstart_pre() {\n\tcheckpath -f -m \"$log_mode\" -o \"$log_owner\" \"$log_file\" \"$err_file\"\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/cert-manager.yaml",
    "content": "---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: cert-manager\n---\napiVersion: helm.cattle.io/v1\nkind: HelmChart\nmetadata:\n  name: cert-manager\n  namespace: kube-system\nspec:\n  chart: \"https://%{KUBERNETES_API}%/static/rancher-desktop/cert-manager.tgz\"\n  targetNamespace: cert-manager\n  # Old versions of the helm-controller don't support createNamespace, so we\n  # created the namespace ourselves.\n  createNamespace: false\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/configure-allowed-images",
    "content": "#!/bin/sh\n\n# This script configures the VM for the allowed-images feature.\n\n# shellcheck shell=ash\n\nset -o errexit -o nounset\n\n# Create nobody user and group for nginx.\naddgroup -S -g 65534 nobody 2>/dev/null || true\nadduser -S -D -H -h /dev/null -s /sbin/nologin -u 65534 -G nobody -g nobody nobody 2>/dev/null || true\n\n# Install mkcert and create default certs for localhost.\nexport CAROOT=/run/mkcert\nmkdir -p $CAROOT\ncd $CAROOT\n# Remove old mkcert certificates\nrm -f /usr/local/share/ca-certificates/mkcert_development_CA_*.crt\nmkcert -install\nmkcert localhost\nchown -R nobody:nobody $CAROOT\n\n# configure proxy only when allowed-images exists\nallowed_images_conf=/usr/local/openresty/nginx/conf/allowed-images.conf\nsetproxy=\"[ -f $allowed_images_conf ] && supervise_daemon_args=\\\"-e HTTPS_PROXY=http://127.0.0.1:3128 \\${supervise_daemon_args:-}\\\" || true\"\nsed -i \"\\#-f $allowed_images_conf#d\" /etc/init.d/containerd\necho \"$setproxy\" >> /etc/init.d/containerd\n\n# openresty is backgrounding itself (and writes its own pid file)\nsed -i 's/^command_background/#command_background/' /etc/init.d/rd-openresty\n\n# Make sure openresty log directory exists\ninstall -d -m755 /var/log/openresty\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/docker-credential-rancher-desktop",
    "content": "#!/bin/sh\n\nset -eu\n\nsource /etc/rancher/desktop/credfwd\n\nDATA=\"@-\"\n# The \"list\" command doesn't have a payload on STDIN\n[ \"$1\" = \"list\" ] && DATA=\"\"\n\n# $CREDFWD_CURL_OPTS is intentionally *not* quoted\nexec curl --silent --user \"$CREDFWD_AUTH\" --data \"$DATA\" --noproxy '*' --fail-with-body ${CREDFWD_CURL_OPTS:-} \"$CREDFWD_URL/$1\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/install-containerd-shims",
    "content": "#!/bin/sh\n\nset -o errexit -o nounset -o pipefail\n\ndest=/usr/local/containerd-shims\n\n# Copy all shims into the data volume so they become part of snapshots.\n# TODO Maybe use rsync to avoid copying files repeatedly?\nmkdir -p \"$dest\"\nfor dir in \"$@\"; do\n  if [[ \"$(uname -a)\" =~ microsoft ]]; then\n    dir=$(wslpath -a -u \"$dir\")\n  fi\n  cp \"${dir}/containerd-shim-\"* \"$dest\" || :\ndone\n\n# Make sure all shims are executable.\nfor file in \"${dest}/\"*; do\n  if [ -e \"$file\" ]; then\n    chmod 755 \"$file\"\n  fi\ndone\n\n# Create symlinks to each shim into /usr/local/bin.\n# In the future this will enable us putting only shims from an allow list on the PATH.\nfind /usr/local/bin -type l -name 'containerd-shim-*' -delete\nfind \"$dest\" -type f -exec ln -sf {} /usr/local/bin \\;\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/install-k3s",
    "content": "#!/bin/sh\n\nset -o errexit -o nounset -o pipefail\n\nif [ -n \"${XTRACE:-}\" ]; then\n    set -o xtrace\nfi\n\nVERSION=\"${1}\"\nCACHE_DIR=\"${CACHE_DIR:-${2}}\"\n\n# Update symlinks for k3s and images to new version\nK3S_DIR=\"${CACHE_DIR}/${VERSION}\"\nif [ ! -d \"${K3S_DIR}\" ]; then\n    echo \"Directory ${K3S_DIR} does not exist\"\n    exit 1\nfi\n\n# Make sure any outdated kubeconfig file is gone\nmkdir -p /etc/rancher/k3s\nrm -f /etc/rancher/k3s/k3s.yaml\n\nK3S=k3s\nARCH=amd64\nif [ \"$(uname -m)\" = \"aarch64\" ]; then\n    K3S=k3s-arm64\n    ARCH=arm64\nfi\n\n# Add images\nIMAGES=\"/var/lib/rancher/k3s/agent/images\"\nmkdir -p \"${IMAGES}\"\nIMAGEPATH=\"${K3S_DIR}/k3s-airgap-images-${ARCH}\"\nif [ -f \"${IMAGEPATH}.tar.zst\" ]; then\n    ln -s -f \"${IMAGEPATH}.tar.zst\" \"${IMAGES}\"\nfi\nif [ -f \"${IMAGEPATH}.tar\" ]; then\n    ln -s -f \"${IMAGEPATH}.tar\" \"${IMAGES}\"\nfi\n# Add k3s binary\nln -s -f \"${K3S_DIR}/${K3S}\" /usr/local/bin/k3s\n# The file system may be readonly (on macOS)\nchmod a+x \"${K3S_DIR}/${K3S}\" || true\n\n# Make sure any old manifests are removed before configuring k3s again.\n# All Rancher Desktop manifest have a name like z123-foo-bar, so only delete\n# those. That way provisioning scripts can still create other manifests.\n# We need to create the directory before we run `k3s server ...` because\n# we install additional manifests that k3s will install during startup.\nMANIFESTS=/var/lib/rancher/k3s/server/manifests\nrm -rf \"${MANIFESTS}/z\"[0-9]*\nmkdir -p \"$MANIFESTS\"\n\nSTATIC=/var/lib/rancher/k3s/server/static/rancher-desktop\nrm -rf \"$STATIC\"\nmkdir -p \"$STATIC\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/install-wsl-helpers",
    "content": "#!/bin/sh\n\n# This script installs WSL helpers into the shared WSL mount at `/mnt/wsl`.\n# Usage: $0 <path to nerdctl-stub>\n\n# shellcheck shell=ash\n\nset -o errexit -o nounset\n\n# The nerdctl shim must be setuid root to be able to create bind mounts within\n# /mnt/wsl so that nerdctl can see it.\nmkdir -p \"/mnt/wsl/rancher-desktop/bin/\"\ncp \"${1}\" \"/mnt/wsl/rancher-desktop/bin/nerdctl\"\nchmod u+s \"/mnt/wsl/rancher-desktop/bin/nerdctl\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/k3s-containerd-config.toml",
    "content": "version = 2\n\nroot = \"/var/lib/rancher/k3s/agent/containerd\"\nstate = \"/run/k3s/containerd\"\n\n[grpc]\n  address = \"/run/k3s/containerd/containerd.sock\"\n\n[plugins.\"io.containerd.internal.v1.opt\"]\n  path = \"/var/lib/rancher/k3s/agent/containerd\"\n\n[plugins.\"io.containerd.grpc.v1.cri\"]\n  stream_server_address = \"127.0.0.1\"\n  stream_server_port = \"10010\"\n  enable_selinux = false\n  enable_unprivileged_ports = true\n  enable_unprivileged_icmp = true\n  sandbox_image = \"rancher/mirrored-pause:3.6\"\n\n[plugins.\"io.containerd.grpc.v1.cri\".containerd]\n  snapshotter = \"overlayfs\"\n  disable_snapshot_annotations = true\n\n[plugins.\"io.containerd.grpc.v1.cri\".cni]\n  bin_dir = \"/usr/libexec/cni\"\n  conf_dir = \"/etc/cni/net.d\"\n\n[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc]\n  runtime_type = \"io.containerd.runc.v2\"\n\n[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.runc.options]\n  SystemdCgroup = false\n\n[plugins.\"io.containerd.grpc.v1.cri\".registry]\n  config_path = \"/var/lib/rancher/k3s/agent/etc/containerd/certs.d\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/logrotate-k3s",
    "content": "/var/log/k3s.log {\n  missingok\n  notifempty\n  copytruncate\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/logrotate-lima-guestagent",
    "content": "/var/log/lima-guestagent.log {\n  missingok\n  notifempty\n  copytruncate\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/logrotate-openresty",
    "content": "/var/log/openresty/*.log {\n\tmissingok\n\tsharedscripts\n\tpostrotate\n\t\t/etc/init.d/rd-openresty --quiet --ifstarted reopen\n\tendscript\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/moproxy.initd",
    "content": "#!/sbin/openrc-run\n\nname=moproxy\n\ndescription=\"A transparent TCP to SOCKSv5/HTTP proxy.\"\n\nextra_started_commands=\"enable disable reload\"\ndescription_enable=\"Start redirecting the network traffic to the HTTP proxy.\"\ndescription_disable=\"Stop redirecting the network traffic to the HTTP proxy.\"\ndescription_reload=\"Reload the proxy list.\"\n\n# TCP Listen address\n: ${host:=${MOPROXY_HOST:-\"::\"}}\n# TCP Listen port\n: ${port:=${MOPROXY_PORT:-\"2080\"}}\n# List of backend proxy servers\n: ${proxy_list:=${MOPROXY_PROXYLIST:-\"/etc/moproxy/proxy.ini\"}}\n# Additional arguments to pass to moproxy\n: ${moproxy_args:=${MOPROXY_ARGS:-\"\"}}\n# Override this argument to disable the use of TLS SNI\n: ${moproxy_remotedns:=${MOPROXY_REMOTE_DNS:-\"--remote-dns\"}}\n# Comma-separated list of port traffic to redirect to moproxy\n: ${ports_redirected:=${MOPROXY_REDIRECTED_PORT:-\"80,443\"}}\n# Comma-separated list of hostname to not redirect to the proxy\n: ${noproxy_rules:=${MOPROXY_NOPROXY:-\"0.0.0.0/8,10.0.0.0/8,127.0.0.0/8,169.254.0.0/16,172.16.0.0/12,192.168.0.0/16,224.0.0.0/4,240.0.0.0/4\"}}\n\ncommand=\"'${MOPROXY_BINARY:-/usr/sbin/moproxy}'\"\ncommand_args=\"--host ${host} --port ${port} ${moproxy_remotedns} --list ${proxy_list} ${moproxy_args}\"\ncommand_background=\"yes\"\npidfile=\"/run/${name}.pid\"\n\nMOPROXY_LOGFILE=\"${MOPROXY_LOGFILE:-${LOG_DIR:-/var/log}/${RC_SVCNAME}.log}\"\noutput_log=\"'${MOPROXY_LOGFILE}'\"\nerror_log=\"'${MOPROXY_LOGFILE}'\"\n\niptables_redirect_to_moproxy_chain() {\n\tiptables --table nat --$1 $2 --protocol tcp --match multiport --dports \"${ports_redirected}\" --jump MOPROXY\n}\n\niptables_redirect() {\n\tiptables --table nat --append MOPROXY --protocol tcp --jump REDIRECT --to-port \"${port}\"\n}\n\niptables_accept() {\n\tiptables --table nat --append MOPROXY --protocol tcp --destination \"$1\" --jump ACCEPT\n}\n\nadd_noproxy_rules() {\n\tfor i in ${noproxy_rules//,/ }\n\tdo\n\t\tiptables_accept \"$i\"\n\tdone\n}\n\nenable_redirection_to_moproxy_chain() {\n\tif ! iptables_redirect_to_moproxy_chain check $1 &> /dev/null\n\tthen\n\t\tiptables_redirect_to_moproxy_chain append $1\n\telse\n\t\teinfo \"Rule already in table\"\n\tfi\n}\n\ndisable_redirection_to_moproxy_chain() {\n\twhile iptables_redirect_to_moproxy_chain check $1  &> /dev/null\n\tdo\n\t\tiptables_redirect_to_moproxy_chain delete $1\n\tdone\n}\n\ncreate_moproxy_chain() {\n\tiptables --table nat --new MOPROXY\n}\n\ndelete_moproxy_chain() {\n\tiptables --table nat --flush MOPROXY\n\tiptables --table nat --delete-chain MOPROXY\n}\n\ndepend() {\n\tafter iptables ip6tables\n}\n\nenable() {\n\teinfo \"Starting the iptables rules to start redirection of ports ${ports_redirected} to ${name}\"\n\tcreate_moproxy_chain\n\tadd_noproxy_rules\n\tiptables_redirect\n\tenable_redirection_to_moproxy_chain OUTPUT\n\tenable_redirection_to_moproxy_chain PREROUTING\n}\n\ndisable() {\n\teinfo \"Removing all the iptables rules to stop redirection to ${name}\"\n\tdisable_redirection_to_moproxy_chain PREROUTING\n\tdisable_redirection_to_moproxy_chain OUTPUT\n\tdelete_moproxy_chain\n}\n\nstart_post() {\n\tenable\n}\n\nstop_pre() {\n\tdisable\n}\n\nreload() {\n\tebegin \"Reloading ${name}\"\n\tstart-stop-daemon --signal HUP --pidfile \"$pidfile\"\n\tiptables --table nat --flush MOPROXY\n\tadd_noproxy_rules\n\tiptables_redirect\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/nerdctl",
    "content": "#!/bin/sh\nexport CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock\nif [ -f /usr/local/openresty/nginx/conf/allowed-images.conf ]; then\n  export HTTPS_PROXY=http://127.0.0.1:3128\nfi\n\n# On WSL, we need to enter the correct pid &c. namespace for nerdctl to work\n# correctly.\n\nif [ -r /run/wsl-init.pid ]; then\n  parent=\"$(cat /run/wsl-init.pid)\"\n  pid=\"$(ps -o pid,ppid,comm | awk '$2 == \"'\"${parent}\"'\" && $3 == \"init\" { print $1 }')\"\n  if [ -n \"${pid}\" ]; then\n    exec /usr/bin/nsenter -p -m -n -t \"${pid}\" /usr/local/libexec/nerdctl/nerdctl \"$@\"\n  fi\nfi\n\nexec /usr/local/libexec/nerdctl/nerdctl \"$@\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/nginx.conf",
    "content": "worker_processes  auto;\n\nerror_log /var/log/openresty/error.log warn;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    map_hash_bucket_size 128;\n    include       mime.types;\n    default_type  application/octet-stream;\n\n    log_format proxy escape=json\n      '{'\n        '\"access_time\":\"$time_local\",'\n        '\"request\":\"$request\",'\n        '\"status\":\"$status\",'\n        '\"bytes_sent\":\"$body_bytes_sent\",'\n        '\"host\":\"$host\",'\n        '\"ssl_protocol\":\"$ssl_protocol\",'\n        '\"connect_host\":\"$connect_host\",'\n        '\"connect_port\":\"$connect_port\",'\n      '}';\n\n    log_format mitm escape=json\n      '{'\n        '\"access_time\":\"$time_local\",'\n        '\"method\":\"$request_method\",'\n        '\"uri\":\"$uri\",'\n        '\"status\":\"$status\",'\n        '\"bytes_sent\":\"$body_bytes_sent\",'\n        '\"upstream_response_time\":\"$upstream_response_time\",'\n        '\"host\":\"$host\",'\n        '\"http_host\":\"$http_host\",'\n        '\"upstream\":\"$upstream_addr\"'\n      '}';\n\n    server {\n        listen 3128;\n        listen [::]:3128;\n        server_name proxy;\n\n        access_log /var/log/openresty/proxy.log proxy;\n\n        proxy_connect;\n        proxy_connect_allow all;\n        proxy_connect_address 127.0.0.1:3129;\n        proxy_max_temp_file_size 0;\n\n        # response non-CONNECT requests\n        location / {\n            add_header \"Content-type\" \"text/plain\" always;\n            return 404 \"The Rancher Desktop allowed-images proxy only allows CONNECT requests\\n\";\n        }\n    }\n\n    map \"$http_host$uri\" $forbidden {\n        default 1;\n        include allowed-images.conf;\n    }\n\n    # don't limit maximum request size to allow for pushing large image layers\n    client_max_body_size 0;\n\n    server {\n        listen 3129 ssl default_server;\n        server_name mitm;\n\n        access_log /var/log/openresty/access.log mitm;\n\n        # nginx complains if these are not set; we'll clear them again right after\n        ssl_certificate /run/mkcert/localhost.pem;\n        ssl_certificate_key /run/mkcert/localhost-key.pem;\n\n        ssl_certificate_by_lua_block {\n            local ssl = require \"ngx.ssl\"\n            local name = ssl.server_name()\n\n            local ok, err = ssl.clear_certs()\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to clear existing (fallback) certificates\")\n                return ngx.exit(ngx.ERROR)\n            end\n\n            local certs_dir = \"/run/mkcert/\"\n            local cert_file = certs_dir .. name .. \".pem\"\n            local key_file = certs_dir .. name .. \"-key.pem\"\n\n            local my_load_certificate_chain = function()\n                local f = io.open(cert_file, \"rb\")\n                if f == nil then\n                    local ngx_pipe = require \"ngx.pipe\"\n                    local cmd = { \"/usr/bin/mkcert\", \"-cert-file\", cert_file, \"-key-file\", key_file, name }\n                    local opts = { environ = {\"CAROOT=\"..certs_dir}, merge_stderr = true }\n                    local proc, err = ngx_pipe.spawn(cmd, opts)\n                    if proc == nil then\n                        ngx.log(ngx.ERR, \"failed to spawn mkcert command: \", err)\n                        return ngx.exit(ngx.ERROR)\n                    end\n\n                    local data, err, partial = proc:stdout_read_all()\n                    local ok, reason, status = proc:wait()\n                    if not ok then\n                        ngx.log(ngx.ERR, \"failed to create cert for \", name, \" reason: \", reason, \" status: \", status)\n                        ngx.log(ngx.ERR, \"  output: \", data, \" err: \", err, \" partial: \", partial)\n                        return ngx.exit(ngx.ERROR)\n                    end\n                    f = io.open(cert_file, \"rb\")\n                end\n                local a = f:read(\"a*\")\n                f:close()\n                return a\n            end\n            local pem_cert_chain = assert(my_load_certificate_chain())\n\n            local der_cert_chain, err = ssl.cert_pem_to_der(pem_cert_chain)\n            if not der_cert_chain then\n                ngx.log(ngx.ERR, \"failed to convert certificate chain from PEM to DER: \", err)\n                return ngx.exit(ngx.ERROR)\n            end\n\n            local ok, err = ssl.set_der_cert(der_cert_chain)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to set DER cert: \", err)\n                return ngx.exit(ngx.ERROR)\n            end\n\n            local my_load_private_key = function()\n                local f = assert(io.open(key_file, \"rb\"))\n                local a = f:read(\"a*\")\n                f:close()\n                return a\n            end\n            local pem_pkey = assert(my_load_private_key())\n\n            local der_pkey, err = ssl.priv_key_pem_to_der(pem_pkey, nil)\n            if not der_pkey then\n                ngx.log(ngx.ERR, \"failed to convert private key from PEM to DER: \", err)\n                return ngx.exit(ngx.ERROR)\n            end\n\n            local ok, err = ssl.set_der_priv_key(der_pkey)\n            if not ok then\n                ngx.log(ngx.ERR, \"failed to set DER private key: \", err)\n                return ngx.exit(ngx.ERROR)\n            end\n        }\n\n        # We need to resolve the real names of our proxied servers.\n        include resolver.conf;\n\n        # Docker needs this. Don't ask.\n        chunked_transfer_encoding on;\n\n        proxy_read_timeout 900;\n\n        # Use SNI during the TLS handshake with the upstream.\n        proxy_ssl_server_name on;\n\n        proxy_ssl_verify on;\n        proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;\n        proxy_ssl_verify_depth 2;\n\n        location ~ ^/v[12]/(.+)/manifests/([^/]+)$ {\n            if ($forbidden) {\n                add_header \"Content-type\" \"application/json\" always;\n                # `code` from https://github.com/distribution/distribution/blob/main/registry/api/errcode/register.go\n                return 403 \"{\\\"errors\\\":[{\\\"code\\\":\\\"UNAUTHORIZED\\\",\\\"message\\\":\\\"image $http_host/$1:$2 is not covered by the Rancher Desktop allowed-images list\\\"}]}\\n\";\n            }\n            proxy_pass https://$http_host;\n        }\n\n        location / {\n            proxy_pass https://$http_host;\n        }\n    }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/rancher-desktop-guestagent.initd",
    "content": "#!/sbin/openrc-run\n# shellcheck shell=ksh\n\ndepend() {\n  after network-online\n}\n\nGUESTAGENT_LOGFILE=\"${GUESTAGENT_LOGFILE:-${LOG_DIR:-/var/log}/${RC_SVCNAME}.log}\"\n\nsupervisor=supervise-daemon\nname=\"Rancher Desktop Guest Agent\"\ncommand=/usr/local/bin/rancher-desktop-guestagent\ncommand_args=\"\n  ${GUESTAGENT_ADMIN_INSTALL:+-adminInstall=${GUESTAGENT_ADMIN_INSTALL}}\n  ${GUESTAGENT_KUBERNETES:+-kubernetes=${GUESTAGENT_KUBERNETES}}\n  ${GUESTAGENT_DOCKER:+-docker=${GUESTAGENT_DOCKER}}\n  ${GUESTAGENT_CONTAINERD:+-containerd=${GUESTAGENT_CONTAINERD}}\n  ${GUESTAGENT_K8S_SVC_ADDR:+-k8sServiceListenerAddr=${GUESTAGENT_K8S_SVC_ADDR}}\n  ${GUESTAGENT_DEBUG:+-debug}\n  \"\ncommand_args=\"${command_args//$'\\n'/ }\"\noutput_log=\"'${GUESTAGENT_LOGFILE}'\"\nerror_log=\"'${GUESTAGENT_LOGFILE}'\"\n\nrespawn_delay=5\nrespawn_max=0\n\nstart_pre() {\n  cat > /etc/logrotate.d/guestagent <<EOF\n  ${GUESTAGENT_LOGFILE} {\n    missingok\n    notifempty\n  }\nEOF\n}\n\n# shellcheck disable=SC2163\nif [ -f /etc/environment ]; then\n    while read -r line; do\n        # pam_env implementation:\n        # - '#' is treated the same as newline; terminates value\n        # - skip leading tabs and spaces\n        # - skip leading \"export \" prefix (only single space)\n        # - skip leading quote ('\\'' or '\"') on the value side\n        # - skip trailing quote only if leading quote has been skipped;\n        #   quotes don't need to match; trailing quote may be omitted\n        line=\"$(echo \"$line\" | sed -E \"s/^[ \\\\t]*(export )?//; s/#.*//; s/(^[^=]+=)[\\\"'](.*[^\\\"'])?[\\\"']?$/\\1\\2/\")\"\n        if [ -n \"$line\" ]; then\n          export \"$line\"\n        fi\n    done </etc/environment\nfi\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/service-cri-dockerd.initd",
    "content": "#!/sbin/openrc-run\n\n# This script is used to manage cri-dockerd via OpenRC.\n\n# shellcheck shell=ksh\n\nname=\"cri-dockerd\"\ndescription=\"Rancher Desktop Shim for Docker Engine\"\n\n\nsupervisor=supervise-daemon\ncommand=/usr/local/bin/cri-dockerd\ncommand_args=\"--container-runtime-endpoint unix:///run/cri-dockerd.sock --network-plugin=cni --cni-bin-dir=/usr/libexec/cni --cni-conf-dir=/etc/cni/net.d  --cni-cache-dir=/var/lib/cni/cache\"\n\nCRI_DOCKERD_LOGFILE=\"${CRI_DOCKERD_LOGFILE:-${LOG_DIR:-/var/log}/${RC_SVCNAME}.log}\"\noutput_log=\"'${CRI_DOCKERD_LOGFILE}'\"\nerror_log=\"'${CRI_DOCKERD_LOGFILE}'\"\n\ndepend() {\n  need docker\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/service-k3s.initd",
    "content": "#!/sbin/openrc-run\n\n# This script is used to manage k3s via OpenRC.\n\n# shellcheck shell=ksh\n\n# shellcheck disable=SC2163\nif [ -f /etc/environment ]; then\n    while read -r line; do\n        # pam_env implementation:\n        # - '#' is treated the same as newline; terminates value\n        # - skip leading tabs and spaces\n        # - skip leading \"export \" prefix (only single space)\n        # - skip leading quote ('\\'' or '\"') on the value side\n        # - skip trailing quote only if leading quote has been skipped;\n        #   quotes don't need to match; trailing quote may be omitted\n        line=\"$(echo \"$line\" | sed -E \"s/^[ \\\\t]*(export )?//; s/#.*//; s/(^[^=]+=)[\\\"'](.*[^\\\"'])?[\\\"']?$/\\1\\2/\")\"\n        if [ -n \"$line\" ]; then\n          export \"$line\"\n        fi\n    done </etc/environment\nfi\n\n# ENGINE configuration variable is either \"moby\" or \"containerd\"\nENGINE=\"${ENGINE:-containerd}\"\nUSE_CRI_DOCKERD=\"${USE_CRI_DOCKERD:-false}\"\n\ndepend() {\n  if [ \"${USE_CRI_DOCKERD}\" == \"true\" ]; then\n      need cri-dockerd\n  fi\n  after network-online\n  want cgroups\n}\n\nstart_pre() {\n  rm -f /tmp/k3s.*\n  checkpath --file --mode 0644 --owner root \"${output_log_unquoted}\" \"${error_log_unquoted}\"\n  ARCH=amd64\n  if [ \"$(uname -m)\" = \"aarch64\" ]; then\n    ARCH=arm64\n  fi\n  IMAGEPATH=\"/var/lib/rancher/k3s/agent/images/k3s-airgap-images-${ARCH}\"\n  if [ \"${ENGINE}\" == \"moby\" ]; then\n    if [ -f ${IMAGEPATH}.tar.zst ]; then\n      zstd -f -c -d ${IMAGEPATH}.tar.zst | docker load\n    elif [ -f ${IMAGEPATH}.tar ]; then\n      docker load --input ${IMAGEPATH}.tar\n    fi\n  else\n    if [ -f ${IMAGEPATH}.tar.zst ]; then\n      nerdctl --namespace k8s.io load ${ALLPLATFORMS} --input ${IMAGEPATH}.tar.zst\n    elif [ -f ${IMAGEPATH}.tar ]; then\n      nerdctl --namespace k8s.io load ${ALLPLATFORMS} --input ${IMAGEPATH}.tar\n    fi\n  fi\n}\n\nsupervisor=supervise-daemon\nname=k3s\ncommand=/usr/local/bin/k3s\ncommand_args=\"server --https-listen-port ${PORT} ${ADDITIONAL_ARGS:-}\"\nif [ \"${ENGINE}\" == \"moby\" ]; then\n  if [ \"${USE_CRI_DOCKERD}\" == \"true\" ]; then\n    command_args=\"${command_args} --container-runtime-endpoint /run/cri-dockerd.sock\"\n  else\n    command_args=\"${command_args} --docker\"\n  fi\nelif [ \"${ENGINE}\" == \"containerd\" ]; then\n  command_args=\"${command_args} --container-runtime-endpoint /run/k3s/containerd/containerd.sock\"\nfi\ncommand_args=\"${command_args} ${K3S_EXEC:-}\"\nK3S_LOGFILE=\"${K3S_LOGFILE:-${LOG_DIR:-/var/log}/${RC_SVCNAME}.log}\"\noutput_log_unquoted=\"${K3S_OUTFILE:-${K3S_LOGFILE}}\"\noutput_log=\"'${output_log_unquoted}'\"\nerror_log_unquoted=\"${K3S_ERRFILE:-${K3S_LOGFILE}}\"\nerror_log=\"'${error_log_unquoted}'\"\n\npidfile=\"/var/run/k3s.pid\"\nrespawn_delay=5\nrespawn_max=0\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/service-wsl-dockerd.initd",
    "content": "#!/sbin/openrc-run\n\n# This is an OpenRC service script (/etc/init.d/wsl-dockerd) that runs\n# wsl-helper docker-proxy start.\n\n# shellcheck shell=ksh\n\nname=\"Rancher Desktop Docker Daemon\"\ndescription=\"Starts dockerd for Rancher Desktop\"\n\nsupervisor=supervise-daemon\nif [ -f /usr/local/openresty/nginx/conf/allowed-images.conf ]; then\n  supervise_daemon_args=\"-e HTTPS_PROXY=http://127.0.0.1:3128\"\nfi\ncommand=\"'${WSL_HELPER_BINARY:-/usr/local/bin/wsl-helper}'\"\ncommand_args=\"docker-proxy start\"\n\nDOCKER_LOGFILE=\"${DOCKER_LOGFILE:-${LOG_DIR:-/var/log}/${RC_SVCNAME}.log}\"\noutput_log=\"'${DOCKER_LOGFILE}'\"\nerror_log=\"'${DOCKER_LOGFILE}'\"\n\nrc_ulimit=\"${DOCKER_ULIMIT:--c unlimited -n 1048576 -u unlimited}\"\n\ndepend() {\n    need sysfs cgroups\n    after iptables ip6tables\n}\n\nhealthcheck() {\n    /usr/bin/curl --fail --unix-socket /mnt/wsl/rancher-desktop/run/docker.sock --url http://./_ping\n}\nhealthcheck_timer=60\nrespawn_delay=5\nrespawn_max=10\nrespawn_period=10\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/spin-operator.yaml",
    "content": "---\napiVersion: core.spinkube.dev/v1alpha1\nkind: SpinAppExecutor\nmetadata:\n  name: containerd-shim-spin\nspec:\n  createDeployment: true\n  deploymentConfig:\n    runtimeClassName: spin\n---\napiVersion: v1\nkind: Namespace\nmetadata:\n  name: spin-operator\n---\napiVersion: helm.cattle.io/v1\nkind: HelmChart\nmetadata:\n  name: spin-operator\n  namespace: kube-system\nspec:\n  chart: \"https://%{KUBERNETES_API}%/static/rancher-desktop/spin-operator.tgz\"\n  targetNamespace: spin-operator\n  # Old versions of the helm-controller don't support createNamespace, so we\n  # created the namespace ourselves.\n  createNamespace: false\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/wsl-data.conf",
    "content": "# This is the /etc/wsl.conf for use with rancher-desktop-data\n# As we do not have an actual data distribution, this file is included as part\n# of the application and written out at runtime.\n[general]\n# Disable GUI integration, only CLI apps work.\nguiApplications=false\n\n[automount]\n# Prevent processing /etc/fstab, since it doesn't exist.\nmountFsTab = false\n# Prevent running ldconfig, since that doesn't exist.\nldconfig = false\n# Needed for compatibility with some `yarn` scenarios.\noptions = metadata\n\n# We _do_ want to generate `/etc/hosts` here, so that it can be used by the main\n# distribution.\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/wsl-exec",
    "content": "#!/bin/ash\n\n# wsl-exec is used to execute user-issued shell commands from\n# rdctl shell ... in a correct namespace. If the experimental\n# rancher desktop networking is enabled all the resulting\n# shell from rdctl shell will be executed in the new namespace\n# associated with the rd networking; otherwise, it will be executed\n# in the default namespace.\n\nset -o errexit -o nounset\n\n# We may have WSLENV set to override PATH; however, that causes the default entries to be missing.\nstr=\"/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:\"\nwhile [ -n \"$str\" ]; do\n  dir=${str%%:*}\n  if ! [[ \":${PATH}:\" =~ :${dir}: ]]; then\n    export PATH=\"${PATH%:}:${dir}\"\n  fi\n  str=${str#*:}\ndone\n\npid=\"$(cat /run/wsl-init.pid)\"\n\nif [ -z \"${pid}\" ]; then\n    echo \"Could not find wsl-init process\" >&2\n    exit 1\nfi\n\n# If the pid is _not_ /sbin/init, find the child that is.\ncommand=\"$(ps -o pid,args | awk \"\\$1 == $pid { print \\$2 }\")\"\nif [ \"$command\" != \"/sbin/init\" ]; then\n  newpid=\"$(ps -o pid,ppid,args | awk \"\\$2 == $pid && \\$3 == \\\"/sbin/init\\\" { print \\$1 }\")\"\n  if [ -n \"${newpid}\" ]; then\n    pid=\"${newpid}\"\n  fi\nfi\n\nif [ $# -eq 0 ]; then\n  set -- /bin/sh\nfi\n# If -w$PWD is specified on the first nsenter, then `wsl-exec pwd`\n# fails with \"pwd: getcwd: No such file or directory\"\nexec /usr/bin/nsenter -n -p -m -t \"${pid}\" /usr/bin/nsenter \"-w${PWD}\" \"$@\"\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/scripts/wsl-init",
    "content": "#!/bin/sh\n\n# This script is used to launch (busybox) init on WSL2 through network-setup process.\n# The network-setup process starts the vm-switch and unshare as its sub processes. this\n# is necessary since we need to do some mount namespace, since we store the data on the\n# WSL shared mount (/mnt/wsl/rancher/desktop/) and that can have issues with\n# lingering tmpfs mounts after we exit.  This means we need to run this script\n# under unshare (to get a private mount namespace), and then we can mark various\n# mount points as shared (for buildkit).  Kubelet will internally do some\n# tmpfs mounts for volumes (secrets, etc.), which will stay private and go away\n# once k3s exits, so that we can delete the data as necessary.\n\nset -o errexit -o nounset -o xtrace\n\nNETWORK_SETUP_LOG=\"${LOG_DIR}/network-setup.log\"\nVM_SWITCH_LOG=\"${LOG_DIR}/vm-switch.log\"\n\n\nif [ $$ -ne \"1\" ]; then\n    # This is not running as PID 1; this means that this is a normal invocation\n    # from WSL.\n    exec /usr/local/bin/network-setup --logfile \"$NETWORK_SETUP_LOG\" \\\n    --vm-switch-path /usr/local/bin/vm-switch --vm-switch-logfile \\\n    \"$VM_SWITCH_LOG\" ${RD_DEBUG:+-debug} --unshare-arg \"${0}\"\nfi\n\n# Mark directories that we will need to bind mount as shared mounts.\n(\n    IFS=:\n    for dir in / ${DISTRO_DATA_DIRS}; do\n        mount --make-shared \"${dir}\"\n    done\n)\n\n# Mount bpffs to allow containers to leverage bpf, and make both bpffs and\n# cgroupfs shared mounts so the pods can mount them correctly.\nmount bpffs -t bpf /sys/fs/bpf\nmount --make-shared /sys/fs/bpf\nmount --make-shared /sys/fs/cgroup\n\n# Mount binfmt_misc to allow nerdctl to see which qemu-* handlers have been loaded.\n# It will display a warning for foreign platforms if their handler seems missing.\nmount -t binfmt_misc binfmt_misc /proc/sys/fs/binfmt_misc\nmount --make-shared /proc/sys/fs/binfmt_misc\n\nif [ -f /var/lib/resolv.conf ]; then\n    ln -s -f /var/lib/resolv.conf /etc/resolv.conf\nfi\n\n# Run init (which never exits).\nexec /sbin/init\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/specs/README.md",
    "content": "## Generators\n\n### To generate go code:\n\n`oapi-codegen pkg/rancher-desktop/assets/specs/command-api.yaml > api/commands.go`\n\n#### Dependencies:\n\n* opai-codegen: To install:\n\n```bash\ngo get github.com/deepmap/oapi-codegen/cmd/oapi-codegen\n```\n\n### To generate documentation:\n\n```\nmkdir tmp\ndocker run --rm -v ${PWD}:/local openapitools/openapi-generator-cli:v5.4.2 generate -i /local/src/assets/specs/command-api.yaml -g html -o /local/tmp/\nopen tmp/index.html # (macOS)\nstart tmp/index.html # (Powershell)\nxdg-open tmp/index.html # (linux, replace with path to a specific browser if you prefer).\n```\n\nRecommended tag: openapitools/openapi-generator-cli:v5.4.2\n\nSo run:\n```\ndocker run ... openapitools/openapi-generator-cli@sha256:3d7c84e4b8f25a2074d6ab44d936cd69d08a223021197269e75d29992204e15e\n```\n\n## References:\n\n* OpenAPI spec: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#mediaTypeObject\n\n* Tools: https://openapi.tools/\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/specs/command-api.yaml",
    "content": "\ninfo:\n  title: Rancher Desktop API\n  version: 0.0.1\npaths:\n  /:\n    get:\n      operationId: listEndpoints\n      summary: List all endpoints.\n      responses:\n        '200':\n          description: A list of endpoints\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { type: string }\n\n  /v0:\n    get:\n      operationId: listV0Endpoints\n      summary: List all version zero endpoints.\n      responses:\n        '200':\n          description: A list of version 0 endpoints, of which there are none.\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { type: string }\n\n  /v1:\n    get:\n      operationId: listV1Endpoints\n      summary: List all version one endpoints.\n      responses:\n        '200':\n          description: A list of endpoints\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { type: string }\n\n  /v1/about:\n    get:\n      operationId: getAbout\n      summary: Returns a description of the endpoints\n      responses:\n        '200':\n          description: A note about endpoints not being forwards-compatible.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/diagnostic_categories:\n    get:\n      operationId: diagnosticCategories\n      summary: Return a list of the category names for the Diagnostics component. Takes no parameters.\n      responses:\n        '200':\n          description: A list of the category names.\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n\n  /v1/diagnostic_checks:\n    get:\n      operationId: diagnosticChecks\n      summary: Return all the checks, optionally filtered by specified category and/or checkID.\n      parameters:\n      - in: query\n        name: category\n      - in: query\n        name: checkID\n      responses:\n        '200':\n          description: A list of check objects. An invalid or unrecognized query parameter returns (200, empty array)\n          content:\n            application/json:\n              schema:\n                \"$ref\" : \"#/components/schemas/diagnostics\"\n    post:\n      operationId: diagnosticRunChecks\n      summary: Run all diagnostic checks, and return any results.\n      responses:\n        '200':\n          description: A list of check results.\n          content:\n            application/json:\n              schema:\n                \"$ref\": \"#/components/schemas/diagnostics\"\n\n  /v1/diagnostic_ids:\n    get:\n      operationId: diagnosticIDsForCategory\n      summary: >-\n        Return a list of the check IDs for the Diagnostics category,\n        or 404 if there is no such `category`.\n        Specifying an existing category with no checks\n        will return status code 200 and an empty array.\n      parameters:\n      - in: query\n        name: category\n      responses:\n        '200':\n          description: A list of the check IDs for the specified category.\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  type: string\n        '404':\n          description: The category is not recognized.\n\n\n  /v1/extensions:\n    get:\n      operationId: listExtensions\n      summary: List currently-installed RDX extensions.\n      responses:\n        '200':\n          description: A list of installed RDX extensions.\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  type: object\n                  properties:\n                    version:\n                      type: string\n                    metadata:\n                      type: object\n                    labels:\n                      type: object\n                      additionalProperties:\n                        type: string\n        '503':\n          description: >-\n            The extension manager has not been loaded yet.  The client should\n            retry the request at some future point in time.\n\n  /v1/extensions/install:\n    post:\n      operationId: installExtension\n      summary: Install an RDX extension\n      parameters:\n      - in: query\n        name: id\n      responses:\n        '201':\n          description: The extension was installed.\n        '204':\n          description: The extension was already installed.\n        '400':\n          description: There was an issue with the parameters.\n        '422':\n          description: The extension could not be installed.\n          content:\n            text/plain:\n              schema:\n                type: string\n        '503':\n          description: An internal error occurred.\n\n  /v1/extensions/uninstall:\n    post:\n      operationId: uninstallExtension\n      summary: Uninstall an RDX extension\n      parameters:\n      - in: query\n        name: id\n      responses:\n        '201':\n          description: The extension was uninstalled.\n        '204':\n          description: The extension was already uninstalled.\n        '400':\n          description: There was an issue with the parameters.\n        '422':\n          description: The extension could not be installed.\n          content:\n            text/plain:\n              schema:\n                type: string\n        '503':\n          description: An internal error occurred.\n\n  /v1/factory_reset:\n    put:\n      operationId: factoryReset\n      summary: Factory reset Rancher Desktop, losing user data\n      requestBody:\n        description: JSON block giving factory reset options.\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                keepSystemImages:\n                  type: boolean\n        required: true\n      responses:\n        '202':\n          description: The application is performing a factory reset.\n        '400':\n          description: An error occurred\n\n  /v1/k8s_reset:\n    put:\n      operationId: k8sReset\n      summary: Reset Kubernetes\n      requestBody:\n        description: JSON block giving k8s reset options.\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                mode:\n                  type: string\n                  enum: [ fast, wipe ]\n        required: true\n      responses:\n        '202':\n          description: The application is performing a Kubernetes reset.\n        '400':\n          description: An error occurred\n\n  /v1/port_forwarding:\n    post:\n      operationId: createPortForward\n      summary: Create a new port forwarding\n      requestBody:\n        description: JSON block consisting of the port forwarding details\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                namespace:\n                  type: string\n                service:\n                  type: string\n                k8sPort:\n                  type:\n                  - string\n                  - integer\n                hostPort:\n                  type: integer\n        required: true\n      responses:\n        '200':\n          description: The port forwarding was created or already exists; the response contains the listening host port.\n          content:\n            text/plain:\n              schema:\n                type: integer\n        '400':\n          description: The port forwarding could not be created.\n    delete:\n      operationId: deletePortForward\n      summary: Delete a port forwarding\n      parameters:\n      - in: query\n        name: namespace\n      - in: query\n        name: service\n      - in: query\n        name: k8sPort\n      responses:\n        '200':\n          description: The port forwarding was deleted or doesn't exist.\n        '400':\n          description: The port forwarding could not be deleted.\n\n  /v1/propose_settings:\n    put:\n      operationId: proposeSettings\n      summary: >-\n        Propose some settings and determine if the backend needs to be restarted\n        or reset (losing user data).\n      requestBody:\n        description: >-\n          JSON block consisting of some or all of the current preferences,\n          with changes applied to any number of settings the backend supports changing this way.\n        content:\n          application/json:\n            schema:\n              \"$ref\" : \"#/components/schemas/preferences\"\n        required: true\n      responses:\n        '202':\n          description: A description of the effects of the proposed settings on the backend.\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  type: object\n                  properties:\n                    current: {}\n                    desired: {}\n                    severity:\n                      type: string\n                      enum: [ restart, reset ]\n        '400':\n          description: The proposed settings were not valid.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/settings:\n    get:\n      operationId: listSettings\n      summary:  List the current preference settings\n      responses:\n        '200':\n          description: The current preferences in JSON format\n          content:\n            application/json:\n              schema:\n                \"$ref\" : \"#/components/schemas/preferences\"\n    put:\n      operationId: updateSettings\n      summary:  Updates the specified preference settings\n      requestBody:\n        description: >-\n          JSON block consisting of some or all of the current preferences,\n          with changes applied to any number of settings the backend supports changing this way.\n        content:\n          application/json:\n            schema:\n              \"$ref\" : \"#/components/schemas/preferences\"\n        required: true\n      responses:\n        '202':\n          description: The settings were accepted.\n          content:\n            text/plain:\n              schema:\n                type: string\n        '400':\n          description: The proposed settings were not valid.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/settings/locked:\n    get:\n      operationId: listLockedSettings\n      summary:  List the current locked settings\n      responses:\n        '200':\n          description: The locked preferences in JSON format\n          content:\n            application/json:\n              schema:\n                \"$ref\" : \"#/components/schemas/preferences\"\n\n  /v1/shutdown:\n    put:\n      operationId: shutdownApp\n      summary:  Shuts down Rancher Desktop\n      responses:\n        '202':\n          description: The application is in the process of shutting down.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/snapshots:\n    get:\n      operationId: listSnapshots\n      summary:  List the snapshots\n      responses:\n        '200':\n          description: The snapshots list in JSON format\n          content:\n            application/json:\n              schema:\n                type: array\n                items:\n                  \"$ref\" : \"#/components/schemas/snapshot\"\n    post:\n      operationId: createSnapshot\n      summary: Creates a new snapshot\n      responses:\n        '200':\n          description: The snapshot was created.\n        '400':\n          description: The snapshot could not be created.\n          content:\n            text/plain:\n              schema:\n                type: string\n    delete:\n      operationId: deleteSnapshot\n      summary:  Deletes a snapshot\n      parameters:\n      - in: query\n        name: name\n      responses:\n        '200':\n          description: The snapshot was deleted.\n        '404':\n          description: The snapshot could not be deleted.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/snapshots/cancel:\n    post:\n      operationId: cancelSnapshot\n      summary: Cancels active snapshot operation\n      responses:\n        '200':\n          description: The snapshot operation was canceled.\n        '400':\n          description: The snapshot could not be canceled.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/snapshot/restore:\n    post:\n      operationId: restoreSnapshot\n      summary: Restore a snapshot\n      parameters:\n      - in: query\n        name: name\n      responses:\n        '202':\n          description: The snapshot was restored.\n        '404':\n          description: The snapshot could not be restored.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/transient_settings:\n    get:\n      operationId: listTransientSettings\n      summary:  List the current transient settings\n      responses:\n        '200':\n          description: The current transient settings in JSON format\n          content:\n            application/json:\n              schema:\n                \"$ref\" : \"#/components/schemas/transientSettings\"\n    put:\n      operationId: updateTransientSettings\n      summary:  Updates application transient settings\n      requestBody:\n        description: JSON block consisting of transient settings\n        content:\n          application/json:\n            schema:\n              \"$ref\" : \"#/components/schemas/transientSettings\"\n        required: true\n      responses:\n        '202':\n          description: The settings were accepted.\n          content:\n            text/plain:\n              schema:\n                type: string\n        '400':\n          description: The proposed transient settings were not valid.\n          content:\n            text/plain:\n              schema:\n                type: string\n\n  /v1/backend_state:\n    get:\n      operationId: getBackendState\n      summary:  Get the current backend state\n      responses:\n        '200':\n          description: The current backend state\n          content:\n            application/json:\n              schema:\n                type: object\n                required:\n                  - vmState\n                  - locked\n                properties:\n                  vmState:\n                    type: string\n                  locked:\n                    type: boolean\n    put:\n      operationId: setBackendState\n      summary:  Set the desired backend state\n      requestBody:\n        description: >-\n          JSON block consisting of some or all of desired backend state, with changes applied\n          to the parts of backend state that are specified.\n        content:\n          application/json:\n            schema:\n              type: object\n              properties:\n                vmState:\n                  type: string\n                locked:\n                  type: boolean\n        required: true\n      responses:\n        '202':\n          description: The desired backend state was accepted.\n          content:\n            text/plain:\n              schema:\n                type: string\n\ncomponents:\n  schemas:\n    preferences:\n      type: object\n      properties:\n        application:\n          type: object\n          properties:\n            adminAccess:\n              type: boolean\n              x-rd-platforms: [darwin, linux] # Only in the specified platforms\n              x-rd-usage: enable privileged operations\n            debug:\n              type: boolean\n              x-rd-usage: generate more verbose logging\n            extensions:\n              type: object\n              properties:\n                allowed:\n                  type: object\n                  properties:\n                    enabled:\n                      type: boolean\n                      x-rd-hidden: true\n                    list:\n                      type: array\n                      items:\n                        type: string\n                        x-rd-hidden: true\n                installed:\n                  type: object\n                  x-rd-usage: installed extensions and their tag\n                  additionalProperties:\n                    type: string\n            pathManagementStrategy:\n              type: string\n              enum: [manual, rcfiles]\n              x-rd-platforms: [darwin, linux]\n              x-rd-usage: update PATH to include ~/.rd/bin\n            telemetry:\n              type: object\n              properties:\n                enabled:\n                  type: boolean\n                  x-rd-usage: allow collection of anonymous statistics\n            updater:\n              type: object\n              properties:\n                enabled:\n                  type: boolean\n                  x-rd-usage: automatically update to the latest release\n            autoStart:\n              type: boolean\n              x-rd-usage: start app when logging in\n            startInBackground:\n              type: boolean\n              x-rd-usage: start app without window\n            hideNotificationIcon:\n              type: boolean\n              x-rd-usage: don't show notification icon\n            window:\n              type: object\n              properties:\n                quitOnClose:\n                  type: boolean\n                  x-rd-usage: terminate app when the main window is closed\n            theme:\n              type: string\n              enum: [system, light, dark]\n              x-rd-usage: set the color theme (system follows OS setting)\n        containerEngine:\n          type: object\n          properties:\n            name:\n              type: string\n              # TODO \"docker\" setting should be a hidden alias of \"moby\".\n              # Why have two values for exactly the same thing?\n              enum: [containerd, docker, moby]\n              x-rd-aliases: [container-engine]\n              x-rd-usage: set engine\n            allowedImages:\n              type: object\n              properties:\n                enabled:\n                  type: boolean\n                  x-rd-usage: only allow images to be pulled that match the allowed patterns\n                patterns:\n                  type: array\n                  # TODO It is not yet possible to specify array/list values with `rdctl set`\n                  x-rd-usage: allowed image names\n                  items:\n                    type: string\n            mobyStorageDriver:\n              type: string\n              enum: [classic, snapshotter, auto]\n              x-rd-usage: override Moby storage driver selection\n        virtualMachine:\n          type: object\n          properties:\n            memoryInGB:\n              type: integer\n              minimum: 1\n              x-rd-platforms: [darwin, linux]\n              x-rd-usage: reserved RAM size\n            numberCPUs:\n              type: integer\n              minimum: 1\n              x-rd-platforms: [darwin, linux]\n              x-rd-usage: reserved number of CPUs\n            type:\n              type: string\n              enum: [qemu, vz]\n              x-rd-platforms: [darwin]\n            useRosetta:\n              type: boolean\n              x-rd-platforms: [darwin]\n            mount:\n              type: object\n              x-rd-platforms: [darwin, linux]\n              properties:\n                type:\n                  type: string\n                  enum: [reverse-sshfs, 9p, virtiofs]\n                  x-rd-usage: how directories are shared; 9p is experimental\n        kubernetes:\n          type: object\n          properties:\n            version:\n              type: string\n              x-rd-aliases: [kubernetes-version]\n              x-rd-usage: choose which version of Kubernetes to run\n            port:\n              type: integer\n              x-rd-usage: apiserver port\n            enabled:\n              type: boolean\n              x-rd-aliases: [kubernetes-enabled]\n              x-rd-usage: run Kubernetes\n            options:\n              type: object\n              properties:\n                traefik:\n                  type: boolean\n                  x-rd-usage: install and run traefik\n                flannel:\n                  type: boolean\n                  x-rd-aliases: [flannel-enabled]\n                  x-rd-usage: use flannel networking; disable to install your own CNI\n            ingress:\n              type: object\n              properties:\n                localhostOnly:\n                  type: boolean\n                  x-rd-platforms: [win32]\n                  x-rd-usage: bind services to 127.0.0.1 instead of 0.0.0.0\n        experimental:\n          type: object\n          properties:\n            containerEngine:\n              type: object\n              properties:\n                webAssembly:\n                  type: object\n                  properties:\n                    enabled:\n                      type: boolean\n                      x-rd-usage: enable support for containerd-wasm shims\n            kubernetes:\n              type: object\n              properties:\n                options:\n                  type: object\n                  properties:\n                    spinkube:\n                      type: boolean\n                      x-rd-usage: install spin operator\n            virtualMachine:\n              type: object\n              properties:\n                diskSize:\n                  type: string\n                  x-rd-platforms: [darwin, linux]\n                  x-rd-usage: >-\n                    desired size of the disk; changing this setting will not\n                    shrink existing disks (example: 10GiB)\n                mount:\n                  type: object\n                  x-rd-platforms: [darwin, linux]\n                  properties:\n                    9p:\n                      type: object\n                      properties:\n                        securityModel:\n                          type: string\n                          enum: [passthrough, mapped-xattr, mapped-file, none]\n                        protocolVersion:\n                          type: string\n                          enum: [9p2000, 9p2000.u, 9p2000.L]\n                        msizeInKib:\n                          type: integer\n                          minimum: 4\n                          x-rd-usage: maximum packet size\n                        cacheMode:\n                          type: string\n                          enum: [none, loose, fscache, mmap]\n                proxy:\n                  type: object\n                  x-rd-platforms: [win32]\n                  x-rd-usage: configure proxy address\n                  properties:\n                    enabled:\n                      type: boolean\n                      x-rd-usage: redirect the traffic to the configured proxy address\n                    address:\n                      type: string\n                      x-rd-usage: proxy address\n                    password:\n                      type: string\n                      x-rd-usage: if needed the password to connect to the proxy\n                    port:\n                      type: integer\n                      x-rd-usage: proxy port\n                    username:\n                      type: string\n                      x-rd-usage: if needed the username to connect to the proxy\n                    noproxy:\n                      type: array\n                      x-rd-usage: list of hostname to exclude from using the proxy\n                      items: { type: string }\n                sshPortForwarder:\n                  type: boolean\n                  x-rd-platforms: [darwin, linux]\n                  x-rd-usage: use SSH for port forwarding instead of gRPC\n        WSL:\n          type: object\n          x-rd-platforms: [win32]\n          # TODO It is not yet possible to configure this via `rdctl set`.\n          x-rd-usage: make container engine and Kubernetes available in these WSL2 distros\n          properties:\n            integrations:\n              type: object\n              additionalProperties: true\n        portForwarding:\n          type: object\n          properties:\n            includeKubernetesServices:\n              type: boolean\n              x-rd-usage: show Kubernetes system services on Port Forwarding page\n        images:\n          type: object\n          properties:\n            showAll:\n              type: boolean\n              x-rd-usage: show system images on Images page\n            namespace:\n              type: string\n              x-rd-usage: select only images from this namespace (containerd only)\n        containers:\n          type: object\n          properties:\n            showAll:\n              type: boolean\n              x-rd-usage: show system containers on Containers page\n            namespace:\n              type: string\n              x-rd-usage: select only namespaces from this namespace (containerd only)\n        diagnostics:\n          type: object\n          properties:\n            showMuted:\n              type: boolean\n              x-rd-usage: unhide muted diagnostics\n            mutedChecks:\n              type: object\n              # TODO It is not possible to modify this setting via `rdctl set`.\n              x-rd-usage: diagnostic ids that have been muted\n              additionalProperties: true\n            connectivity:\n              type: object\n              properties:\n                interval:\n                  type: integer\n                  x-rd-usage: >-\n                    Number of milliseconds before polling for network access;\n                    set this to zero to disable background connectivity checking\n                timeout:\n                  type: integer\n                  x-rd-usage: Number of milliseconds to wait before timing out\n\n    diagnostics:\n      type: object\n      properties:\n        last_update:\n          type: string\n          format: date-time\n          example: \"1970-01-01T00:00:00.000Z\"\n        checks:\n          type: array\n          items:\n            type: object\n            properties:\n              id:\n                type: string\n              category:\n                type: string\n              documentation:\n                type: string\n              description:\n                type: string\n              passed:\n                type: boolean\n              mute:\n                type: boolean\n              fixes:\n                type: array\n                items:\n                  type: object\n                  properties:\n                    description:\n                      type: string\n    transientSettings:\n      type: object\n      properties:\n        noModalDialogs:\n          type: boolean\n        preferences:\n          type: object\n          properties:\n            navItem:\n              type: object\n              properties:\n                current:\n                  type: string\n                currentTabs:\n                  type: object\n                  additionalProperties: true\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/app.scss",
    "content": "@import \"./vendor/normalize\";\n\n@import \"./base/variables\";\n@import \"./base/functions\";\n@import \"./base/mixins\";\n@import \"./base/helpers\";\n@import \"./base/color\";\n@import \"./base/basic\";\n@import \"./base/typography\";\n\n@import \"./fonts/fontstack\";\n@import \"./fonts/dots\";\n@import \"./fonts/zerowidthspace\";\n@import \"./fonts/icons\";\n\n@import \"./themes/light\";\n@media screen and (prefers-color-scheme: dark) {\n  @import \"./themes/dark\";\n}\n@import './themes/_suse.scss';\n\n\n@import \"./global/columns\";\n@import \"./global/cards\";\n@import \"./global/button\";\n@import \"./global/form\";\n@import \"./global/gauges\";\n@import \"./global/labeled-input\";\n@import \"./global/tooltip\";\n@import \"./global/table\";\n@import \"./global/select\";\n@import \"./global/resource\";\n\n@import \"./vendor/vue-select\";\n\n@import \"./rancher-desktop.scss\";\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_basic.scss",
    "content": "// -----------------------------------------------------------------------------\n// This file contains very basic styles.\n// -----------------------------------------------------------------------------\n\n//\nHTML {\n  box-sizing: border-box;\n  height: 100%;\n}\n\nBODY {\n  color: var(--body-text);\n  direction: ltr;\n  position: relative;\n  margin: 0;\n  scrollbar-width: thin;\n  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);\n\n  &.overflow-hidden {\n    overflow: hidden;\n  }\n}\n\n.dashboard-body {\n  // this was moved to its own class because the background color prop conflicts with storybookjs addon \"backgrounds\".\n  // decoupling this style allows us to preview components in both light and dark themes\n  background: var(--body-bg);\n}\n\n::-webkit-scrollbar {\n  width: 8px !important;\n  height: 8px !important;\n}\n\n::-webkit-scrollbar {\n  width: 8px !important;\n  height: 8px !important;\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: var(--scrollbar-thumb) !important;\n  border-radius: var(--border-radius);\n}\n\n::-webkit-scrollbar-track {\n  background-color: var(--scrollbar-track) !important;\n}\n\n/*\n * Make all elements from the DOM inherit from the parent box-sizing\n * Since `*` has a specificity of 0, it does not override the `html` value\n * making all elements inheriting from the root box-sizing value\n * See: https://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/\n */\n*, *::before, *::after {\n  box-sizing: inherit;\n}\n\n:focus, .focused {\n  outline-color: var(--outline);\n  outline-style: solid;\n  outline-width: var(--outline-width);\n}\n\nINPUT,\nSELECT,\nTEXTAREA,\nBUTTON,\n.btn,\n.labeled-input,\n.labeled-select,\n.unlabeled-select,\n.checkbox-custom,\n.radio-custom {\n  &:focus, &.focused {\n    @include form-focus }\n}\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  margin: var(--outline-width);\n}\n\nA {\n  @include link-color(var(--link), var(--body-text));\n\n  text-decoration: none;\n\n  &:hover,\n  &:active {\n    text-decoration: underline;\n    color: var(--body-text);\n  }\n}\n\nHR {\n  height: 0;\n  border: 0;\n  border-top: 1px solid var(--border);\n  width: 100%;\n\n  &.dark {\n    border-color: var(--nav-bg);\n  }\n}\n\nHR.vertical {\n  border-top: 0;\n  border-left: 1px solid var(--border);\n  height: 100%;\n  position: absolute;\n  left: 50%;\n  margin-left: -1px;\n  top: 0;\n}\n\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_color.scss",
    "content": ".bg-transparent {\n  background-color: transparent;\n}\n\n.bg-disabled {\n  background-color: var(--disabled-bg) !important;\n}\n\n.text-disabled {\n  color: var(--disabled-text) !important;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_functions.scss",
    "content": "///Computes the \"brightness\" of a color\n@use 'sass:math';\n\n@function brightness($color) {\n  @if type-of($color) == color {\n    @return math.div(red($color) * 0.299 + green($color) * 0.587 + blue($color) * 0.114, 255) * 100%;\n  }\n  @else {\n    @return unquote(\"brightness(#{$color})\");\n  }\n}\n\n\n///Select the more readable foreground color for a given background color.\n@function contrast-color($color, $dark: $contrasted-dark, $light: $contrasted-light) {\n  @if $color == null {\n    @return null;\n  }\n  @else {\n    $color-brightness: brightness($color);\n    $dark-text-brightness: brightness($dark);\n    $light-text-brightness: brightness($light);\n    @return if(math.abs($color-brightness - $light-text-brightness) > math.abs($color-brightness - $dark-text-brightness), $light, $dark);\n  }\n}\n\n@function add-z-index($key, $value) {\n  @return map-merge($z-indexes, ($key: $value));\n}\n\n@function z-index($key) {\n  @if map-has-key($z-indexes, $key) {\n    @return map-get($z-indexes, $key);\n  }\n\n  @warn \"Unknown key `#{$key}` in $z-indexes\";\n  @return null;\n}\n\n// _decimal.scss | MIT License | gist.github.com/terkel/4373420\n\n// Round a number to specified digits.\n//\n// @param  {Number} $number A number to round\n// @param  {Number} [$digits:0] Digits to output\n// @param  {String} [$mode:round] (round|ceil|floor) How to round a number\n// @return {Number} A rounded number\n// @example\n//     decimal-round(0.333)    => 0\n//     decimal-round(0.333, 1) => 0.3\n//     decimal-round(0.333, 2) => 0.33\n//     decimal-round(0.666)    => 1\n//     decimal-round(0.666, 1) => 0.7\n//     decimal-round(0.666, 2) => 0.67\n//\n@function decimal-round ($number, $digits: 0, $mode: round) {\n  $n: 1;\n  // $number must be a number\n  @if type-of($number) != number {\n      @warn '#{ $number } is not a number.';\n      @return $number;\n  }\n  // $digits must be a unitless number\n  @if type-of($digits) != number {\n      @warn '#{ $digits } is not a number.';\n      @return $number;\n  } @else if not unitless($digits) {\n      @warn '#{ $digits } has a unit.';\n      @return $number;\n  }\n  @for $i from 1 through $digits {\n      $n: $n * 10;\n  }\n  @if $mode == round {\n      @return math.div(round($number * $n), $n);\n  } @else if $mode == ceil {\n      @return math.div(ceil($number * $n), $n);\n  } @else if $mode == floor {\n      @return math.div(floor($number * $n), $n);\n  } @else {\n      @warn '#{ $mode } is undefined keyword.';\n      @return $number;\n  }\n}\n\n// Ceil a number to specified digits.\n//\n// @param  {Number} $number A number to round\n// @param  {Number} [$digits:0] Digits to output\n// @return {Number} A ceiled number\n// @example\n//     decimal-ceil(0.333)    => 1\n//     decimal-ceil(0.333, 1) => 0.4\n//     decimal-ceil(0.333, 2) => 0.34\n//     decimal-ceil(0.666)    => 1\n//     decimal-ceil(0.666, 1) => 0.7\n//     decimal-ceil(0.666, 2) => 0.67\n//\n@function decimal-ceil ($number, $digits: 0) {\n  @return decimal-round($number, $digits, ceil);\n}\n\n// Floor a number to specified digits.\n//\n// @param  {Number} $number A number to round\n// @param  {Number} [$digits:0] Digits to output\n// @return {Number} A floored number\n// @example\n//     decimal-floor(0.333)    => 0\n//     decimal-floor(0.333, 1) => 0.3\n//     decimal-floor(0.333, 2) => 0.33\n//     decimal-floor(0.666)    => 0\n//     decimal-floor(0.666, 1) => 0.6\n//     decimal-floor(0.666, 2) => 0.66\n//\n@function decimal-floor ($number, $digits: 0) {\n  @return decimal-round($number, $digits, floor);\n}\n\n@function sizzle-gradient($color) {\n  $angle: 135deg;\n  $startPos: 0%;\n  $start: 0.3;\n  $middlePos: 110px;\n  $middle: 0.1;\n  $endPos: 100%;\n  $end: 0;\n\n  @return transparent linear-gradient(#{$angle},\n      #{rgba($color, $start)} #{$startPos},\n      #{rgba($color, $middle)} #{$middlePos},\n      #{rgba($color, $end)} #{$endPos}\n  ) 0% 0% no-repeat padding-box;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_helpers.scss",
    "content": "// -----------------------------------------------------------------------------\n// This file contains CSS helper classes.\n// -----------------------------------------------------------------------------\n\n// Text indent, margins, and paddings from 5-50px in 5px increments\n// e.g. in-10 - {text-indent: 10px}\n// e.g. p-10 - {padding: 10px}\n// e.g. mt-20 - {margin-top: 20px}\n$spacing-property-map: (\n    m:  margin,\n    mt: margin-top,\n    mr: margin-right,\n    ml: margin-left,\n    mb: margin-bottom,\n    p:  padding,\n    pt: padding-top,\n    pb: padding-bottom,\n    pl: padding-left,\n    pr: padding-right,\n    in: text-indent,\n);\n\n@each $keyword, $property in $spacing-property-map {\n  .#{$keyword}-0 { #{$property}: 0 !important; }\n\n  @for $size from 1 through 10 {\n    $val: $size * 5;\n    .#{$keyword}-#{$val} { #{$property}: $val * 1px !important; }\n  }\n}\n\n.spacer {\n  padding: 40px 0 0 0;\n}\n\n.spacer-small {\n  padding: 20px 0 0 0;\n}\n\n.pull-right {\n  float: right !important;\n}\n.pull-left {\n  float: left !important;\n}\n\n/**\n * Main content containers\n * 1. Make the container full-width with a maximum width\n * 2. Center it in the viewport\n * 3. Leave some space on the edges, especially valuable on small screens\n */\n.container {\n  max-width: $max-width; /* 1 */\n  min-width: $min-width;\n  margin-left: auto; /* 2 */\n  margin-right: auto; /* 2 */\n  padding-left: 20px; /* 3 */\n  padding-right: 20px; /* 3 */\n  width: 100%; /* 1 */\n }\n\n/**\n * Hide text while making it readable for screen readers\n * 1. Needed in WebKit-based browsers because of an implementation bug;\n *    See: https://code.google.com/p/chromium/issues/detail?id=457146\n */\n.hide-text {\n  overflow: hidden;\n  padding: 0; /* 1 */\n  text-indent: 101%;\n  white-space: nowrap;\n}\n\n/**\n * Hide element while making it readable for screen readers\n * https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css#L119-L133\n */\n.visually-hidden {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n\n.text-left {\n  text-align: left !important;\n}\n\n.text-center {\n  text-align: center !important;\n}\n\n.text-right {\n  text-align: right !important;\n}\n\n.text-small {\n  font-size: .8em;\n}\n\n.text-normal {\n  font-size: initial;\n}\n\n.text-italic {\n  font-style: italic;\n}\n\n.text-bold {\n  font-weight: bold;\n}\n\n.text-uppercase {\n  text-transform: uppercase;\n}\n\n.text-lowercase {\n  text-transform: lowercase;\n}\n\n.text-capitalize {\n  text-transform: capitalize;\n}\n\n.text-label {\n  color: var(--input-label);\n}\n\n.hide {\n  display: none !important;\n}\n\n.block {\n  display: block !important;\n}\n\n.inline {\n  display: inline !important;\n}\n\n.inline-block {\n  display: inline-block !important;\n}\n\n.table-cell {\n  display: table-cell !important;\n}\n\n.vertical-middle {\n  display: inline-block;\n  vertical-align: middle;\n}\n\n.invisible {\n  visibility: hidden;\n}\n\n.helper-text {\n  font-size: 12px;\n  color: var(--secondary);\n}\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content\n.sr-only {\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  margin: -1px;\n  padding: 0;\n  overflow: hidden;\n  clip: rect(0,0,0,0);\n  border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n.sr-only-focusable {\n  &:active,\n  &:focus {\n    position: static;\n    width: auto;\n    height: auto;\n    margin: 0;\n    overflow: visible;\n    clip: auto;\n  }\n}\n\n[role=\"button\"] {\n  cursor: pointer;\n}\n\n.eased {\n  -webkit-transition: all 0.5s ease;\n  -moz-transition: all 0.5s ease;\n  -o-transition: all 0.5s ease;\n  transition: all 0.5s ease;\n}\n\n.no-ease {\n  -webkit-transition: none !important;\n  -moz-transition: none !important;\n  -o-transition: none !important;\n  transition: none !important;\n}\n\n.full-height {\n  height: 100%;\n}\n\n.full-width {\n  width: 100%;\n}\n\n.align-top {\n  vertical-align: top !important;\n}\n\n.vertical-scroll {\n  max-height: 150px;\n  overflow: scroll;\n}\n\n.comma-list {\n  span:after {\n    content: ','\n  }\n  span:last-of-type:after {\n    content: ''\n\n  }\n}\n\n.link[disabled] {\n  pointer-events: none;\n}\n\n.subtle-box {\n  border: .5px solid var(--subtle-border);\n  box-shadow: 0 0 10px var(--shadow);\n  padding: 20px;\n  margin-bottom: 20px;\n  border-radius: var(--border-radius);\n}\n\n.plus-more {\n  color: var(  --input-placeholder );\n  font-size: 0.8em;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_mixins.scss",
    "content": "// -----------------------------------------------------------------------------\n// This file contains all application-wide Sass mixins.\n// -----------------------------------------------------------------------------\n\n/// Clear inner floats\n@mixin clearfix() {\n  &:before,\n  &:after {\n    content: \" \"; // 1\n    display: table; // 2\n  }\n  &:after {\n    clear: both;\n  }\n}\n\n@mixin list-unstyled {\n  margin: 0;\n  padding: 0;\n  list-style-type: none;\n}\n\n@mixin no-select {\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n}\n\n@mixin no-resize {\n  resize : none;\n}\n\n@mixin hand {\n  cursor : pointer;\n  cursor : hand;\n}\n\n@mixin fixed {\n  table-layout : fixed;\n}\n\n@mixin clip {\n  text-overflow : ellipsis;\n  overflow      : hidden;\n  white-space   : nowrap;\n  word-wrap     : break-word;\n}\n\n@mixin force-wrap {\n  word-wrap : break-word;\n  white-space: normal;\n}\n\n@mixin bordered-section {\n  border-bottom: 1px solid var(--border);\n  margin-bottom: 20px;\n  padding-bottom: 20px;\n}\n\n@mixin section-divider {\n  margin-bottom: 20px;\n  margin-top: 20px;\n}\n\n.clearfix         { @include clearfix; }\n.list-unstyled    { @include list-unstyled }\n.no-select        { @include no-select }\n.no-resize        { @include no-resize }\n.hand             { @include hand }\n.fixed            { @include fixed }\n.clip             { @include clip }\n.force-wrap       { @include force-wrap }\n.bordered-section { @include bordered-section }\n.section-divider  { @include section-divider }\n\n/// Sets the specified background color and calculates a dark or light contrasted text color.\n@mixin contrasted($background-color, $dark: $contrasted-dark, $light: $contrasted-light) {\n  color: contrast-color($background-color, $dark, $light);\n\n  &:hover {\n    text-decoration: underline;\n    color: var(--body-text);\n  }\n}\n\n/// Sets base color and darkens bg on hover\n@mixin bg-lighten($bg) {\n  background: $bg;\n  * {\n    background:lighten($bg,20%);\n  }\n}\n\n@mixin link-color($color, $hover) {\n  @if not($hover) {\n    $hover: $color;\n  }\n\n  color: $color;\n\n  &:hover\n   {\n    text-decoration: underline;\n    color: $hover;\n  }\n}\n\n@mixin icon-rotate($degrees, $rotation) {\n  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});\n  -webkit-transform: rotate($degrees);\n      -ms-transform: rotate($degrees);\n          transform: rotate($degrees);\n}\n\n@mixin icon-flip($horiz, $vert, $rotation) {\n  filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation});\n  -webkit-transform: scale($horiz, $vert);\n      -ms-transform: scale($horiz, $vert);\n          transform: scale($horiz, $vert);\n}\n\n@mixin input-status-color {\n  &:not(.focused) {\n    &.success {\n      border: solid 1px var(--success);\n      input, .selected {\n        color: var(--success);\n      }\n\n      .vs__actions:after {\n        color: var(--success);\n      }\n    }\n\n    &.warning {\n      border: solid 1px var(--warning);\n      input, .selected {\n        color: var(--warning);\n      }\n\n      .vs__actions:after {\n        color: var(--warning);\n      }\n    }\n\n    &.error {\n      border: solid 1px var(--error);\n      input, .selected {\n        color: var(--error);\n      }\n\n      .vs__actions:after {\n        color: var(--error);\n      }\n    }\n  }\n}\n\n@mixin form-focus {\n  // Focus for form like elements (not to be confused with basic :focus style)\n  outline: none;\n  box-shadow: 0 0 0 var(--outline-width) var(--outline);\n  background: var(--input-focus-bg)\n}"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_typography.scss",
    "content": "HTML, BODY {\n  font-family: $body-font;\n  font-size: 14px;\n}\n\nH1, H2, H3, H4, H5, H6 {\n  color: var(--body-text);\n  font-style: normal;\n  font-weight: 400;\n  margin: 0 0 10px 0;\n}\n\nH1 {\n  font-size: 24px;\n}\n\nH2 {\n  font-size: 21px;\n}\n\nH3 {\n  font-size: 18px;\n}\n\nH4 {\n  font-size: 16px;\n}\n\nH5 {\n  font-size: 14px;\n}\n\nH6 {\n  font-size: 12px;\n  text-transform: uppercase;\n}\n\nP {\n  font-weight: 400;\n  font-style: normal;\n  margin: 0;\n}\n\n//code\ncode, samp, kbd, .monospace {\n  font-family: $mono-font;\n  text-align: left;\n}\n\npre {\n  padding: 10px;\n  border-radius: var(--border-radius);\n  background: var(--box-bg);\n  margin: 5px;\n  overflow: auto;\n}\n\ncode {\n  background-color: var(--box-bg);\n  display: inline-block;\n  padding: 5px;\n  border: 1px solid var(--border);\n  border-radius: var(--border-radius);\n}\n\n.v-popper__popper code,\npre code {\n  background: transparent;\n  padding: 0;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/base/_variables.scss",
    "content": "$header-font: 'Poppins', sans-serif;\n$body-font: 'Lato', arial, helvetica, sans-serif;\n$mono-font: 'Roboto Mono', monospace;\n\n$max-width: 1440px !default;\n$min-width: 75% !default;\n$input-height: 61px;\n$unlabeled-input-height: 40px;\n\n$input-padding-lg: 18px;\n$input-padding-sm: 10px;\n$input-line-height: 18px;\n\n$column-gutter: 1.75%;\n\n$sideways-tabs-width: 200px;\n\n$array-list-remove-margin: 75px;\n\n$z-indexes: (\n  zero: 0,\n  default: 1,\n  overContent: 2,\n  hoverOverContent: 3,\n\n  tableGroup: 10,\n  fixedTableHeader: 11,\n\n  modalOverlay: 20,\n  modalContent: 21,\n\n  tooltip: 30,\n\n  dropdownOverlay: 40,\n  dropdownContent: 41,\n\n  loadingOverlay: 50,\n  loadingContent: 51\n);\n\n// Usage Example:\n// @media only screen and (min-width: map-get($breakpoints, '--viewport-*')) {\n// }\n$breakpoints: (\n  '--viewport-4':  480px,\n  '--viewport-7':  768px,\n  '--viewport-9':  992px,\n  '--viewport-12': 1281px,\n);\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/fonts/_dots.scss",
    "content": "@font-face {\n  font-family: 'dotsfont';\n  font-weight: normal;\n  font-style: normal;\n  src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAyEABEAAAAAV7gAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABgAAAABwAAAAcgM9Wv0dERUYAAAGcAAAAHQAAAB4AJwDeT1MvMgAAAbwAAABMAAAAYHOnumtjbWFwAAACCAAAAUwAAAGSId/p/mN2dCAAAANUAAAADAAAAAwF+BA6ZnBnbQAAA2AAAAGxAAACZVO0L6dnYXNwAAAFFAAAAAgAAAAIAAAAEGdseWYAAAUcAAABlQAARtxIEfQVaGVhZAAABrQAAAAyAAAANhPqDEtoaGVhAAAG6AAAAB4AAAAkD9kKlmhtdHgAAAcIAAAATAAAA2Cb0mq3bG9jYQAAB1QAAAGfAAABsumd1/5tYXhwAAAI9AAAACAAAAAgAfIAQm5hbWUAAAkUAAABRwAAApIWY2jUcG9zdAAAClwAAAHJAAACkyUTPVZwcmVwAAAMKAAAAFIAAABSWo1sY3dlYmYAAAx8AAAABgAAAAaskFpGAAAAAQAAAADV7pT1AAAAANR0ZLoAAAAA1mxdD3jaY2BkYGDgAWIxIGZiYATC60DMAuYxAAAM2wEGAAAAeNpjYOaUY5zAwMrAwmrMcpaBgWEWhGY6y5DGlAbkA6XggJkBCYR6h/sxODAoqP5hY/gH5LMxMGkoMDAwguQYvzC9A1IKDIwAF8ELN3jaY2BgYGaAYBkGRgYQ6AHyGMF8FoYCIC3BIAAU4WCoY9jC8J/pGNMdBS4FEQVJBX2FeNU///8DVSgwLGDYBpZhUBBQkIDJ/H/8/9D/g39//3324PCDfQ92P1j2oPzWbagtWAEjGwNcmpEJSDChKwA6lYWVjZ2Dk4ubh5ePX0BQSFhEVExcQlJKWkZWTl5BUUlZRVVNXUNTS1tHV0/fwNDI2MTUzNzC0sraxtbO3sHRydnF1c3dw9PL28fXzz8gMCg4JDQsPCIyKjomNi4+IZGhta2ja9L0uYsWLl66ZNmKVStXr1m3dv2GTVs2b92+bfeuPXsZilJSMxnKFxRkM5RlMbTPZChmYEiHuC6nmmH5zobkPBA7t4YhqbFlGgPDxUsMDJev7GA4APNDBRA3dzf1dPb1T+idMpVh8uw5sxgOHioEClcCMQAKvGiaAAAFdQW0BbQARAUReNpdUbtOW0EQ3Q0PA4HE2CA52hSzmZDGe6EFCcTVjWJkO4XlCGk3cpGLcQEfQIFEDdqvGaChpEibBiEXSHxCPiESM2uIojQ7O7NzzpkzS8qRqnfpa89T5ySQwt0GzTb9Tki1swD3pOvrjYy0gwdabGb0ynX7/gsGm9GUO2oA5T1vKQ8ZTTuBWrSn/tH8Cob7/B/zOxi0NNP01DoJ6SEE5ptxS4PvGc26yw/6gtXhYjAwpJim4i4/plL+tzTnasuwtZHRvIMzEfnJNEBTa20Emv7UIdXzcRRLkMumsTaYmLL+JBPBhcl0VVO1zPjawV2ys+hggyrNgQfYw1Z5DB4ODyYU0rckyiwNEfZiq8QIEZMcCjnl3Mn+pED5SBLGvElKO+OGtQbGkdfAoDZPs/88m01tbx3C+FkcwXe/GUs6+MiG2hgRYjtiKYAJREJGVfmGGs+9LAbkUvvPQJSA5fGPf50ItO7YRDyXtXUOMVYIen7b3PLLirtWuc6LQndvqmqo0inN+17OvscDnh4Lw0FjwZvP+/5Kgfo8LK40aA4EQ3o3ev+iteqIq7wXPrIn07+xWgAAAAABAAH//wAPeNrt2jFLI0EUwPH3dncSBeVcOWxssnE97ggYTYxVOlNa2PgNLPwOVgpiYXNe5XewMLOoYDiOuxPtQsDCWGhhuYiFrcrgrkW6a2yE888w8OYx7/emfyOetES8VbMivhRlxqpUm0kxCO5rtmCum4nvZaFYP0+bPJ0UC+a5mWier4dROB2FUcsruVj33JpZedxvBV0RUe3LSHHHbMgnmZR2ULUjmmp7rGpFK3bIS22oFZmd08/jQb0WjzfmvXiqHHja7+hSr6fLvzvusNtzB3+8X6fu/OhEF8/+avO4435iY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Njv7MtbwH4W4aNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY39oebC726Y/sCYqNcWcuPLVLmQGZvp7o+77c30++799r/rhwf1EkSlWBrzEkelQLSvRi+0nK0rHXVPbsbduhv31T38X7Py/PFmy2yJL98kyQ4Vq5oOAs3ateXS+lmXfJvXTnNhFOZFj+vZvReYPphnAAAAeNpjYGRgYADiTd8OXoznt/nKIM/BAAJXSlJ2gehrObH8IJrzOmsrkOJgYALxAFBACosAAHjaY2BkYGBj+HuDgYH7KQMQcF5nYGRABTcAYDIEhgAAeNpjesPgwgAETKsYGDhngjDj9VE8EHg07AcaMx1iYGBtBeYFKM14HYgTgZnjNRRvAPKlgLQfalyxP2W8zv0UwQepAekDmQEADJUueHjadcI7KIQBAADg//1+v98vkiQZJEmSDJIkXZcuGS5J0iXDdYMkSZcMMkiSDDJckiTJIIOkSwYZJF2XdBl0SZckA8mq7wMAoP5PAsgCR8ATKIEdYArcAPPgB1QDxaA5aB8qwAzcCo/Bq/A5/IYESB8yjeSQOxRDm9Akuoyeoi+YhXVjaWwbu8EBvAEfwhfxY7xEKEQnMUlsElfEJ1lLxsl58oAsUhzVRo1Ta9QFVaEjup+eoXfpe4ZgmpkRZoU5Y8qsw/awGXaHveUgrpEb5pa4E+6Z1/guforf4q/5L6FOGBQWhEPhURR+tIsT4rp4Kb5L1dKANCvtSQ8yJbfICTkrH8lPiqR0KCllQ8krH2qNGlPn1H21oDFaqzamrWrn2pse6H36tJ7T7wzMaDKSxrJxaryYltltps1t88YCrAZryFq0jq2Srdid9qS9aV/Zn06tE3fmnQOn6HJumzvurrkXbsWLvH5vxtv17n3Cb/ZH/BX/zC8HTtATZIKd4DaEwsZwOFwKT8LnSIu6/jEaZaNclI9eq7yq3l+pb5RddPEAAAEAAADYABAAAgAAAAAAAgABAAIAFgAAAQAALgAAAAB42oWRu0oDQRSGvzFRiEjURiTVFrbGRIjXSpQ0gkVE05rLGqMbL9kkoE/gkwj2VhapvTyBjc9h6b+zQxIWQZaZ8838Z/5zZhaY55UUJp0BLjViNixoFfMUWR4cp9jg0XEaj6HjaXJ8O57R/o/jDEtm2fEcKybveJGsqTh+U86Z43cKZuD4QzlPjj+ZNS8xf6XImSH73HDLPV3atLigp6rPGusUKKpLj7pUj0PNAb6oqjkgtPt5rfe0ChTHDqFd+Yq+4kBzU5lNVYq0c8VrUUX7Lfo6W1NWURkF++1yompVjkTJM6vWd3wqqXsJ11PbQajOIt2bqPKf89/3jW7X03uF7LCmryPtymXmacirk/CJTtcn+os7KNvX8jiQ2rCvvW21knxLbNm5NPoLm+rWl0fNuvbk1XU3Ko98j7mT2pYS1Q9+AfhBY1UAeNpt0UdMVHEUxeHfpczA0DvYFUFRwPfeMBT7DDhYUGyISlEUmBlFRHBUbGgssUSCMWEHEXWjiZpoosaEFQsVe0ACLlzbMCzUnYno+7vzbL7kLM7iXgL4m19e6vlfhkECJJBAggjGgpUQQrERRjgRRBJFNDHEEkc8CSSSRDIpTGAik5jMFKYyjenMIJWZpJHOLGaTwRzmkkkW2cxDQ8fATg4OcskjnwLms4CFLGIxS1iKExeFFLEMN8UsZwUrWUUJq1lDKWtZx3o2sJEyNlHOZrawlQoqqaKabWynRoK4zmnO0EsnHzlLOxfp4iY3JJgLvOcUV8QiVi5xjj4+SAjd3OIH3/nJNW7zjCfcYQc76aCW59TxlH5e84KXvOLT+O0GeMNb7uJhjMsMMcg7vHxhlPPswsdu9tBAI1fZyz6aaKYFP/s5wEE+c4jDtHKEYxzlET20cZwTnOQr33g8/oMRCRWbhEm4REikREm0xEisxEm8JEgi97jPAx5KkiRLisXT0Nrk1U0Mq7/Rp2lOTVlk6lK9y64s+KOhaZpSVxpKuzJH6VDmKvOU+cp/e05TXe3quq3e5/E319XWtHjNynCbOkwd7sLfiLiJTAAAALgB/4WwAY0AS7AIUFixAQGOWbFGBitYIbAQWUuwFFJYIbCAWR2wBitcWACwASBFsAMrRAGwAiBFsAMrRLADIEW6AAJ//wACK7EDRnYrRFmwFCsAAAABWkasjwAA) format('woff');\n}\n\n.conceal:not(:invalid):not(:focus) {\n  font-family: 'dotsfont' !important;\n  font-size: 6px;\n  }\n\n.conceal PRE {\n  font-family: 'dotsfont' !important;\n  font-size: 10px;\n}\n\n\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/fonts/_fontstack.scss",
    "content": "/* poppins-300 - latin */\n@font-face {\n  font-family: 'Poppins';\n  font-style: normal;\n  font-weight: normal;\n  src: local(''),\n       url('@pkg/assets/fonts/poppins/poppins-v15-latin-300.woff2') format('woff2'), /* Super Modern Browsers */\n       url('@pkg/assets/fonts/poppins/poppins-v15-latin-300.woff') format('woff'), /* Modern Browsers */\n}\n\n/* poppins-500 - latin */\n@font-face {\n  font-family: 'Poppins';\n  font-style: normal;\n  font-weight: bold;\n  src: local(''),\n       url('@pkg/assets/fonts/poppins/poppins-v15-latin-500.woff2') format('woff2'), /* Super Modern Browsers */\n       url('@pkg/assets/fonts/poppins/poppins-v15-latin-500.woff') format('woff'), /* Modern Browsers */\n}\n\n/* lato-regular - latin */\n@font-face {\n  font-family: 'Lato';\n  font-style: normal;\n  font-weight: normal;\n  src: local(''),\n       url('@pkg/assets/fonts/lato/lato-v17-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */\n       url('@pkg/assets/fonts/lato/lato-v17-latin-regular.woff') format('woff'), /* Modern Browsers */\n}\n\n/* lato-700 - latin */\n@font-face {\n  font-family: 'Lato';\n  font-style: normal;\n  font-weight: bold;\n  src: local(''),\n       url('@pkg/assets/fonts/lato/lato-v17-latin-700.woff2') format('woff2'), /* Super Modern Browsers */\n       url('@pkg/assets/fonts/lato/lato-v17-latin-700.woff') format('woff'), /* Modern Browsers */\n}\n\n/* roboto-mono-regular - latin */\n@font-face {\n  font-family: 'Roboto Mono';\n  font-style: normal;\n  font-weight: normal;\n  src: local(''),\n       url('@pkg/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */\n       url('@pkg/assets/fonts/roboto-mono/roboto-mono-v13-latin-regular.woff') format('woff'), /* Modern Browsers */\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/fonts/_icons.scss",
    "content": "@use 'sass:math';\n\n@import \"~rancher-icons/style.scss\";\n\n// Animated Icons\n// --------------------------\n.icon-spin {\n  -webkit-animation: icon-spin 5000ms infinite linear;\n          animation: icon-spin 5000ms infinite linear;\n}\n\n@-webkit-keyframes icon-spin {\n  from {\n      transform:rotate(0deg);\n  }\n  to {\n      transform:rotate(360deg);\n  }\n}\n\n@keyframes icon-spin {\n  from {\n      transform:rotate(0deg);\n  }\n  to {\n      transform:rotate(360deg);\n  }\n}\n\n// FontAwesomeness\n$icon-li-width:         math.div(30em, 14) !default;\n$icon-inverse:          #fff !default;\n\n.icon {\n  display: inline-block;\n}\n\n// Sizes\n.icon-fw {\n  width: math.div(18em, 14);\n  text-align: center;\n}\n\n.icon-sm {\n  font-size: (1em*0.8);\n}\n\n.icon-lg {\n  font-size: math.div(4em, 3);\n  line-height: (3em * 0.25);\n  vertical-align: -15%;\n}\n.icon-2x { font-size: 2em; }\n.icon-3x { font-size: 3em; }\n.icon-4x { font-size: 4em; }\n.icon-5x { font-size: 5em; }\n\n// Stacked\n.icon-stack {\n  position: relative;\n  display: inline-block;\n  // width: 2em;\n  height: 2em;\n  line-height: 2em;\n  vertical-align: middle;\n}\n.icon-stack-1x, .icon-stack-2x {\n  position: absolute;\n  left: 0;\n  width: 100%;\n  text-align: center;\n}\n.icon-stack-1x { line-height: inherit; }\n.icon-stack-2x { font-size: 2em; }\n.icon-inverse { color: $icon-inverse; }\n\n// List\n.icon-ul {\n  padding-left: 0;\n  margin-left: $icon-li-width;\n  list-style-type: none;\n  > li { position: relative; }\n}\n\n.icon-li {\n  position: absolute;\n  left: -$icon-li-width;\n  width: $icon-li-width;\n  top: math.div(2em, 14);\n  text-align: center;\n  &.icon-lg {\n    left: -$icon-li-width + math.div(4em, 14);\n  }\n}\n\n.icon-rotate-90  { @include icon-rotate(90deg, 1);  }\n.icon-rotate-180 { @include icon-rotate(180deg, 2); }\n.icon-rotate-270 { @include icon-rotate(270deg, 3); }\n.icon-flip-horizontal { @include icon-flip(-1, 1, 0); }\n.icon-flip-vertical   { @include icon-flip(1, -1, 2); }\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/fonts/_zerowidthspace.scss",
    "content": "// Zero-width space font, for killing space between inline-block elements\n\n@font-face {\n  font-family: 'zerowidthspace';\n  font-weight: normal;\n  font-style: normal;\n  src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAakABEAAAAACYwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABgAAAABwAAAAceSKT/EdERUYAAAGcAAAAIgAAACYAJwAsT1MvMgAAAcAAAABKAAAAYGJnjp9jbWFwAAACDAAAAEoAAAFSBLks8GN2dCAAAAJYAAAAAgAAAAIAAAAAZnBnbQAAAlwAAAGxAAACZVO0L6dnYXNwAAAEEAAAAAgAAAAIAAAAEGdseWYAAAQYAAAANgAAADg03gskaGVhZAAABFAAAAA0AAAANgPvJ9JoaGVhAAAEhAAAAB4AAAAkBEYD32htdHgAAASkAAAAFQAAABgKqv8zbG9jYQAABLwAAAAOAAAADgBEADxtYXhwAAAEzAAAAB8AAAAgASAADG5hbWUAAATsAAABUgAAAq4ayl+KcG9zdAAABkAAAAAsAAAAPmMjcrlwcmVwAAAGbAAAAC4AAAAusPIrFHdlYmYAAAacAAAABgAAAAa9JVnfAAAAAQAAAADUUbVqAAAAAM7LcO4AAAAA1gVto3jaY2BkYGDgAWIxBjkGJgZGIGQFYhagCBMQM0IwAAipAFQAAHjaY2BhYWD8wsDKwMJqzDqDgYFRHkIzX2VIYRJgYGBiYGNmgAEECwgC0lxTGA4wKKj+YUv7l8bAwOLCoAEUZkRSosDACADyugnvAAB42mNgYGBmgGAZBkYGEPAB8hjBfBYGAyDNAYRMQFqBYYHqn///Eaz/j/+n3OKE6gIDRjYGOJcRpIeJARUwQqwaGoCFLF0AOm0M0gAAAAAAAHjaXVG7TltBEN0NDwOBxNggOdoUs5mQxnuhBQnE1Y1iZDuF5QhpN3KRi3EBH0CBRA3arxmgoaRImwYhF0h8Qj4hEjNriKI0Ozuzc86ZM0vKkap36WvPU+ckkMLdBs02/U5ItbMA96Tr642MtIMHWmxm9Mp1+/4LBpvRlDtqAOU9bykPGU07gVq0p/7R/AqG+/wf8zsYtDTT9NQ6CekhBOabcUuD7xnNussP+oLV4WIwMKSYpuIuP6ZS/rc052rLsLWR0byDMxH5yTRAU2ttBJr+1CHV83EUS5DLprE2mJiy/iQTwYXJdFVTtcz42sFdsrPoYIMqzYEH2MNWeQweDg8mFNK3JMosDRH2YqvECBGTHAo55dzJ/qRA+UgSxrxJSjvjhrUGxpHXwKA2T7P/PJtNbW8dwvhZHMF3vxlLOvjIhtoYEWI7YimACURCRlX5hhrPvSwG5FL7z0CUgOXxj3+dCLTu2EQ8l7V1DjFWCHp+29zyy4q7VrnOi0J3b6pqqNIpzftezr7HA54eC8NBY8Gbz/v+SoH6PCyuNGgOBEN6N3r/orXqiKu8Fz6yJ9O/sVoAAAAAAQAB//8AD3jaY2D6b8zAwHCWxYWBmYGdgUFdUFGQ1VhQ+SzjrH9nz5xJYy7/05kGlGRgZEAChgwAYkELOgAAeNpjYGRgYABincimc/H8Nl8ZuDkYQODc6YJ3IPoaa+7i/8ZAxlkWFyDJwcAEEgUAM5AKvXjaY2BkYGBx+X8DSDL8N2ZgYDjLABRBAWwAa5AEKwAAeNpjYfhvzAAETKsY4IAFiAEktwHnAAAAAAAAFAAUABQAFAAUABwAAHjaY2BkYGBgY+BgYGIAASYGRiAWY2BgZIAAAAMcAC4AeNqFUctKw0AUPWOqYBGXLlyUWSrYGCtW7LbQhSAWKgruUjuxkfpKUsQu/QjX4ke4dqnVH/AH/ALX4snMNShCZZg7594599yTCYA53MKDKs0CuOd2WKHCzOEpch4FezjBq+ASVlRd8DS06gqeQUXdCH7CgroT/IxAPQgeY169C35BWX04/OZhUX2iiQQGITLGHjS6uGbcZiXFOc6I2/AZm8yO0Cc7ZNWg/KfzCjFxn6hlOzN7JjjmvUaNKgHPJTIyrgs0sMoVCTcquD4nR4z5lAzLGPFMeJvr9+yElN0h3RiiHTs9xhCnE+c2uA9FqYqDQkuj80NNiw9NhuFbuL4tdqxRqcodEK3/40n/crVvuSkz956B1fDtmTubrOV8fL+Sls49sobMdsly1ZqNG/QboM7oKs7vJnUNNUL2DMjPv9C5aRW6HVzyNuZN/lcHX4j5amYAAHjaY2BiAIO/5xnSGLABNiBmZGBiYGZkYmRmL83LNDBwNADRRqZuzgCcZgavuAH/hbABjQBLsAhQWLEBAY5ZsUYGK1ghsBBZS7AUUlghsIBZHbAGK1xYWbAUKwAAAAFZ370kAAA=) format('woff');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_button.scss",
    "content": "$btn-padding: 0 21px 0 21px;\n$btn-sm-padding: 0 7px 0 7px;\n$btn-height: 40px;\n$btn-sm-height: 30px;\n\n// -----------------------------------------------------------------------------\n// This file contains all styles related to the button component.\n// -----------------------------------------------------------------------------\n.btn,\nbutton,\n[class^='btn-'] {\n  display: inline-block;\n  text-align: center;\n  white-space: nowrap;\n  vertical-align: middle;\n  cursor: pointer;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  border: 0;\n  padding: $btn-padding;\n  border-radius: var(--border-radius);\n  color: var(--lightest);\n  line-height: $btn-height;\n  min-height: $btn-height;\n\n  &:hover {\n    text-decoration: none;\n    color: var(--lightest);\n  }\n\n  &.bg-transparent {\n    color: var(--body-text);\n  }\n}\n\n//icon button\n.icon-btn {\n  padding: 0;\n  line-height: initial;\n\n  &.btn-sm {\n    padding: 0;\n  }\n\n  span {\n    padding: 0 10px 0 5px;\n    vertical-align: middle;\n  }\n}\n\n.btn-sm,\n.btn-group-sm > .btn,\n.btn-sm .btn-label {\n  padding: $btn-sm-padding;\n  min-height: $btn-sm-height;\n  line-height: 28px;\n}\n\n//btn roles\n.role-primary {\n  background: var(--primary);\n  color: var(--primary-text);\n\n  &:hover {\n    background-color: var(--primary-hover-bg);\n    color: var(--primary-text);\n  }\n\n  &:focus {\n    background-color: var(--primary-hover-bg);\n    color: var(--primary-text);\n  }\n}\n\n.role-secondary {\n  background: transparent;\n  color: var(--primary) !important;\n  border: solid 1px var(--primary);\n  line-height: $btn-height - 2px;\n}\n\n.role-tertiary {\n  background: var(--accent-btn);\n  border: solid 1px var(--primary);\n  color: var(--primary);\n}\n\n.role-danger {\n  background: var(--error);\n\n  &:hover {\n    background-color: var(--error-hover-bg);\n  }\n}\n\n.role-link {\n  background: transparent;\n  color: var(--link) !important;\n}\n\n.role-multi-action {\n  background: var(--accent-btn);\n  border: solid thin var(--primary);\n  color: var(--primary);\n  border-radius: 2px;\n}\n\n.icon-group i {\n  font-size: 1.5em;\n}\n\n//disabled\n.btn-disabled,\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n  cursor: not-allowed;\n  color: var(--disabled-text) !important;\n  &:not(.role-link){\n    background-color: var(--disabled-bg) !important;\n  }\n}\n\n.btn-group {\n  position: relative;\n  text-align: initial;\n  vertical-align: middle;\n  padding: 0;\n  border-radius: var(--border-radius);\n\n  .btn {\n    position: relative;\n    display: inline-block;\n    border-radius: 0;\n    text-align: center;\n\n    &:focus {\n      // Move the focused one to the top so that the focus ring is all visible\n      z-index: 1;\n    }\n\n    &.active {\n      @extend .bg-primary;\n    }\n\n    &:first-child {\n      border-top-left-radius: var(--border-radius);\n      border-bottom-left-radius: var(--border-radius);\n    }\n\n    &:last-child {\n      border-top-right-radius: var(--border-radius);\n      border-bottom-right-radius: var(--border-radius);\n    }\n  }\n\n  .btn[disabled] {\n    // Ensure disabled button's border remains as-is; otherwise, button appears vertically shorter than others in group\n    border: inherit;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_cards.scss",
    "content": ".links {\n  display: flex;\n  flex-wrap: wrap;\n  width: 100%;\n\n  .link-container {\n    position: relative;\n    background-color: var(--input-bg);\n    border-radius: var(--border-radius);\n    border: solid 1px var(--input-border);\n    display: flex;\n    flex-basis: 40%;\n    margin: 0 10px 10px 0;\n    max-width: 325px;\n    min-height: 100px;\n    border-left: solid 10px var(--primary);\n\n    a[disabled], &.disabled {\n      cursor: not-allowed;\n      color: var(--disabled-text);\n    }\n\n    &.disabled{\n      background-color: var(--disabled-bg);\n      border-left: solid 10px var(--disabled-text);\n      > * {\n        opacity: .3;\n      }\n\n     .disabled-msg{\n      position:absolute;\n      color: var(--error);\n      z-index: z-index('hoverOverContent');\n      opacity: 1;\n      top: 0px;\n      bottom: 0px;\n      left: 0px;\n      right: 0px;\n      display: flex;\n      justify-content: center;\n      align-items: flex-end;\n      }\n    }\n\n    &:hover:not(.disabled) {\n      box-shadow: 0px 0px 1px var(--outline-width) var(--outline);\n    }\n\n    > * {\n      align-items: center;\n      display: flex;\n      flex: 1 0;\n      padding: 10px;\n\n      .link-logo,\n      .link-content {\n        display: inline-block;\n        height: 100%;\n      }\n\n      .link-logo {\n        text-align: center;\n        width: 60px;\n        height: 60px;\n        border-radius: calc(2 * var(--border-radius));\n        background-color: white;\n\n        img {\n          width: 56px;\n          height: 56px;\n          -o-object-fit: contain;\n          object-fit: contain;\n          position: relative;\n          top: 2px;\n        }\n      }\n\n      .link-content {\n        width: 100%;\n        margin-left: 10px;\n      }\n\n      .description {\n        margin-top: 10px;\n        display: -webkit-box;\n        -webkit-box-orient: vertical;\n        // -webkit-line-clamp: 3;\n        // line-clamp: 3;\n        text-overflow: ellipsis;\n        color: var(--secondary);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_columns.scss",
    "content": "@use 'sass:math';\n\n@import \"@pkg/assets/styles/base/_functions\";\n@import \"@pkg/assets/styles/base/_variables\";\n\n\n$COLUMNS: 6 10 11 12 23 24;\n$DEFAULT_COLUMNS: 12;\n\n/* SECTIONS */\n.section {\n  clear: both;\n  padding: 0px;\n  margin: 0px;\n}\n\n/* COLUMN SETUP */\n.col {\n  flex: 0 0 auto;\n  margin: 0 $column-gutter 0 0;\n\n  &:last-child {\n    margin-right: 0;\n  }\n\n  &.equal-height {\n    display: flex;\n    flex: 1;\n  }\n}\n\n/* ROWS */\n.row {\n  display: flex;\n  // margin-bottom: 20px;\n}\n\n.row:before,\n.row:after {\n  content:\"\";\n  display:table;\n}\n.row:after {\n  clear:both;\n}\n\n/* COLUMNS */\n@each $cols in $COLUMNS {\n  $span: $cols;\n  $suffix: null;\n\n  @if ( $cols != $DEFAULT_COLUMNS ) {\n    $suffix: -of-#{$cols}\n  }\n\n  @while $span > 0 {\n    // Normal column with a gutter\n    .span-#{$span}#{$suffix} { width: decimal-round( (math.div(100 - ($column-gutter * ($cols - 1)), $cols) * $span) + (($span - 1) * $column-gutter) , 3, 'floor'); }\n\n    // Gutterless column\n    .gutless .span-#{$span}#{$suffix} { margin-right: 0; width: decimal-round( math.div($span, $cols) * 100%, 3, 'floor'); }\n\n    // Offsets\n    .offset-#{$span}#{$suffix} {\n      margin-left: decimal-round( (math.div($span, $cols)*100%) + $span*math.div($column-gutter, $cols), 3, 'floor' );\n    }\n\n    // Gutterless offset\n    .gutless .offset-#{$span}#{$suffix} { margin-left: decimal-round( (math.div($span, $cols)*100%), 3, 'floor' ); }\n\n    $span: $span - 1;\n  }\n}\n\n//flex grid\n.container-flex {\n  display: flex;\n  flex-wrap: wrap;\n\n  .flex-item-half {\n    flex-grow: 1;\n    display: flex;\n    width: 50%;\n  }\n}\n\n.flex-justify-center {\n  justify-content: center;\n}\n\n.flex-justify-right {\n  justify-content: right;\n}\n\n.flex-justify-left {\n  justify-content: left;\n}\n\n.container-flex-center {\n  @extend .container-flex;\n  align-items: center;\n}\n\n// Equal height columns\n.row-full-height {\n  height: 100%;\n}\n\n.col-full-height {\n  height: 100%;\n  vertical-align: middle;\n}\n\n.row-same-height {\n  display: table;\n  width: 100%;\n  /* fix overflow */\n  table-layout: fixed;\n}\n\n\n//breakpoint stuff here\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_form.scss",
    "content": "$spacing: 10px;\n\nINPUT:not([type]),\nINPUT[type='text'],\nINPUT[type='password'],\nINPUT[type='number'],\nINPUT[type='date'],\nINPUT[type='email'],\nINPUT[type='search']:not(.vs__search),\nINPUT[type='tel'],\nINPUT[type='url'],\nSELECT,\nTEXTAREA,\n.labeled-input,\n.labeled-select,\n.unlabeled-input,\n.unlabeled-select {\n  position: relative;\n  display: block;\n  box-sizing: border-box;\n  width: 100%;\n  opacity: 1;\n  padding: $input-padding-sm;\n  background-color: var(--input-bg);\n  border-radius: var(--border-radius);\n  border: solid var(--border-width) var(--input-border);\n  color: var(--input-text);\n\n  @include input-status-color;\n\n  LABEL {\n    color: var(--input-label);\n  }\n  &:hover {\n    &, .vs__dropdown-menu {\n      background: var(--input-hover-bg);\n    }\n  }\n\n  &::placeholder {\n    color: var(--input-placeholder);\n  }\n\n  &.disabled, &.disabled .selected, &[disabled], &[disabled]:hover, &.view {\n    color: var(--input-disabled-text);\n    background-color: var(--input-disabled-bg);\n    outline-width: 0;\n    border-color: var(--input-disabled-border);\n    cursor: not-allowed;\n    label {\n      color: var(--input-disabled-label);\n      display: inline-block;\n      z-index: 1;\n    }\n    &::placeholder {\n        color: var(--input-disabled-placeholder);\n    }\n  }\n\n  LABEL {\n    margin: $spacing 0 0 0;\n  }\n}\n\nINPUT[type='search']:not(.vs__search) {\n  padding: calc(#{$input-padding-sm} + 2px);\n}\n\nTEXTAREA {\n  padding: $input-padding-lg 10px 10px 10px;\n  line-height: $input-line-height;\n}\n\nFORM {\n  LABEL {\n    color: var(--input-label);\n    display: inline-block;\n    margin: $spacing 0 $spacing 0;\n    font-size: 12px;\n\n    .radio-label,\n    .checkbox-label {\n      font-size: 14px;\n    }\n\n    &.radio, &.checkbox {\n      cursor: pointer;\n      margin: 5px 0;\n\n      > INPUT {\n        margin-right: 5px;\n      }\n    }\n\n    &.radio + LABEL.radio,\n    &.checkbox + LABEL.checkbox {\n      margin-left: 20px;\n    }\n  }\n\n  .actions {\n    padding-top: $spacing;\n  }\n\n  .detail {\n    margin-top: 2px;\n    @extend .text-small;\n    color: var(--muted);\n  }\n\n  .group {\n    border: 1px solid var(--input-border);\n    padding: 20px;\n  }\n}\n\n.field-required {\n  color: var(--error);\n  font-weight: bold;\n}\n\nINPUT.inline-input {\n  display: inline-block;\n  width: 75px;\n  margin: 0 10px;\n}\n\n.input-title {\n    clear: both;\n    margin-left: 24px;\n    font-size: 12px;\n}\n\n.fixed select, .fixed.v-select, .fixed input:not(.vs__search){\n  height: 50px;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_gauges.scss",
    "content": "@media only screen and (min-width: map-get($breakpoints, \"--viewport-7\")) {\n  .resource-gauges {\n    grid-template-columns: 1fr 1fr;\n  }\n\n  .hardware-resource-gauges {\n    &,\n    &.live {\n      grid-template-columns: 1fr;\n    }\n  }\n}\n@media only screen and (min-width: map-get($breakpoints, \"--viewport-9\")) {\n  .resource-gauges {\n    grid-template-columns: 1fr 1fr 1fr;\n  }\n\n  .hardware-resource-gauges {\n    grid-template-columns: 1fr 1fr 1fr;\n\n    &.live {\n      grid-template-columns: 1fr 1fr;\n    }\n  }\n}\n@media only screen and (min-width: map-get($breakpoints, \"--viewport-12\")) {\n  .resource-gauges {\n    grid-template-columns: 1fr 1fr 1fr;\n  }\n}\n\n.resource-gauges {\n  display: grid;\n  grid-column-gap: 10px;\n  grid-row-gap: 15px;\n  margin-top: 25px;\n\n  & > * {\n    width: 100%;\n    height: 100%;\n  }\n}\n\n.hardware-resource-gauges {\n  display: grid;\n  grid-column-gap: 15px;\n  grid-row-gap: 20px;\n  &:first-of-type {\n    margin-top: 35px;\n  }\n\n  & > * {\n    width: 100%;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_labeled-input.scss",
    "content": ".labeled-input {\n  position: relative;\n  display: table;\n  border-collapse: separate;\n  min-height: $input-height;\n\n  LABEL {\n    position: absolute;\n    transform: translate(0, -10px) scale(1);\n    transform-origin: top left;\n    transition-property: transform, font-size;\n    transition-duration: 0.1s;\n    transition-timing-function: ease-in-out;\n    color: var(--input-label);\n    pointer-events: none;\n    i {\n      pointer-events: initial;\n    }\n  }\n\n  .corner {\n    top: 5px;\n    right: 10px;\n    margin: 0;\n    padding: 0;\n    text-align: right;\n    z-index: 3;\n    transform: none !important;\n  }\n\n  .required {\n    color: var(--error);\n  }\n\n  INPUT, SELECT {\n    position: relative;\n    font-size: 14px;\n    display: block;\n    width: 100%;\n  }\n\n  SELECT.empty {\n    color: var(--input-placeholder);\n  }\n\n  SELECT {\n    -webkit-appearance: textfield;\n    user-select: none;\n  }\n\n  INPUT, INPUT:hover, INPUT:focus,\n  TEXTAREA, TEXTAREA:hover, TEXTAREA:focus,\n  SELECT, SELECT:hover, SELECT:focus {\n    border: none;\n    background-color: transparent;\n    outline: 0;\n    box-shadow: none;\n    padding: $input-padding-lg 0 0 0;\n    line-height: calc(#{$input-line-height} + 1px);\n\n    &.no-label {\n      padding: $input-padding-sm 0px $input-padding-sm 0px;\n    }\n  }\n\n  &.view > DIV:not(.addon) {\n    font-size: 14px;\n    padding: $input-padding-lg 0 0 0;\n\n    &.no-label {\n      padding-top:0px;\n    }\n  }\n\n  &.create,\n  &.edit,\n  &.view {\n    .addon,\n    .addon.btn {\n      display: table-cell;\n      vertical-align: middle;\n      width: 1%;\n      white-space: nowrap;\n      vertical-align: middle;\n      color: #{$secondary};\n    }\n\n    .addon {\n      padding: 6px 12px;\n      font-size: 14px;\n      font-weight: normal;\n      line-height: 1;\n      text-align: center;\n      border-left: solid thin #{$secondary};\n    }\n  }\n\n  &.suffix INPUT {\n    padding-right: 8px;\n  }\n\n  .cron-label{\n    position: absolute;\n    top: 100%;\n    padding-top: 5px;\n    left: 0;\n    color: var(--input-label);\n  }\n\n\n\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_resource.scss",
    "content": ".create-resource-container {\n  .subtypes-container {\n    display: flex;\n    flex-wrap: wrap;\n    width: 100%;\n  }\n  .subtype-content {\n    width: 100%;\n  }\n\n  .subtype-banner {\n    border-left: 5px solid var(--primary);\n    border-radius: var(--border-radius);\n    display: flex;\n    flex-basis: 40%;\n    margin: 10px;\n    min-height: 80px;\n    padding: 10px;\n    box-shadow: 0 0 20px var(--shadow);\n\n    &.disabled {\n      cursor: not-allowed !important;\n      background-color: var(--disabled-bg);\n    }\n\n    &.selected {\n      background-color: var(--accent-btn);\n    }\n\n    &.top {\n      background-image: linear-gradient(\n        -90deg,\n        var(--body-bg),\n        var(--accent-btn)\n      );\n\n      h2 {\n        margin: 0px;\n      }\n    }\n\n    .title {\n      align-items: center;\n      display: flex;\n      width: 100%;\n\n      h5 {\n        margin: 0;\n      }\n\n      .flex-right {\n        margin-left: auto;\n      }\n    }\n\n    .description {\n      color: var(--input-label);\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n    }\n\n    &:not(.top) {\n      align-items: top;\n      flex-direction: row;\n      justify-content: start;\n      &:hover {\n        cursor: pointer;\n        box-shadow: 0px 0px 1px var(--outline-width) var(--outline);\n      }\n    }\n\n    .round-image {\n      border-radius: 50%;\n      height: 50px;\n      margin-right: 10px;\n      width: 50px;\n      overflow: hidden;\n    }\n\n    .banner-abbrv {\n      align-items: center;\n      background-color: var(--primary);\n      color: white;\n      display: flex;\n      font-size: 2.5em;\n      height: 100%;\n      justify-content: center;\n      width: 100%;\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_select.scss",
    "content": ".labeled-select {\n  cursor: text;\n  padding: 0;\n  width: 100%;\n\n  .selected {\n    padding-top: $input-padding-lg;\n  }\n}\n\n.col {\n  > .labeled-select:not(.taggable),\n  > .unlabeled-select:not(.taggable) {\n    min-height: $input-height;\n    padding-bottom: calc(#{$input-padding-sm}/2);\n  }\n}\n\n.labeled-select,\n.unlabeled-select {\n  min-width: 75px;\n  // line-height: $input-line-height;\n\n  .required {\n    color: var(--error);\n  }\n\n  .v-select {\n    &.inline {\n\n      .vs__search {\n        background-color: transparent;\n      }\n\n      .vs__dropdown-toggle,\n      .vs__dropdown-toggle > * {\n        background-color: transparent;\n        border: transparent;\n      }\n\n      .vs__dropdown-menu {\n        outline: none;\n      }\n\n      .selected {\n        position: relative;\n        top: 1.4em;\n      }\n    }\n  }\n  .v-select.inline.vs--single {\n\n    &.vs--searching .vs__selected {\n      display: none;\n    }\n    &:not(.vs--searching) {\n      .vs__selected-options {\n        overflow: hidden;\n        flex-wrap: nowrap;\n        .vs__selected {\n          overflow: hidden;\n          white-space: nowrap;\n          text-overflow: ellipsis;\n          display: inline-block;\n        }\n      }\n    }\n  }\n\n  .v-select.inline:not(.vs--single) {\n    margin-bottom: -5px; // targets multi-select tag boxes to make the same size as rows next to it\n    min-height: 30px;\n\n    .vs__selected {\n      min-height: 25px;\n      padding: 0 7px;\n\n      &:not(:only-child) {\n        margin-bottom: 3px;\n      }\n    }\n  }\n\n  &.focused {\n    outline: none;\n    box-shadow: 0 0 0 var(--outline-width) var(--outline);\n    .v-select {\n      // Can toggle this to get full width dd - maybe make an option?\n      .vs__dropdown-menu {\n        min-width: max-content;\n        background: var(--dropdown-bg);\n      }\n    }\n  }\n}\n\n.unlabeled-select {\n  background-color: var(--input-bg);\n  border-radius: var(--border-radius);\n  color: var(--input-text);\n  padding: 3px 0;\n\n  &.disabled {\n    border: solid var(--border-width) var(--input-disabled-border);\n\n    .vs__dropdown-toggle, input {\n      cursor: not-allowed;\n    }\n  }\n\n  .vs--single .vs__selected-options {\n    flex-wrap: nowrap;\n  }\n\n  .v-select {\n\n    &.inline {\n      height: 100%;\n\n      .vs__dropdown-toggle {\n        height: 100%;\n      }\n      .vs__actions {\n        width: auto;\n      }\n    }\n  }\n\n  &:not(.view) {\n    background-color: var(--input-bg);\n    border: solid var(--border-width) var(--input-border);\n\n    &:hover {\n      &,\n      .vs__dropdown-menu {\n        background: var(--input-hover-bg);\n      }\n    }\n\n    &.disabled .v-select {\n      background-color: var(--input-disabled-bg);\n      border-color: var(--input-disabled-border);\n      cursor: not-allowed;\n\n      .vs__dropdown-toggle, input {\n        cursor: not-allowed;\n      }\n      .vs__selected {\n        color: var(--input-disabled-text);\n      }\n    }\n  }\n\n  .labeled-tooltip .status-icon {\n    top: $input-padding-sm;\n  }\n}"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_table.scss",
    "content": "//bordered table\n.bordered-table {\n  width: 100%;\n  max-width: 100%;\n  margin-bottom: 1rem;\n  // background-color: var(--body-bg);\n  border-collapse: collapse;\n\n  th,\n  td {\n    padding: 12px;\n    border-top: 1px solid var(--border);\n  }\n\n  thead th {\n    vertical-align: bottom;\n    border-bottom: 2px solid var(--border);\n  }\n\n  tbody + tbody {\n    border-top: 2px solid var(--border);\n  }\n\n  table {\n    // background-color: var(--body-bg);\n  }\n}\n\n\n//zebra table\n.zebra-table {\n  border-collapse: collapse;\n  margin: 25px 0;\n  min-width: 400px;\n  border-radius: 5px 5px 0 0;\n  overflow: hidden;\n  box-shadow: 0 0 20px var(--shadow);\n\n  thead {\n    tr {\n      background-color: var(--sortable-table-header-bg);\n      color: #ffffff;\n      text-align: left;\n    }\n  }\n\n  th {\n    padding: 12px 15px;\n    font-weight: normal;\n    border: 0;\n  }\n\n  td {\n    padding: 12px 15px;\n    border: 0;\n  }\n\n  tbody {\n    tr {\n      border-bottom: 1px solid var(--sortable-table-top-divider);\n\n      &:nth-of-type(even) {\n        background-color: var(--sortable-table-accent-bg);\n      }\n\n      &:last-of-type {\n        border-bottom: 2px solid var(--sortable-table-top-divider);\n      }\n    }\n\n    tr.active-row {\n      color: var(--sortable-table-header-bg);\n    }\n  }\n}\n\n .for-inputs{\n   & TABLE.sortable-table {\n    width: 100%;\n    border-collapse: collapse;\n    margin-bottom: $spacing;\n\n    >TBODY>TR>TD, >THEAD>TR>TH {\n      padding-right: $spacing;\n      padding-bottom: $spacing;\n\n      &:last-of-type {\n        padding-right: 0;\n      }\n    }\n\n    >TBODY>TR:first-of-type>TD {\n      padding-top: $spacing;\n    }\n\n    >TBODY>TR:last-of-type>TD {\n      padding-bottom: 0;\n    }\n  }\n\n    &.edit, &.create, &.clone {\n     TABLE.sortable-table>THEAD>TR>TH {\n      border-color: transparent;\n      }\n    }\n  }\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/global/_tooltip.scss",
    "content": ".v-popper__popper.v-popper--theme-tooltip {\n  $triangle-size: 8px;\n  $triangle-inner-size: $triangle-size - 1px;\n  $center: calc(50% - #{$triangle-size});\n\n  display: block !important;\n  z-index: z-index('tooltip');\n  max-width: 50vw;\n\n  .v-popper__inner {\n    background: var(--tooltip-bg);\n    color: var(--tooltip-text);\n    border-radius: var(--border-radius);\n    padding: 8px;\n  }\n\n  .v-popper__arrow-container {\n    border: 0 solid transparent;\n    z-index: 1;\n    position: absolute;\n    width: $triangle-size;\n    height: $triangle-size;\n\n    .v-popper__arrow-outer {\n      border-radius: 0;\n      border: $triangle-size solid transparent;\n      width: 0;\n      height: 0;\n      box-sizing: content-box;\n      margin-left: -2px;\n    }\n    .v-popper__arrow-inner {\n      border: $triangle-inner-size solid transparent;\n      border-radius: 0;\n      width: 0;\n      height: 0;\n      box-sizing: content-box;\n      margin-left: -1px;\n    }\n  }\n\n  :is(&[data-popper-placement^=\"top\"], &[data-popper-placement^=\"bottom\"]) {\n    .v-popper__arrow-inner {\n      margin-top: -$triangle-size;\n    }\n  }\n\n  :is(&[data-popper-placement^=\"left\"], &[data-popper-placement^=\"right\"]) {\n    .v-popper__arrow-inner {\n      margin-top: calc(-2 * $triangle-size);\n    }\n  }\n\n  &[data-popper-placement^=\"top\"] {\n    .v-popper__wrapper {\n      margin-bottom: $triangle-size;\n    }\n    .v-popper__arrow-container > * {\n      border-top-color: var(--tooltip-bg);\n      border-bottom-width: 0;\n    }\n  }\n\n  &[data-popper-placement^=\"bottom\"] {\n    .v-popper__inner {\n      margin-top: $triangle-size;\n    }\n    .v-popper__arrow-container {\n      top: 0;\n      & > * {\n        border-bottom-color: var(--tooltip-bg);\n        border-top-width: 0;\n      }\n    }\n  }\n\n  &[data-popper-placement^=\"right\"] {\n    .v-popper__inner {\n      margin-left: calc(#{$triangle-size} - 2px);\n    }\n    .v-popper__arrow-container > * {\n      border-right-color: var(--tooltip-bg);\n      border-left-width: 0;\n    }\n  }\n\n  &[data-popper-placement^=\"left\"] {\n    .v-popper__inner {\n      margin-right: $triangle-size;\n    }\n    .v-popper__arrow-container > * {\n      border-left-color: var(--tooltip-bg);\n      border-right-width: 0;\n    }\n  }\n\n  &.tooltip-warning {\n    .v-popper__inner {\n      background: var(--tooltip-bg-warning);\n      color: var(--tooltip-text-warning);\n    }\n\n    &[data-popper-placement^=\"top\"] {\n      .v-popper__arrow-container {\n        .v-popper__arrow-outer {\n          border-top-color: var(--tooltip-bg-warning);\n        }\n      }\n    }\n\n\n    &[data-popper-placement^=\"bottom\"] {\n      .v-popper__arrow-container {\n        .v-popper__arrow-outer {\n          border-bottom-color: var(--body-bg);\n        }\n      }\n    }\n\n    &[data-popper-placement^=\"right\"] {\n      .v-popper__arrow-container {\n        .v-popper__arrow-outer {\n          border-right-color: var(--tooltip-bg-warning);\n        }\n      }\n    }\n\n    &[data-popper-placement^=\"left\"] {\n      .v-popper__arrow-container {\n        .v-popper__arrow-outer {\n          border-left-color: var(--tooltip-bg-warning);\n        }\n      }\n    }\n  }\n}\n\n.v-popper__popper {\n  $color: var(--popover-bg);\n  top: 0;\n  left: 0;\n  border-radius: var(--border-radius-lg);\n\n  &:focus {\n    outline: none;\n  }\n\n  .v-popper__inner {\n    background: $color;\n    color: var(--popover-text);\n    padding: 0;\n    border-radius: var(--border-radius-lg);\n    overflow: hidden; /* for border-radius */\n    border: 1px solid var(--dropdown-border);\n\n    li {\n      padding: 10px;\n      &:not(.divider):hover {\n        background-color: var(--dropdown-hover-bg);\n        color: var(--dropdown-hover-text);\n        cursor: pointer;\n      }\n    }\n\n    a {\n      color: var(--popover-text);\n    }\n\n    .resize-observer, .resize-observer object {\n      position: absolute;\n    }\n  }\n\n  .v-popper__arrow-container {\n    border-color: transparent;\n    .v-popper__arrow-outer {\n      border-color: transparent;\n    }\n  }\n}\n\n.v-popper__popper.v-popper--theme-dropdown {\n  z-index: z-index('tooltip');\n\n  &.containerLogsDropdown, &.fleet-summary-tooltip{\n    .v-popper__arrow-container {\n      display: none;\n    }\n  }\n}\n\n.v-popper {\n  display: inline;\n}\n\n.v-popper__popper.v-popper--theme-tooltip,\n.v-popper {\n  &[aria-hidden='true'] {\n    // This removes it from the layout of ButtonDropDown (so it doesn't render huge for SSR) but\n    // still allows it to maintain its dimensions for v-tooltip to calculate the appropriate position.\n    position: absolute;\n    visibility: hidden;\n    opacity: 0;\n    transition: opacity .15s, visibility .15s;\n  }\n\n  &[aria-hidden='false'] {\n    visibility: visible;\n    opacity: 1;\n    transition: opacity .15s;\n  }\n}\n\n//icon tooltip\n.icon-info.v-popper--has-tooltip {\n  font-size: 14px;\n}\n\n.tooltip-footer {\n  top: 0;\n  font-size: 12px;\n\n  .v-popper__inner {\n    font-size: 12px;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/rancher-desktop.scss",
    "content": "/** Rancher Desktop specific styles */\nbody {\n  background-color: var(--body-bg);\n  color: var(--body-text);\n  font-size: 14px;\n}\n\n.labeled-input {\n    margin-bottom: 1em;\n}\nlabel > button:first-child:not(.btn-sm) {\n    margin-right: 1em;\n}\n\n.locked-radio .radio-container span.radio-custom {\n  &[aria-checked=\"true\"] {\n    opacity: 1;\n  }\n\n  &:not([aria-checked=\"true\"]) {\n    opacity: 1;\n    background-color: var(--radio-locked-bg);\n    box-shadow: var(--radio-locked-shadow);\n  }\n}\n\n@media screen and (prefers-color-scheme: dark) {\n    option {\n        background-color: var(--input-bg);\n    }\n}\n\n:is(.btn, button, [class*='btn-']):not([class*='role-']):not([class*=bg-primary]) {\n  /* buttons should have one of the role- classes:\n   * .role-primary, .role-secondary, .role-tertiary, .role-multi-action, etc.\n   */\n  outline: 10px dashed red !important;\n}\n\n.btn-icon-text {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n}\n\n:root {\n  --preferences-content-padding: 0.75rem;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/themes/_dark.scss",
    "content": ":root {\n  // Local variables for reused colors\n\n  //dark sidebar\n  $darkest: #141419;\n\n  //dark body\n  $darker: #1b1c21;\n\n  //dark inputs\n  $dark: #27292e;\n\n  //dark borders and button\n  $medium: #4a4b52;\n\n  // dark disabled,\n  $light: #6c6c76;\n\n  //dark secondary\n  $lighter: #b6b6c2;\n\n  // dark main text\n  $lightest: #ffffff;\n\n  $secondary: $lighter;\n  $disabled: $light;\n\n  //Contrast colors\n  $contrasted-dark: $lightest !default;\n  $contrasted-light: $darkest !default;\n\n\n  --default                    : #{$dark};\n  --default-text               : #{$light};\n  --default-hover-bg           : #{darken($dark, 10%)};\n  --default-hover-text         : #{saturate($lightest, 20%)};\n  --default-active-bg          : #{darken($dark, 25%)};\n  --default-active-text        : #{contrast-color(darken($dark, 25%))};\n  --default-border             : #($dark);\n  --default-banner-bg          : #{rgba($dark, 0.15)};\n  --default-light-bg           : #{rgba($dark, 0.05)};\n\n  --muted                      : #{$disabled};\n\n  --body-bg                    : #{$darker};\n  --body-text                  : #{$lightest};\n  --scrollbar-thumb            : #{$medium};\n  --scrollbar-thumb-dropdown   : #{$medium};\n\n  --header-bg                  : #{$darker};\n  --header-border              : #{$medium};\n  --header-btn-bg              : #{$dark};\n  --header-btn-text            : #{$lightest};\n  --footer-bg                  : #{$darker};\n  --footer-border              : #{$medium};\n\n  --nav-bg                     : #{$darkest};\n  --nav-active                 : var(--primary-active-bg);\n  --nav-border                 : #{$medium};\n  --nav-hover                  : var(--primary);\n  --nav-expander-hover         : var(--primary-banner-bg);\n\n  --disabled-bg                : #{darken($disabled, 10%)};\n  --disabled-text              : #{$secondary};\n  --box-bg                     : #{$darkest};\n  --subtle-border              : #{$darkest};\n  --border                     : #{$medium};\n\n  --topmenu-bg                 : #{$darkest};\n  --topmenu-text               : #{$lightest};\n  --topmost-border             : #{$medium};\n  --topmost-shadow             : #{lighten($darkest, 5%)};\n  --topmost-light-hover        : #{$medium};\n\n  --accent-btn                 : var(--primary-banner-bg);\n  --accent-btn-hover           : var(--primary);\n  --accent-btn-hover-text      : #{$lightest};\n\n  --modal-bg                   : #{$dark};\n  --modal-border               : #{$medium};\n  --overlay-bg                 : #{rgba($darkest, 0.75)};\n  --shadow                     : #{rgba($darkest, 0.9)};\n\n  --checkbox-tick              : #{$lightest};\n  --checkbox-border            : #{$medium};\n  --checkbox-tick-disabled     : #{lighten($disabled, 50%)};\n  --checkbox-disabled-bg       : #{$disabled};\n  --checkbox-tick-locked       : #{$darkest};\n  --checkbox-locked-bg         : #{lighten($disabled, 50%)};\n  --checkbox-ticked-bg         : var(--primary);\n  --checkbox-locked-border     : #{lighten($disabled, 50%)};\n  --checkbox-locked-shadow     : #{lighten($disabled, 50%)};\n\n  --dropdown-bg                : #{mix($medium, $dark, 10%)};\n  --dropdown-border            : #{$light};\n  --dropdown-divider           : #{$light};\n  --dropdown-text              : #{$link};\n  --dropdown-active-text       : #{$lightest};\n  --dropdown-active-bg         : #{$selected};\n  --dropdown-hover-text        : #{$lightest};\n  --dropdown-hover-bg          : #{$link};\n  --dropdown-disabled-bg       : #{$disabled};\n  --dropdown-disabled-text     : #{$disabled};\n  --dropdown-locked-text       : #{$lightest};\n\n  --input-text                 : #{$lightest};\n  --input-label                : #{$lighter};\n  --input-placeholder          : #{$disabled};\n  --input-border               : var(--border);\n  --input-bg                   : var(--body-bg);\n  --input-bg-accent            : #{darken($dark, 3%)};\n  --input-hover-bg             : var(--box-bg);\n  --input-focus-bg             : var(--box-bg);\n  --input-disabled-text        : #{darken($lightest, 50%)};\n  --input-disabled-label       : #{darken($lighter, 30%)};\n  --input-disabled-bg          : #{darken($disabled, 30%)};\n  --input-disabled-border      : #{darken($medium, 30%)};\n  --input-disabled-placeholder : #{darken($disabled, 10%)};\n  --input-addon-bg             : #{$darker};\n  --input-locked-text          : #{$lightest};\n\n  --radio-locked-bg            : var(--body-bg);\n  --radio-locked-shadow        : var(--body-bg);\n\n  --progress-bg                : #{$medium};\n  --progress-divider           : #{$lightest};\n\n  --sortable-table-bg          : #{lighten($darkest, 10%)};\n  --sortable-table-row-bg      : #{$darker};;\n  --sortable-table-header-bg   : #{$darkest};\n  --sortable-table-accent-bg   : #{$darker};\n  --sortable-table-accent-alt  : #{$dark};\n  --sortable-table-top-divider : var(--border);\n  --sortable-table-hover-bg    : #{$darkest};\n  --sortable-table-selected-bg : var(--primary-light-bg);\n  --sortable-table-group-label : #{$lighter};\n\n  --tag-primary                : #{$lightest};\n  --tag-bg                     : #{$medium};\n\n  --popover-bg                 : var(--body-bg);\n  --popover-border             : var(--border);\n  --popover-text               : var(--body-text);\n\n  --tooltip-bg                 : #{$medium};\n  --tooltip-border             : var(--tag-primary);\n  --tooltip-text               : var(--body-text);\n  --tooltip-text-warning       : var(--body-text);\n\n  --icon-circle                : #{$medium};\n\n  --tabbed-border              : #{$medium};\n  --tabbed-sidebar-bg          : #{$darkest};\n  --tabbed-container-bg        : #{mix($medium, $dark, 20%)};\n\n  --yaml-editor-bg             : #{$darkest};\n\n  --diff-border                : var(--border);\n  --diff-header-bg             : var(--nav-bg);\n  --diff-header-border         : var(--border);\n  --diff-header                : #{rgba($darkest, 0.3)};\n  --diff-linenum-bg            : var(--nav-bg);\n  --diff-linenum               : var(--muted);\n  --diff-linenum-border        : var(--border);\n  --diff-line-ins-bg           : $success;\n  --diff-line-del-bg           : #{rgba($error, 0.75)};\n  --diff-del-bg                : #{rgba($error, 0.3)};\n  --diff-del-border            : #{$error};\n  --diff-ins-bg                : #{rgba($success, 0.3)};\n  --diff-ins-border            : #{rgba($success, 0.5)};\n  --diff-chg-ins               : #{rgba($success, 0.25)};\n  --diff-chg-del               : #{rgba($warning, 0.5)};\n  --diff-empty-placeholder     : #{$darker};\n\n\n  --wm-tabs-bg                 : #{mix($medium, $dark, 10%)};\n  --wm-tab-bg                  : #{$darkest};\n  --wm-closer-hover-bg         : #{$medium};\n  --wm-tab-active-bg           : #{$darker};\n  --wm-title-bg                : #{$darkest};\n  --wm-title-border            : #{$medium};\n  --wm-body-bg                 : #{$darkest};\n  --wm-border                  : black;\n\n  --glance-divider             : #{$medium};\n\n  --resource-gauge-back-circle : 74, 75, 82, 0.5;\n\n  --simple-box-bg              : #{$darker};\n  --simple-box-border          : #{$darkest};\n  --simple-box-divider         : #{$medium};\n  --simple-box-shadow          : rgba(0, 0, 0, 0.15);\n\n  --terminal-bg                : var(--wm-body-bg);\n  --terminal-cursor            : var(--warning);\n  --terminal-selection         : #{$selected};\n  --terminal-text              : var(--body-text);\n\n  --logs-bg                    : var(--wm-body-bg);\n  --logs-highlight             : var(--wm-body-bg);\n  --logs-highlight-bg          : var(--warning);\n  --logs-text                  : var(--body-text);\n\n  --gauge-divider              : rgba(255, 255, 255, 0.3);\n  --gauge-success-primary      : 75, 95, 64;\n  --gauge-success-secondary    : 150, 189, 127;\n  --gauge-warning-primary      : 218, 195, 66;\n  --gauge-warning-secondary    : 109, 98, 33;\n  --gauge-error-primary        : 239, 90, 83;\n  --gauge-error-secondary      : 120, 45, 42;\n\n  --product-icon               : #{$lighter};\n  --product-icon-active        : #{$lightest};\n\n  --button-icon                : #{$medium};\n  --button-icon-bg             : #{$lightest};\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/themes/_light.scss",
    "content": "// Local variables for reused colors\n//light main text\n$darkest   : #141419;\n\n//light secondary\n$darker    : #6C6C76;\n\n//light disabled\n$dark      : #B6B6C2;\n\n//light border and buttons\n$medium    : #DCDEE7;\n\n//light inputs\n$light     : #EEEFF4;\n\n//light sidebar and box\n$lighter   : #F4F5FA;\n\n//light body bg\n$lightest  : #FFFFFF;\n\n//color for items that are not enabled\n$disabled  : $medium;\n\n$primary         : #3D98D3;\n$secondary       : $darker;\n$link            : #3D98D3;\n\n// Status colors\n$success         : #5D995D;\n$warning         : #DAC342;\n$error           : #F64747;\n$info            : #3D98D3;\n\n$contrasted-dark: $darkest !default;\n$contrasted-light: $lightest !default;\n\n// Text selection color for terminal window (we don't want this to change with the primary color)\n// The terminal alway uses a light background, so okay to use a fixed color\n$selected: rgba(#3D98D3, .5);\n\n:root {\n\n  --primary                    : #{$primary};\n  --primary-text               : #{contrast-color($primary)};\n  --primary-hover-bg           : #{darken($primary, 10%)};\n  --primary-hover-text         : #{saturate($lightest, 20%)};\n  --primary-active-bg          : #{darken($primary, 25%)};\n  --primary-active-text        : #{contrast-color(darken($primary, 25%))};\n  --primary-border             : #($primary);\n  --primary-banner-bg          : #{rgba($primary, 0.15)};\n  --primary-light-bg           : #{rgba($primary, 0.05)};\n\n\n  .text-primary {\n    color: var(--primary) !important;\n  }\n\n  .bg-primary {\n    background-color: var(--primary);\n    color: var(--primary-text);\n\n    &.btn:hover {\n      color: var(--primary-hover-text);\n      background: var(--primary-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--primary-active-text);\n      background: var(--primary-active-bg);\n      }\n  }\n\n  --link                    : #{$link};\n  --link-text               : #{contrast-color($link)};\n  --link-hover-bg           : #{darken($link, 10%)};\n  --link-hover-text         : #{saturate($lightest, 20%)};\n  --link-active-bg          : #{darken($link, 25%)};\n  --link-active-text        : #{contrast-color(darken($link, 25%))};\n  --link-border             : #($link);\n  --link-banner-bg          : #{rgba($link, 0.15)};\n  --link-light-bg           : #{rgba($link, 0.05)};\n\n\n  .text-link {\n    color: var(--link) !important;\n  }\n\n  .bg-link {\n    background-color: var(--link);\n    color: var(--link-text);\n\n    &.btn:hover {\n      color: var(--link-hover-text);\n      background: var(--link-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--link-active-text);\n      background: var(--link-active-bg);\n      }\n  }\n\n  --default                    : #{$light};\n  --default-text               : #{contrast-color($light)};\n  --default-hover-bg           : #{darken($light, 10%)};\n  --default-hover-text         : #{saturate($lightest, 20%)};\n  --default-active-bg          : #{darken($light, 25%)};\n  --default-active-text        : #{contrast-color(darken($light, 25%))};\n  --default-border             : #($light);\n  --default-banner-bg          : #{rgba($light, 0.15)};\n  --default-light-bg           : #{rgba($light, 0.05)};\n\n\n  .text-default {\n    color: var(--default) !important;\n  }\n\n  .bg-default {\n    background-color: var(--default);\n    color: var(--default-text);\n\n    &.btn:hover {\n      color: var(--default-hover-text);\n      background: var(--default-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--default-active-text);\n      background: var(--default-active-bg);\n      }\n  }\n\n  --muted                      : #{$dark};\n\n  .text-muted {\n    color: var(--muted) !important;\n  }\n\n  --darker                    : #{$darker};\n  --darker-text               : #{contrast-color($darker)};\n  --darker-hover-bg           : #{darken($darker, 10%)};\n  --darker-hover-text         : #{saturate($lightest, 20%)};\n  --darker-active-bg          : #{darken($darker, 25%)};\n  --darker-active-text        : #{contrast-color(darken($darker, 25%))};\n  --darker-border             : #($darker);\n  --darker-banner-bg          : #{rgba($darker, 0.15)};\n  --darker-light-bg           : #{rgba($darker, 0.05)};\n\n  .text-darker {\n    color: var(--default) !important;\n  }\n\n  .bg-darker {\n    background-color: var(--darker);\n    color: var(--darker-text);\n\n    &.btn:hover {\n      color: var(--darker-hover-text);\n      background: var(--darker-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--darker-active-text);\n      background: var(--darker-active-bg);\n      }\n  }\n\n\n\n  --success                    : #{$success};\n  --success-text               : #{contrast-color($success)};\n  --success-hover-bg           : #{darken($success, 10%)};\n  --success-hover-text         : #{saturate($lightest, 20%)};\n  --success-active-bg          : #{darken($success, 25%)};\n  --success-active-text        : #{contrast-color(darken($success, 25%))};\n  --success-border             : #($success);\n  --success-banner-bg          : #{rgba($success, 0.15)};\n  --success-light-bg           : #{rgba($success, 0.05)};\n\n  .text-success {\n    color: var(--success) !important;\n  }\n\n  .bg-success {\n    background-color: var(--success);\n    color: var(--success-text);\n\n    &.btn:hover {\n      color: var(--success-hover-text);\n      background: var(--success-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--success-active-text);\n      background: var(--success-active-bg);\n      }\n  }\n\n\n  --info                    : #{$info};\n  --info-text               : #{contrast-color($info)};\n  --info-hover-bg           : #{darken($info, 10%)};\n  --info-hover-text         : #{saturate($lightest, 20%)};\n  --info-active-bg          : #{darken($info, 25%)};\n  --info-active-text        : #{contrast-color(darken($info, 25%))};\n  --info-border             : #($info);\n  --info-banner-bg          : #{rgba($info, 0.15)};\n  --info-light-bg           : #{rgba($info, 0.05)};\n\n  .text-info {\n    color: var(--info) !important;\n  }\n\n  .bg-info {\n    background-color: var(--info);\n    color: var(--info-text);\n\n    &.btn:hover {\n      color: var(--info-hover-text);\n      background: var(--info-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--info-active-text);\n      background: var(--info-active-bg);\n      }\n  }\n\n\n  --warning                    : #{$warning};\n  --warning-text               : #{contrast-color($warning)};\n  --warning-hover-bg           : #{darken($warning, 10%)};\n  --warning-hover-text         : #{saturate($lightest, 20%)};\n  --warning-active-bg          : #{darken($warning, 25%)};\n  --warning-active-text        : #{contrast-color(darken($warning, 25%))};\n  --warning-border             : #($warning);\n  --warning-banner-bg          : #{rgba($warning, 0.15)};\n  --warning-light-bg           : #{rgba($warning, 0.05)};\n\n  .text-warning {\n    color: var(--error) !important;\n  }\n\n  .bg-warning {\n    background-color: var(--warning);\n    color: var(--warning-text);\n\n    &.btn:hover {\n      color: var(--warning-hover-text);\n      background: var(--warning-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--warning-active-text);\n      background: var(--warning-active-bg);\n      }\n  }\n\n\n  --error                    : #{$error};\n  --error-text               : #{contrast-color($error)};\n  --error-hover-bg           : #{darken($error, 10%)};\n  --error-hover-text         : #{saturate($lightest, 20%)};\n  --error-active-bg          : #{darken($error, 25%)};\n  --error-active-text        : #{contrast-color(darken($error, 25%))};\n  --error-border             : #($error);\n  --error-banner-bg          : #{rgba($error, 0.15)};\n  --error-light-bg           : #{rgba($error, 0.05)};\n\n\n  .text-error {\n    color: var(--error) !important;\n  }\n\n  .bg-error {\n    background-color: var(--error);\n    color: var(--error-text);\n\n    &.btn:hover {\n      color: var(--error-hover-text);\n      background: var(--error-hover-bg);\n      transition: all 0.3s ease;\n    }\n\n    &.btn:active {\n      color: var(--error-active-text);\n      background: var(--error-active-bg);\n      }\n  }\n\n\n  --body-bg                    : #{$lightest};\n  --body-text                  : #{$darkest};\n  --scrollbar-thumb            : #{$dark};\n  --scrollbar-thumb-dropdown   : #{$lighter};\n  --scrollbar-track            : transparent;\n\n  --header-bg                  : #{$lightest};\n  --header-btn-bg              : #{$light};\n  --header-btn-text            : #{$darker};\n  --header-input-text          : #{$lightest};\n  --header-height              : 55px;\n  --header-border              : #{$medium};\n  --header-border-size         : 1px;\n  --nav-width                  : 230px;\n  --nav-bg                     : #{$lightest};\n  --nav-active                 : #{$light};\n  --nav-hover                  : #{$medium};\n  --nav-expander-hover         : #{darken($medium, 10%)};\n  --nav-border                 : #{$medium};\n  --nav-border-size            : 1px;\n  --footer-bg                  : #{$lightest};\n  --footer-border              : #{$medium};\n\n  --topmenu-bg                 : #{$lightest};\n  --topmenu-text               : #{$darkest};\n  --topmost-border             : #{$medium};\n  --topmost-shadow             : #{lighten($lightest, 10%)};\n  --topmost-light-hover        : #{$light};\n\n  --disabled-bg                : #{$disabled};\n  --disabled-text              : #{$secondary};\n  --box-bg                     : #{$lighter};\n  --subtle-border              : #{$medium};\n  --border                     : #{$medium};\n  --border-width               : 1px;\n  --border-radius              : 4px;\n  --border-radius-md           : 6px;\n  --border-radius-lg           : 8px;\n  --outline                    : var(--primary);\n  --outline-width              : 1px;\n\n  --accent-btn                 : var(--primary-banner-bg);\n  --accent-btn-hover           : var(--primary);\n  --accent-btn-hover-text      : #{$lightest};\n\n  --modal-bg                   : #{$lightest};\n  --modal-border               : #{$dark};\n  --overlay-bg                 : #{rgba($lighter, 0.75)};\n  --shadow                     : #{rgba($medium, 0.85)};\n\n  --checkbox-tick              : #{$lightest};\n  --checkbox-border            : #{$medium};\n  --checkbox-tick-disabled     : #{darken($disabled, 40%)};\n  --checkbox-disabled-bg       : #{$disabled};\n  --checkbox-tick-locked       : #{$darkest};\n  --checkbox-locked-bg         : #{lighten($disabled, 5%)};\n  --checkbox-ticked-bg         : #{$link};\n  --checkbox-locked-border     : #{lighten($disabled, 5%)};\n  --checkbox-locked-shadow     : #{lighten($disabled, 5%)};\n\n  --dropdown-bg                : #{$lightest};\n  --dropdown-border            : #{$medium};\n  --dropdown-divider           : #{$medium};\n  --dropdown-text              : #{$link};\n  --dropdown-active-text       : #{$lightest};\n  --dropdown-active-bg         : #{$dark};\n  --dropdown-hover-text        : var(--body-text);\n  --dropdown-hover-bg          : #{$light};\n  --dropdown-disabled-text     : var(--muted);\n  --dropdown-disabled-bg       : #{$disabled};\n  --dropdown-locked-text       : #{$darkest};\n\n  // UNUSED?\n  --card-header                : var(--primary-banner-bg);\n\n  --input-text                 : #{$darkest};\n  --input-label                : #{$secondary};\n  --input-placeholder          : #{darken($disabled, 10%)};\n  --input-border               : var(--border);\n  --input-bg                   : var(--body-bg);\n  --input-bg-accent            : #{darken($light, 2%)};\n  --input-hover-bg             : var(--box-bg);\n  --input-focus-bg             : var(--box-bg);\n  --input-disabled-text        : #{darken($disabled, 60%)};\n  --input-disabled-label       : #{darken($disabled, 40%)};\n  --input-disabled-bg          : #{$disabled};\n  --input-disabled-border      : #{darken($medium, 10%)};\n  --input-disabled-placeholder : #{darken($medium, 15%)};\n  --input-addon-bg             : #{$darker};\n  --input-locked-text          : #{$darkest};\n\n  --radio-locked-bg            : var(--body-bg);\n  --radio-locked-shadow        : var(--body-bg);\n\n  --progress-bg                : #{$medium};\n  --progress-divider           : #{$medium};\n\n  --sortable-table-bg          : #{darken($lightest, 5%)};\n  --sortable-table-row-bg      : #{$lightest};\n  --sortable-table-header-bg   : #{$lighter};\n  --sortable-table-accent-bg   : #{$lighter};\n  --sortable-table-accent-alt  : #{$lightest};\n  --sortable-table-top-divider : var(--border);\n  --sortable-table-body-divider : #{$medium};\n\n  --sortable-table-hover-bg    : #{$lighter};\n  //--sortable-table-selected-bg : #{rgba($primary, 0.02)};\n\n  --sortable-table-selected-bg : var(--primary-light-bg);\n  --sortable-table-group-label : #{$secondary};\n\n  --tag-primary                : #{$darkest};\n  --tag-bg                     : #{$medium};\n\n  --popover-bg                 : var(--body-bg);\n  --popover-border             : var(--border);\n  --popover-text               : var(--body-text);\n  --popover-border-radius      : var(--border-radius);\n  --tooltip-bg                 : #{$medium};\n  --tooltip-border             : var(--tag-primary);\n  --tooltip-text               : var(--tag-primary);\n  --tooltip-bg-warning         : #{rgba($warning, 0.8)};\n  --tooltip-text-warning       : var(--body-text);\n\n  --icon-circle                : #{$medium};\n\n  --tabbed-border              : #{$medium};\n  --tabbed-sidebar-bg          : #{$lighter};\n  --tabbed-container-bg        : #{mix($light, $lighter, 15%)};\n\n  --yaml-editor-bg             : #{$lighter};\n\n  --diff-border                : var(--border);\n  --diff-header-bg             : var(--nav-bg);\n  --diff-header-border         : var(--border);\n  --diff-header                : #{rgba($darkest, 0.3)};\n  --diff-linenum-bg            : var(--nav-bg);\n  --diff-linenum               : var(--muted);\n  --diff-linenum-border        : var(--border);\n  --diff-line-ins-bg           : $success;\n  --diff-line-del-bg           : #{rgba($error, 0.75)};\n  --diff-del-bg                : #{rgba($error, 0.3)};\n  --diff-del-border            : #{$error};\n  --diff-ins-bg                : #{rgba($success, 0.3)};\n  --diff-ins-border            : #{rgba($success, 0.5)};\n  --diff-chg-ins               : #{rgba($success, 0.25)};\n  --diff-chg-del               : #{rgba($warning, 0.5)};\n  --diff-empty-placeholder     : #{$lightest};\n\n  --wm-tabs-bg                 : #{$medium};\n  --wm-tab-bg                  : #{$light};\n  --wm-closer-hover-bg         : #{$lighter};\n  --wm-tab-active-bg           : #{$lighter};\n  --wm-title-bg                : #{$lightest};\n  --wm-title-border            : #{$medium};\n  --wm-body-bg                 : #{$lighter};\n  --wm-border                  : var(--border);\n  --wm-tab-height              : 29px;\n\n  --glance-bg-rgb              : 61, 152, 211;\n  --glance-divider             : #{$medium};\n\n  --resource-gauge-back-circle : 255, 255, 255, 0.15;\n\n  --simple-box-bg              : #{$lightest};\n  --simple-box-border          : #{$medium};\n  --simple-box-divider         : #{$medium};\n  --simple-box-shadow          : none;\n\n  --terminal-bg                : var(--body-bg);\n  --terminal-cursor            : var(--warning);\n  --terminal-selection         : #{$selected};\n  --terminal-text              : var(--body-text);\n\n  --logs-bg                    : var(--wm-body-bg);\n  --logs-highlight             : var(--wm-body-bg);\n  --logs-highlight-bg          : var(--warning);\n  --logs-text                  : var(--body-text);\n\n  --gauge-divider              : #{$lightest};\n  --gauge-zero                 : #{$medium};\n  --gauge-success-primary      : 150, 189, 127;\n  --gauge-success-secondary    : 190, 211, 172;\n  --gauge-warning-primary      : 238, 226, 176;\n  --gauge-warning-secondary    : 218, 195, 66;\n  --gauge-error-primary        : 249, 186, 171;\n  --gauge-error-secondary      : 239, 90, 83;\n\n  --sizzle-0                   : 180, 210, 30;\n  --sizzle-1                   : 225, 45, 74;\n  --sizzle-2                   : 212, 66, 148;\n  --sizzle-3                   : 0, 169, 217;\n  --sizzle-4                   : 244, 136, 68;\n  --sizzle-5                   : 0, 147, 128;\n  --sizzle-6                   : 136, 81, 165;\n  --sizzle-7                   : 45, 47, 149;\n  --sizzle-8                   : 255, 235, 0;\n\n  --sizzle-success             : #{red($success)}, #{green($success)}, #{blue($success)};\n  --sizzle-info                : #{red($info)},    #{green($info)},    #{blue($info)};\n  --sizzle-warning             : #{red($warning)}, #{green($warning)}, #{blue($warning)};\n  --sizzle-error               : #{red($error)},   #{green($error)},   #{blue($error)};\n  --sizzle-unknown             : #{red($disabled)},#{green($disabled)},#{blue($disabled)};\n\n  $rancher                     : $primary;\n  $partner                     : #FEA424;\n  $other                       : #614EA2;\n\n  --app-rancher-accent         : #{$rancher};\n  --app-rancher-accent-text    : #{$darkest};\n\n  --app-partner-accent         : #{$partner};\n  --app-partner-accent-text    : black;\n\n  --app-color1-accent          : rgba(var(--sizzle-1), 1);\n  --app-color1-accent-text     : white;\n\n  --app-color2-accent          : rgba(var(--sizzle-2), 1);\n  --app-color2-accent-text     : white;\n\n  --app-color3-accent          : rgba(var(--sizzle-3), 1);\n  --app-color3-accent-text     : white;\n\n  --app-color4-accent          : rgba(var(--sizzle-4), 1);\n  --app-color4-accent-text     : white;\n\n  --app-color5-accent          : rgba(var(--sizzle-5), 1);\n  --app-color5-accent-text     : white;\n\n  --app-color6-accent          : rgba(var(--sizzle-6), 1);\n  --app-color6-accent-text     : white;\n\n  --app-color7-accent          : rgba(var(--sizzle-7), 1);\n  --app-color7-accent-text     : white;\n\n  --app-color8-accent          : rgba(var(--sizzle-8), 1);\n  --app-color8-accent-text     : white;\n\n  --product-icon               : #{$darker};\n  --product-icon-active        : #{$darkest};\n\n  --button-icon                : #{$dark};\n  --button-icon-bg             : #{$secondary};\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/themes/_suse.scss",
    "content": ".suse {\n  $primary: hsl(151, 59%, 46%);\n  $info:  mix($primary, $secondary, 50%);\n  $selected: rgba($primary, .5);\n\n  --primary                    : #{$primary};\n  --primary-text               : #{contrast-color($primary)};\n  --primary-hover-bg           : #{darken($primary, 10%)};\n  --primary-hover-text         : #{saturate($lightest, 20%)};\n  --primary-active-bg          : #{darken($primary, 25%)};\n  --primary-active-text        : #{contrast-color(darken($primary, 25%))};\n  --primary-border             : #($primary);\n  --primary-banner-bg          : #{rgba($primary, 0.15)};\n  --primary-light-bg           : #{rgba($primary, 0.05)};\n\n  --info                    : #{$info};\n  --info-text               : #{contrast-color($info)};\n  --info-hover-bg           : #{darken($info, 10%)};\n  --info-hover-text         : #{saturate($lightest, 20%)};\n  --info-active-bg          : #{darken($info, 25%)};\n  --info-active-text        : #{contrast-color(darken($info, 25%))};\n  --info-border             : #($info);\n  --info-banner-bg          : #{rgba($info, 0.15)};\n  --info-light-bg           : #{rgba($info, 0.05)};\n\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/vendor/normalize.scss",
    "content": "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n   ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n  line-height: 1.15; /* 1 */\n  -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n   ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n  margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n  display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n/* Grouping content\n   ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n  box-sizing: content-box; /* 1 */\n  height: 0; /* 1 */\n  overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n   ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n  background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n  border-bottom: none; /* 1 */\n  text-decoration: underline; /* 2 */\n  text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n  font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n  font-family: monospace, monospace; /* 1 */\n  font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n  font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsub {\n  bottom: -0.25em;\n}\n\nsup {\n  top: -0.5em;\n}\n\n/* Embedded content\n   ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n  border-style: none;\n}\n\n/* Forms\n   ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  font-family: inherit; /* 1 */\n  font-size: 100%; /* 1 */\n  line-height: 1.15; /* 1 */\n  margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n  overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n  text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n  -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n  border-style: none;\n  padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n  outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n  padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n *    `fieldset` elements in all browsers.\n */\n\nlegend {\n  box-sizing: border-box; /* 1 */\n  color: inherit; /* 2 */\n  display: table; /* 1 */\n  max-width: 100%; /* 1 */\n  padding: 0; /* 3 */\n  white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n  vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n  overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n  box-sizing: border-box; /* 1 */\n  padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n  -webkit-appearance: textfield; /* 1 */\n  outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n  -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n  -webkit-appearance: button; /* 1 */\n  font: inherit; /* 2 */\n}\n\n/* Interactive\n   ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n  display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n  display: list-item;\n}\n\n/* Misc\n   ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n  display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n  display: none;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/styles/vendor/vue-select.scss",
    "content": ".v-select {\n  position: relative;\n  font-family: inherit;\n\n  &.auto-width {\n    display: inline-block;\n    width: auto;\n    min-width: 2em;\n  }\n}\n\n.v-select,\n.v-select * {\n  box-sizing: border-box;\n}\n\n.vs__dropdown-toggle {\n  cursor: pointer;\n}\n\n.vs--disabled {\n  .vs__dropdown-toggle,\n  .vs__clear,\n  .vs__search,\n  .vs__open-indicator {\n    cursor: not-allowed;\n    color: var(--dropdown-disabled-text);\n  }\n}\n\n.vs__dropdown-menu {\n  display: block;\n  position: absolute;\n  left: -2px;\n  z-index: z-index('dropdownContent');\n  padding: $input-padding-sm 0;\n  margin: 0;\n  width: calc(100% + 4px);\n  max-height: 350px;\n  min-width: 160px;\n  overflow-y: auto;\n  border: 1px solid var(--dropdown-border);\n  border-radius: var(--border-radius);\n  text-align: left;\n  list-style: none;\n  background: var(--dropdown-bg);\n\n  &::-webkit-scrollbar-thumb {\n    background-color: var(--scrollbar-thumb-dropdown) !important;\n    border-radius: 4px;\n  }\n\n  &[data-popper-placement='top'] {\n    border-radius: 4px 4px 0 0;\n    border-top-style: solid;\n    box-shadow: 0px -8px 16px 0px var(--shadow);\n  }\n}\n\n.vs__dropdown-option {\n  line-height: 1.42857143; /* Normalize line height */\n  display: block;\n  padding: 0 calc(#{$input-padding-sm}/2);\n  clear: both;\n  color: var(--dropdown-text);\n  white-space: nowrap;\n  z-index: 1000;\n\n  &:hover {\n    cursor: pointer;\n  }\n\n  a {\n    display: block;\n\n    &:hover {\n      color: var(--body-text);\n    }\n  }\n\n  &.vs__dropdown-option--disabled {\n    color: var(--dropdown-disabled-text);\n    cursor: not-allowed;\n\n    hr {\n      cursor: default;\n    }\n  }\n\n  &.vs__dropdown-option--selected {\n    background-color: var(--dropdown-active-bg);\n    color: var(--dropdown-active-text);\n    text-decoration: none;\n  }\n\n  &.vs__dropdown-option--highlight {\n    color: var(--dropdown-hover-text);\n    background: var(--dropdown-hover-bg);\n\n    a {\n      color: var(--dropdown-hover-text);\n      text-decoration: none;\n    }\n  }\n}\n\n.vs__dropdown-toggle {\n  appearance: none;\n  display: flex;\n  background: var(--input-bg);\n  border: 1px solid var(--dropdown-border);\n  border-radius: var(--border-radius);\n  white-space: normal;\n}\n\n.vs__selected-options {\n  display: flex;\n  flex-basis: 100%;\n  flex-grow: 1;\n  flex-wrap: wrap;\n  padding: 0;\n  position: relative;\n}\n\n.lg .vs__selected-options {\n  margin: 8px;\n}\n\n.vs__actions {\n  display: flex;\n  align-items: center;\n  pointer-events: none;\n  position: relative;\n  width: 100%;\n  justify-content: flex-end;\n  flex-shrink: 8;\n\n  svg {\n    display: none;\n  }\n\n  &:after {\n    content: $icon-chevron-down;\n    font-family: 'icons';\n    font-size: 2rem;\n    color: var(--secondary);\n  }\n}\n\n.vs--searchable .vs__dropdown-toggle {\n  cursor: text;\n}\n\n.vs--unsearchable .vs__dropdown-toggle {\n  cursor: pointer;\n}\n\n$transition-timing-function: cubic-bezier(1, -0.115, 0.975, 0.855);\n$transition-duration: 150ms;\n\n.vs__action:after {\n  fill: var(--dropdown-disabled-text);\n  transform: scale(1);\n  transition: transform $transition-duration $transition-timing-function;\n  transition-timing-function: $transition-timing-function;\n}\n\n.vs--open .vs__actions:after {\n  transform: rotate(180deg) scale(1);\n}\n\n.vs--loading .vs__open-indicator {\n  opacity: 0;\n}\n\n/**\n * Super weird bug... If this declaration is grouped\n * below, the cancel button will still appear in chrome.\n * If it's up here on it's own, it'll hide it.\n */\n.vs__search::-webkit-search-cancel-button {\n  display: none;\n}\n\n.vs__search::-webkit-search-decoration,\n.vs__search::-webkit-search-results-button,\n.vs__search::-webkit-search-results-decoration,\n.vs__search::-ms-clear {\n  display: none;\n}\n\n.vs__search,\n.vs__search:focus {\n  appearance: none;\n  border-left: none;\n  outline: none;\n  margin: 0;\n  background: none;\n  box-shadow: none;\n  width: 0;\n  max-width: 100%;\n  flex-grow: 1;\n  margin-left: $input-padding-sm;\n}\n\n.vs__search::placeholder {\n  color: var(--input-placeholder);\n}\n\n.vs--unsearchable {\n  .vs__search {\n    opacity: 1;\n\n    &:hover {\n      cursor: pointer;\n    }\n  }\n}\n.vs--single.vs--searching:not(.vs--open):not(.vs--loading) {\n  .vs__search {\n    opacity: 0.2;\n  }\n}\n\n/* States */\n\n.vs--single {\n  .vs__selected {\n    background-color: transparent;\n    border-color: transparent;\n  }\n\n  &.vs--searching .vs__selected {\n    display: none;\n  }\n}\n\n.vs__selected {\n  display: flex;\n  align-items: center;\n  background-color: var(--accent-btn);\n  border: 1px solid var(--primary);\n  border-radius: 3px;\n  color: var(--link);\n  margin-left: $input-padding-sm;\n\n  &:not(:last-of-type) {\n    margin-right: 2px;\n  }\n}\n\n.vs__deselect {\n  display: inline-flex;\n  appearance: none;\n  margin-left: 8px;\n  padding: 0;\n  border: 0;\n  cursor: pointer;\n  background: none;\n  fill: var(--primary);\n\n  svg {\n    display: none;\n  }\n\n  &:after {\n    content: $icon-close;\n    font-family: 'icons';\n    color: var(--link);\n  }\n}\n\n/*inline single-option select*/\n\n.v-select.inline {\n  background-color: transparent;\n\n  &.vs--single {\n    min-height: 29px;\n    &.vs--open {\n      .vs__selected {\n        position: absolute;\n        opacity: 0.4;\n      }\n      .vs__search {\n        margin-left: $input-padding-sm;\n      }\n    }\n    .vs__selected {\n      color: var(--input-text);\n    }\n  }\n\n  .vs__dropdown-menu {\n    min-width: 0px;\n    margin-top: 2px;\n  }\n  .vs__dropdown-toggle {\n    background-color: var(--input-bg);\n    border: none;\n    padding: none;\n    border-radius: var(--border-radius);\n    border: 1px solid var(--dropdown-border);\n  }\n\n  &.vs--single .vs__selected-options {\n    align-items: center;\n  }\n  .vs__search {\n    background-color: rgba(0, 0, 0, 0);\n    &:hover {\n      background-color: rgba(0, 0, 0, 0);\n    }\n  }\n  .vs__open-indicator {\n    fill: var(--input-label);\n  }\n  .vs__clear {\n    display: none;\n  }\n}\n\n.v-select.mini {\n  position: relative;\n  top: 2px;\n\n  .vs__dropdown-toggle {\n    padding: 5px 0;\n  }\n\n  .vs__selected {\n    margin: 0;\n  }\n\n  input {\n    padding: 0;\n  }\n}\n\n.vs__selected-options input {\n  width: 0;\n  display: inline-block;\n  border: 0;\n  background-color: var(--input-bg);\n  color: var(--input-text);\n}\n\nheader .vs__selected-options input {\n  color: var(--header-input-text);\n}\n\nheader .vs-select .vs__dropdown-toggle {\n  background: var(--error) !important;\n}\n\n.vs__no-options {\n  color: var(--dropdown-text);\n  padding: 3px 20px;\n}\n\nheader {\n  .unlabeled-select {\n    padding: 0;\n\n    &.focused {\n      border: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/translations/en-us.yaml",
    "content": "##############################\n# Special stuff\n##############################\ngeneric:\n  add: Add\n  back: Back\n  cancel: Cancel\n  close: Close\n  comingSoon: Coming Soon\n  copy: Copy\n  create: Create\n  created: Created\n  customize: Customize\n  default: Default\n  disabled: Disabled\n  done: Done\n  enabled: Enabled\n  ignored: Ignored\n  invalidCron: Invalid cron schedule\n  labelsAndAnnotations: Labels & Annotations\n  loading: Loading&hellip;\n  members: Members\n  na: n/a\n  name: Name\n  never: Never\n  none: None\n  notFound: Not Found\n  number: '{prefix}{value, number}{suffix}'\n  overview: Overview\n  plusMore: \"+ {n} more\"\n  readFromFile: Read from File\n  register: Register\n  remove: Remove\n  resource: |-\n    {count, plural,\n    one  {resource}\n    other {resources}\n    }\n  resourceCount: |-\n    {count, plural,\n    one  {1 resource}\n    other {# resources}\n    }\n  save: Save\n  showAdvanced: Show Advanced\n  hideAdvanced: Hide Advanced\n  type: Type\n  unknown: Unknown\n  key: Key\n  value: Value\n  yes: Yes\n  no: No\n  units:\n    time:\n        5s: 5s\n        10s: 10s\n        30s: 30s\n        1m: 1m\n        5m: 5m\n        15m: 15m\n        30m: 30m\n        1h: 1h\n        2h: 2h\n        6h: 6h\n        1d: 1d\n        7d: 7d\n        30d: 30d\n\nlocale:\n  en-us: English\n  zh-hans: 简体中文\n  none: (None)\n\nnav:\n  backToRancher: Cluster Manager\n  clusterTools: Cluster Tools\n  kubeconfig: Download KubeConfig\n  import: Import YAML\n  home: Home\n  shell: Kubectl Shell\n  support: Get Support\n  group:\n    cluster: Cluster\n    inUse: More Resources\n    rbac: RBAC\n    serviceDiscovery: Service Discovery\n    starred: Starred\n    storage: Storage\n    workload: Workload\n    monitoring: Monitoring\n  ns:\n    all: All Namespaces\n    clusterLevel: Only Cluster Resources\n    namespace: \"{name}\"\n    namespaced: Only Namespaced Resources\n    orphan: Not in a Project\n    project: \"Project: {name}\"\n    system: Only System Namespaces\n    user: Only User Namespaces\n  apps: Apps\n  categories:\n    explore: Explore Cluster\n    multiCluster: Global Apps\n    legacy: Legacy Apps\n    configuration: Configuration\n  search:\n    placeholder: Type to search clusters\n    noResults: No matching clusters\n  resourceSearch:\n    label: Resource Search\n    toolTip: Resource Search {key}\n    placeholder: Type to search for a resource...\n  header:\n    setLoginPage: Set as login page\n    restoreCards: Restore hidden cards\n  userMenu:\n    clusterDashboard: Cluster Dashboard\n    preferences: Preferences\n    accountAndKeys: Account & API Keys\n    logOut: Log Out\n\nproduct:\n  apps: Apps & Marketplace\n  auth: Users & Authentication\n  backup: Rancher Backups\n  cis: CIS Benchmark\n  ecm: Cluster Manager\n  explorer: Cluster Explorer\n  fleet: Continuous Delivery\n  longhorn: Longhorn\n  manager: Cluster Management\n  gatekeeper: OPA Gatekeeper\n  istio: Istio\n  logging: Logging\n  rio: Rio\n  settings: Global Settings\n  clusterManagement: Cluster Management\n  monitoring: Monitoring\n  mcapps: Multi-cluster Apps\n  version: Version\n  versionChecking: (checking...)\n  networkStatus: Network status\n  kubernetesVersion: Kubernetes\n  containerEngine:\n    fullName: Container Engine\n    abbreviation: CE\n  notFound: not found\n  deactivated: deactivated\n\nsuffix:\n  percent: \"%\"\n  milliCpus: milli CPUs\n  cpus: CPUs\n  ib: iB\n  mib: MiB\n  gb: GB\n  revisions: |-\n    {count, plural,\n      =1 { Revision }\n      other { Revisions }\n    }\n  seconds: |-\n    {count, plural,\n      =1 { Second }\n      other { Seconds }\n    }\n  sec: Sec\n  times: |-\n    {count, plural,\n      =1 { Time }\n      other { Times }\n    }\n##############################\n# Components & Pages\n##############################\napp:\n  name: Rancher Desktop\n  update: \"There's one last step to finish updating Rancher Desktop\"\nfirstRun:\n  kubernetesVersion:\n    legend: Kubernetes Version\n    cachedOnly: (cached versions only)\n  ok: OK\nmarketplace:\n  title: Extensions\n  noResults: No extension found for the search criteria\n  tabs:\n    catalog: Catalog\n    installed: Installed\n  banners:\n    install: Extension {name} has been installed.\n    uninstall: Extension {name} has been removed.\n  labels:\n    install: Install\n    uninstall: Remove\n    upgrade: Upgrade\n  loading:\n    install: Installing...\n    uninstall: Removing...\n    upgrade: Upgrading...\n  moreInfo: More information\ncontainers:\n  title: Containers\n  sortableTables:\n    noRows: There are no containers to show\n  logs:\n    title: Container Logs\n    loading: Loading container logs...\n    noLogs: No logs available\n    refresh: Refresh\n    fetchError: Failed to fetch container logs\n  manage:\n    table:\n      header:\n        state: State\n        containerName: Name\n        image: Image\n        ports: Port(s)\n        started: Uptime\n        actions: Actions\n\nvolumes:\n  title: Volumes\n  sortableTables:\n    noRows: There are no volumes to show\n  manage:\n    table:\n      header:\n        volumeName: Volume Name\n        driver: Driver\n        mountpoint: Mount Point\n        created: Created\n  manager:\n    table:\n      action:\n        browse: Browse Files\n        delete: Delete\n  files:\n    title: Volume Files\n    loading: Loading volume files...\n    volumeNotFound: 'Volume \"{name}\" not found'\n    checkError: Error checking volume status\n    listError: 'Error listing files: {error}'\n    noFiles: This volume is empty\n    table:\n      header:\n        name: Name\n        size: Size\n        modified: Modified\n        permissions: Permissions\n\nimages:\n  title: Images\n  sortableTables:\n    noRows: There are no images to show\n  state:\n    k8sUnready: Waiting for Kubernetes to be ready\n    imagesUnready: Waiting for image manager to be ready\n    unknown: 'Error: Unknown state; please reload.'\n    close: Close Output to Continue\n  action:\n    add: Add Image\n  manager:\n    close: Close Output to Continue\n    title: Image Output\n    input:\n      pull:\n        label: 'Name of image to pull:'\n        placeholder: 'registry.example.com/namespace/image'\n        button: Pull\n      build:\n        label: 'Name of image to build:'\n        placeholder: 'registry.example.com/namespace/image:tag'\n        button: Build\n    table:\n      label: All images\n      header:\n        imageName: Image\n        tag: Tag\n        imageId: Image ID\n        size: Size\n      action:\n        push: Push\n        delete: Delete\n        scan: Scan...\n  add:\n    title: Add Image\n    action:\n      build: Build\n      pull: Pull\n      pastTense:\n        build: Built\n        pull: Pulled\n    loadingText: '{action}ing image...'\n    successText: '{action} image'\n    errorText: Error trying to { action } ''{ image }'' - see console output for more information\n  scan:\n    title: Scan details for ''{ image }''\n    loadingText: Scanning ''{ image }''\n    errorText: Error trying to scan ''{ image }'' - see console output for more information\n    results:\n      headers:\n        severity: Severity\n        package: Package\n        vulnerabilityId: Vulnerability ID\n        installed: Installed\n        fixed: Fixed\n    labels:\n      critical: CRITICAL\n      high: HIGH\n      medium: MEDIUM\n      low: LOW\n      issuesFound: Issues Found\n    details:\n      description: Description\n      primaryUrl: Primary URL\n      references: References\nk8s:\n  title: Kubernetes Settings\n  dialog:\n    ok: OK\n    cancel: Cancel\nportForwarding:\n  title: Port Forwarding\n  sortableTables:\n    noRows: There are no port forwarding entries to show\ngeneral:\n  title: Welcome to Rancher Desktop by SUSE\n  description: Rancher Desktop provides Kubernetes and image management through the use of a desktop application.\nabout:\n  title: About\n  versions:\n    title: Versions\n    component: Component\n    version: Version\n    cli: CLI\n    helm: Helm\n    machine: Machine\n    releaseNotes: 'View release notes'\n  os:\n    mac: macOS\n    windows: Windows\n    linux: Linux\n  downloadImageList:\n    title: Image Lists\n  downloadCLI:\n    title: CLI Downloads\n\napplication:\n  behavior:\n    autoStart:\n      legendText: Startup\n      label: Automatically start at login\n    background:\n      legendText: Background\n      legendTooltip: >\n        Hide the app window when Rancher Desktop is running in the background.\n        It can be opened via the Notification Icon (if visible) or by running Rancher Desktop from the Applications menu.\n    startInBackground:\n      label: Start in the background\n    windowQuitOnClose:\n      label: Quit when closing application window\n    notificationIcon:\n      legendText: Notification Icon\n      label: Hide Notification Icon\n    theme:\n      legendText: Appearance\n      options:\n        system:\n          label: System\n          description: Follow the operating system setting\n        light:\n          label: Light\n          description: Always use light mode\n        dark:\n          label: Dark\n          description: Always use dark mode\n\nvirtualMachine:\n  networkingTunnel:\n    legend: Networking Tunnel\n    label: Enable networking tunnel\n  mount:\n    type:\n      legend: Mount Type\n      options:\n        reverse-sshfs:\n          label: reverse-sshfs\n          description: Exposes the filesystem by running an SFTP server.\n        9p:\n          label: 9p\n          description: Exposes the filesystem by using QEMU's virtio-9p-pci devices.\n          options:\n            cacheMode:\n              legend: Cache Mode\n              tooltip: Caching policy to be used\n              options:\n                none: none\n                loose: loose\n                fscache: fscache\n                mmap: mmap\n            mSizeInKib:\n              legend: Memory Size In KiB\n              tooltip: Maximum package size in KiB\n            protocolVersion:\n              legend: Protocol Version\n              tooltip: 9P protocol version\n              options:\n                9p2000: 9p2000\n                9p2000u: '9p2000.u'\n                9p2000L: '9p2000.L'\n            securityModel:\n              legend: Security Model\n              tooltip: Security model used for the export path\n              options:\n                passthrough: passthrough\n                'mapped-xattr': mapped-xattr\n                'mapped-file': mapped-file\n                none: none\n        virtiofs:\n          label: virtiofs\n          description: Exposes the filesystem by using an Apple Virtualization framework shared directory device.\n  proxy:\n    legend: WSL Proxy\n    label: Enable the proxy used by rancher-desktop\n    addressTitle: Proxy address\n    address: Address\n    port: Port\n    authTitle: Authentication information\n    username: Username\n    password: Password\n    noproxy:\n      legend: No proxy hostname list\n      placeholder: Hostname to not redirect to the proxy\n      errors:\n        duplicate: Error, item is duplicate.\n  type:\n    legend: Virtual Machine Type\n    options:\n      qemu:\n        label: QEMU\n        description: Use the QEMU emulator.\n      vz:\n        label: VZ\n        description: Use the Apple Virtualization framework.\n  useRosetta:\n    legend: VZ Option\n    label: Enable Rosetta support\n\ncontainerEngine:\n  label: Container Engine\n  options:\n    moby:\n      label: dockerd (moby)\n      description: Docker API; use with Docker CLI.\n    containerd:\n      label: containerd\n      description: Namespaces for container images; use with nerdctl.\n\nwebAssembly:\n  label: WebAssembly (Wasm)\n  enabled: Enabled\n  description: Please read the documentation before enabling the experimental WebAssembly feature!\n\nallowedImages:\n  label: Allowed Image Patterns\n  patterns:\n    placeholder: Type the image pattern\n  errors:\n    duplicate: Error, item is duplicate.\n  enable: Enable\n  alert:\n    The image name needs to match one of the patterns defined in the Allowed Images preference tab.\n\npathManagement:\n  label: Configure PATH\n  tooltip: Rancher Desktop ships with tools, such as kubectl, nerdctl, helm and docker. In order to use these tools, <code>$HOME/.rd/bin</code> must be in your PATH.\n  options:\n    rcFiles:\n      label: Automatic\n      description: Rancher Desktop edits your shell profile for you. Restart any open shells for changes to take effect.\n    manual:\n      label: Manual\n      description: 'Rancher Desktop does not change your PATH configuration; add <code>$HOME/.rd/bin</code> to your path manually.'\n  accept: Accept\n\nlegacyIntegrations:\n  title: Legacy Integrations Found\n  messageFirstPart: Rancher Desktop detected legacy tool symlinks in\n  messageSecondPart: but did not have the permissions required to remove them.\n  messageThirdPart: >\n    Legacy symlinks have the potential to cause path conflicts.\n    Please remove them at your earliest convenience.\n  details: >\n    Rancher Desktop creates symlinks from bundled tools, such as kubectl and docker,\n    to a directory in order to make them usable. This directory changed in Rancher Desktop\n    1.3.0. If you are seeing this message, Rancher Desktop was unable to remove the symlinks\n    from the old directory automatically. You should remove them manually to prevent future\n    path conflicts.\n  ok: OK\n\nsudoPrompt:\n  title: Administrative Access Required\n  message: 'Rancher Desktop requires administrative access (\"sudo access\") for the following reasons:'\n  messageSecondPart: The prompt will be displayed once this window is closed. Cancelling the prompt or disabling administrative access requires you to switch the docker context to rancher-desktop in order to continue using Rancher Desktop.\n  explanation: 'This will modify the following paths:'\n  buttonText: OK\n\nunmetPrerequisites:\n  title: Rancher Desktop is unable to start\n  message: 'Rancher Desktop cannot start because requirements are missing or not configured:'\n  action: 'Please ensure all requirements are met and try again. Rancher Desktop will now close.'\n  buttonText: OK\n\naccountAndKeys:\n  title: Account and API Keys\n  account:\n    title: Account\n    change: Change Password\n  apiKeys:\n    title: API Keys\n    notAllowed: You do not have permission to manage API Keys\n    add:\n      description:\n        label: Description\n        placeholder: Optionally enter a description to help you identify this API Key\n      label: Create API Key\n      expiry:\n        label: Automatically expire\n        options:\n          never: Never\n          day: A day from now\n          month: A month from now\n          year: A year from now\n          custom: Custom\n          maximum: \"{value} - Maximum allowed\"\n      customExpiry:\n        options:\n          minute: Minutes\n          hour: Hours\n          day: Days\n          month: Months\n          year: Years\n      scope: Scope\n      noScope: No Scope\n    info:\n      accessKey: Access Key\n      secretKey: Secret Key\n      bearerToken: Bearer Token\n      saveWarning: Save the info above! This is the only time you'll be able to see it. If you lose it, you'll need to create a new API key.\n      keyCreated: A new API Key has been created\n      bearerTokenTip: \"Access Key and Secret Key can be sent as the username and password for HTTP Basic auth to authorize requests. You can also combine them to use as a Bearer token:\"\n      ttlLimitedWarning: The Expiry time for this API Key was reduced due to system configuration\n\nauthConfig:\n  accessMode:\n    label: 'Configure who should be able to log in and use {vendor}'\n    required: Restrict access to only the authorized users & groups\n    restricted: 'Allow members of clusters and projects, plus authorized users & groups'\n    unrestricted: Allow any valid user\n  allowedPrincipalIds:\n    title: Authorized Users & Groups\n  associatedWarning: 'Note: The {provider} user you authenticate as will be associated as an alternate way to log in to the {vendor} user you are currently logged in as <code>{username}</code>; all the global permissions, project, and cluster role bindings of this {vendor} user will also apply to the {provider} user.'\n  github:\n    clientId:\n      label: Client ID\n    clientSecret:\n      label: Client Secret\n    form:\n      app:\n        label: Application name\n        value: 'Anything you like, e.g. My {vendor}'\n      calllback:\n        label: Authorization callback URL\n      description:\n        label: Application description\n        value: 'Optional, can be left blank'\n      homepage:\n        label: Homepage URL\n      instruction: 'Fill in the form with these values:'\n      prefix:\n        1: <li>Open <a href=\"{baseUrl}/settings/developers\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">GitHub application settings</a> in a new window.</li>\n        2: <li>Click on the \"OAuth Apps\" tab.</li>\n        3: <li>Click the \"New OAuth App\" button.</li>\n      suffix:\n        1: <li>Click \"Register application\"</li>\n        2: <li>Copy and paste the Client ID and Client Secret of your newly created OAuth app into the fields below</li>\n    host:\n      label: GitHub Enterprise Host\n      placeholder: e.g. github.mycompany.example\n    target:\n      label: Which version of GitHub do you want to use?\n      private: A private installation of GitHub Enterprise\n      public: Public GitHub.com\n    table:\n      server: Server\n      clientId: Client ID\n  googleoauth:\n    adminEmail: Admin Email\n    domain: Domain\n    oauthCredentials:\n      label: OAuth Credentials\n      tip: The OAuth Credentials JSON can be found in the Google API developers console.\n    serviceAccountCredentials:\n      label: Service Account Credentials\n      tip: The Service Account Credentials JSON can be found in the service accounts section of the Google API developers console.\n    steps:\n      1:\n        title: 'Open <a href=\"https://console.developers.google.com/apis/credentials\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">applications settings</a> in a new window'\n        body:\n         1: Login to your account. Navigate to \"APIs & Services\" and then select \"OAuth consent screen\".\n         2: 'Authorized domains:'\n         3: 'Application homepage link: '\n         4: 'Under Scopes for Google APIs, enable \"email\", \"profile\", and \"openid\".'\n         5: 'Click on \"Save\".'\n        topPrivateDomain: 'Top private domain of:'\n      2:\n        title: 'Navigate to the \"Credentials\" tab to create your OAuth client ID'\n        body:\n          1: 'Select the \"Create Credentials\" dropdown, and select \"OAuth clientID\", then select \"Web application\".'\n          2: 'Authorized JavaScript origins:'\n          3: 'Authorized redirect URIs:'\n          4: 'Click \"Create\", and then click on the \"Download JSON\" button.'\n          5: 'Upload the downloaded JSON file in the OAuth credentials box.'\n      3:\n        title: 'Create Service Account credentials'\n        introduction: 'Follow <a href=\"https://rancher.com/docs/rancher/v2.x/en/admin-settings/authentication/google/#creating-service-account-credentials\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">this</a> guide to:'\n        body:\n          1: Create a service account.\n          2: Generate a key for the service account.\n          3: Add the service account as an OAuth client in your google domain.\n  ldap:\n    freeipa: Configure a FreeIPA server\n    activedirectory: Configure an Active Directory account\n    openldap: Configure an OpenLDAP server\n    defaultLoginDomain:\n      label: Default Login Domain\n      placeholder: eg mycompany\n      hint: This domain will be used if a user logs in without specifying one.\n    cert: Certificate\n    disabledStatusBitmask: Disabled Status Bitmask\n    groupDNAttribute: Group DN Attribute\n    groupMemberMappingAttribute: Group Member Mapping Attribute\n    groupMemberUserAttribute: Group Member User Attribute\n    groupSearchBase:\n      label: Group Search Base\n      placeholder: 'ou=groups,dc=mycompany,dc=com'\n    hostname: Hostname/IP\n    loginAttribute: Login Attribute\n    nameAttribute: Name Attribute\n    nestedGroupMembership:\n      label: Nested Group Membership\n      options:\n        direct: Search only direct group memberships\n        nested: Search direct and nested group memberships\n    objectClass: Object Class\n    password: Password\n    port: Port\n    customizeSchema: Customize Schema\n    users: Users\n    groups: Groups\n    searchAttribute: Search Attribute\n    searchFilter: Search Filter\n    serverConnectionTimeout: Server Connection Timeout\n    serviceAccountDN: Service Account Distinguished Name\n    serviceAccountPassword: Service Account Password\n    serviceAccountInfo: '{vendor} needs a service account that has read-only access to all of the domains that will be able to login, so that we can determine what groups a user is a member of when they make a request with an API key.'\n    starttls:\n      label: Start TLS\n      tip: Upgrades non-encrypted connections by wrapping with TLS during the connection process. Cannot be used in conjunction with TLS.\n    tls: TLS\n    userEnabledAttribute: User Enabled Attribute\n    userMemberAttribute: User Member Attribute\n    userSearchBase:\n      label: User Search Base\n      placeholder: 'e.g. ou=users,dc=mycompany,dc=com'\n    username: Username\n    usernameAttribute: Username Attribute\n    table:\n      server: Server\n      clientId: Client ID\n  saml:\n    entityID: Entity ID Field\n    UID: UID Field\n    adfs: Configure an AD FS account\n    api: '{vendor} API Host'\n    cert:\n      label: Certificate\n      placeholder: Paste in the certificate, starting with -----BEGIN CERTIFICATE-----\n    displayName: Display Name Field\n    groups: Groups Field\n    key:\n      label: Private Key\n      placeholder: Paste in the private key, typically starting with -----BEGIN RSA PRIVATE KEY-----\n    keycloak: Configure a Keycloak account\n    metadata:\n      label: Metadata XML\n      placeholder: Paste in the IDP Metadata XML\n    okta: Configure an Okta account\n    ping: Configure a Ping account\n    shibboleth: Configure a Shibboleth account\n    showLdap: Configure an OpenLDAP Server\n    userName: User Name Field\n  azuread:\n    tenantId: Tenant ID\n    applicationId: Application ID\n    endpoint: Endpoint\n    graphEndpoint: Graph Endpoint\n    tokenEndpoint: Token Endpoint\n    authEndpoint: Auth Endpoint\n  oidc:\n    oidc: Configure an OIDC account\n    keycloakoidc: Configure a Keycloak OIDC account\n    rancherUrl: Rancher URL\n    clientId: Client ID\n    clientSecret: Client Secret\n    customEndpoint:\n      label: Endpoints\n      custom: Specify\n      standard: Generate\n    keycloak:\n      url: Keycloak URL\n      realm: Keycloak Realm\n    issuer: Issuer\n    authEndpoint: Auth Endpoint\n    cert:\n      label: Certificate\n      placeholder: Paste in the certificate, starting with -----BEGIN CERTIFICATE-----\n    key:\n      label: Private Key\n      placeholder: Paste in the private key, typically starting with -----BEGIN RSA PRIVATE KEY-----\n  stateBanner:\n    disabled: 'The {provider} authentication provider is currently disabled.'\n    enabled: 'The {provider} authentication provider is currently enabled.'\n  testAndEnable: Test and Enable Authentication\n  noneEnabled: Local Authentication is always enabled, but you may select another additional authentication provider from those shown below.\n  localEnabled: '{vendor} is configured to allow access to accounts in its local database.'\n  manageLocal: Manage Accounts\n\nauthGroups:\n  actions:\n    refresh: Refresh Group Memberships\n    assignRoles: Assign Global Roles\n  assignEdit:\n    assignTitle: Assign Global Roles To Group\n\nassignTo:\n  title: |-\n    {count, plural,\n      =1 { Assign Cluster To&hellip; }\n      other { Assign {count} Clusters To&hellip; }\n    }\n  labelsTitle: |-\n    {count, plural,\n      =1 { Assign Cluster To&hellip; }\n      other { Assign {count} Clusters To&hellip; }\n    }\n  workspace: Workspace\n\nasyncButton:\n  apply:\n    action: Apply\n    success: Applied\n    waiting: Applying&hellip;\n  continue:\n    action: Continue\n    success: Saved\n    waiting: Saving&hellip;\n  copy:\n    action: Click to Copy\n    success: Copied!\n  create:\n    action: Create\n    success: Created\n    waiting: Creating&hellip;\n  default:\n    action: Action\n    error: Error\n    success: Success\n    waiting: Waiting\n  delete:\n    action: Delete\n    success: Deleted\n    waiting: Deleting&hellip;\n  disable:\n    action: Disable\n    success: Disabled\n    waiting: Disabling&hellip;\n  activate:\n    action:  Activate\n    waiting: Activating&hellip;\n    success: Activated\n  deactivate:\n    action:  Deactivate\n    waiting: Deactivating&hellip;\n    success: Deactivated\n  done:\n    action: Done\n    success: Saved\n    waiting: Saving&hellip;\n  download:\n    action: Download\n    success: Saving\n    waiting: Downloading&hellip;\n  edit:\n    action: Save\n    success: Saved\n    waiting: Saving&hellip;\n  enable:\n    action: Enable\n    success: Enabled\n    waiting: Enabling&hellip;\n  finish:\n    action: Finish\n    success: Finished\n    waiting: Finishing&hellip;\n  import:\n    action: Import\n    success: Imported\n    waiting: Importing&hellip;\n  install:\n    action: Install\n    success: Installing\n    waiting: Starting&hellip;\n  refresh:\n    action: ''\n    actionIcon: refresh\n    error: ''\n    errorIcon: error\n    success: ''\n    successIcon: checkmark\n    waiting: ''\n    waitingIcon: refresh\n  remove:\n    action: Remove\n    success: Removed\n    waiting: Removing&hellip;\n  restore:\n    action: Restore\n    waiting: Restoring&hellip;\n    success: Restored\n  snapshot:\n    action: Snapshot Now\n    waiting: Snapshotting&hellip;\n    success: Snapshot Creating\n  uninstall:\n    action: Uninstall\n    success: Uninstalled\n    waiting: Uninstalling&hellip;\n  update:\n    action: Update\n    success: Updated\n    waiting: Updating&hellip;\n  upgrade:\n    action: Upgrade\n    success: Upgrading\n    waiting: Starting&hellip;\n\nbackupRestoreOperator:\n  backupFilename: Backup Filename\n  deleteTimeout:\n    label: Delete Timeout\n    tip: Seconds to wait for a resource delete to succeed before removing finalizers to force deletion.\n  deployment:\n    rancherNamespace: Rancher ResourceSet Namespace\n    size: Size\n    storage:\n      label: Default Storage Location\n      options:\n        defaultStorageClass: 'Use the default storage class ({name})'\n        none: No default storage location\n        pickPV: Use an existing persistent volume\n        pickSC: Use an existing storage class\n        s3: Use an S3-compatible object store\n      persistentVolume:\n        label: Persistent Volume\n      storageClass:\n        label: Storage Class\n      tip: 'Configure a storage location where all backups are saved by default. You will have the option to override this with each backup, but will be limited to using an S3-compatible object store.'\n      warning: 'This {type} does not have its reclaim policy set to \"Retain\".  Your backups may be lost if the volume is changed or becomes unbound.'\n  encryption: Encryption\n  encryptionConfigName:\n    backuptip: 'Any secret in the <code>cattle-resource-system</code> namespace that has an <code>encryption-provider-config.yaml</code> key. <br/>The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'\n    label: Encryption Config Secret\n    options:\n      none: Store the contents of the backup unencrypted\n      secret: 'Encrypt backups using an <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration\">Encryption Config Secret</a> (Recommended)'\n    restoretip: 'If the backup was performed with encryption enabled, a secret containing the same encryption-provider-config should be used during restore.'\n    warning: 'The contents of this file are necessary to perform a restore from this backup, and are not stored by Rancher Backup.'\n  lastBackup: Last Backup\n  nextBackup: Next Backup\n  noResourceSet: You must define a ResourceSet in this namespace to create a backup CR.\n  prune:\n    label: Prune\n    tip: Delete the resources managed by Rancher that are not present in the backup. (Recommended)\n  resourceSetName: Resource Set\n  restoreFrom:\n    default: The default storage target\n    existing: An existing backup config\n    s3: An S3-compatible object store\n  retentionCount:\n    label: Retention Count\n    units: |-\n      {count, plural,\n        =1 { File }\n        other { Files }\n      }\n  s3:\n    bucketName: Bucket Name\n    credentialSecretName: Credential Secret\n    endpoint: Endpoint\n    endpointCA: Endpoint CA\n    folder: Folder\n    insecureTLSSkipVerify: Skip TLS Verifications\n    region: Region\n    storageLocation: Storage Location\n    titles:\n      backupLocation: Backup Source\n      location: Storage Location\n      s3: S3\n  schedule:\n    label: Schedule\n    options:\n      disabled: One-Time Backup\n      enabled: Recurring Backups\n    placeholder: e.g. @midnight or 0 0 * * *\n  storageSource:\n    configureS3: Use an S3-compatible object store\n    useBackup: Use the s3 location specified on the Backup CR\n    useDefault: Use the default storage location configured during installation\n  targetBackup: Target Backup\n\n\n\ncatalog:\n  app:\n    managed: Managed\n    section:\n      notes: Release Notes\n      readme: Chart README\n      resources: Resources\n      values: Values YAML\n  chart:\n    header:\n      charts: Charts\n    info:\n      appVersion: Application Version\n      chartVersions:\n        label: Chart Versions\n        showMore: Show More\n        showLess: Show Less\n      home: Home\n      maintainers: Maintainers\n      related: Related\n      chartUrls: Chart\n      keywords: Keywords\n    errors:\n      clusterToolExists: This chart has a fixed namespace and name. A matching <a href=\"{url}\">application</a> has been found and any changes will be made to it.\n  charts:\n    all: All\n    categories:\n      all: All Categories\n    certified:\n      other: Other\n      partner: Partner\n      rancher: '{vendor}'\n    header: Deploy Chart\n    noCharts: 'There are no charts available, have you added any repos?'\n    noWindows: Your catalogs do not contain any charts capable of being deployed on a Windows cluster.\n    search: Filter\n  install:\n    action:\n      goToUpgrade: Edit/Upgrade\n    appReadmeMissing: This chart doesn't have any additional chart information.\n    appReadmeTitle: Chart Information (Helm README)\n    chart: Chart\n    error:\n      requiresFound: '<a href=\"{url}\">{name}</a> must be installed before you can install this chart.'\n      requiresMissing: 'This chart requires another chart that provides {name}, but none was found.'\n      insufficientCpu: 'This chart requires {need, number} CPU cores, but the cluster only has {have, number} available.'\n      insufficientMemory: 'This chart requires {need} of memory, but the cluster only has {have} available.'\n    header:\n      install: 'Install {name}'\n      installGeneric: Install Chart\n      upgrade: 'Upgrade {name}'\n    helm:\n      atomic: Atomic\n      cleanupOnFail: Cleanup on Failure\n      crds: Apply custom resource definitions\n      dryRun: Dry Run\n      force: Force\n      historyMax:\n        label: Keep last\n        unit: |-\n          {value, plural,\n            =1 { revision }\n            other { revisions }\n          }\n      hooks: Execute chart hooks\n      openapi: Validate OpenAPI schema\n      resetValues: Reset Values\n      timeout:\n        label: Timeout\n        unit: |-\n          {value, plural,\n            =1 { second }\n            other { seconds }\n          }\n      wait: Wait\n    namespaceIsInProject: \"This chart's target namespace, <code>{namespace}</code>, already exists and cannot be added to a different project.\"\n    project: Install into Project\n    section:\n      chartOptions: Edit Options\n      valuesYaml: Edit YAML\n      diff: Compare Changes\n    slideIn:\n      dock: Dock to bottom\n    steps:\n      basics:\n        label: Metadata\n        subtext: Set App metadata\n        description: This process will help {action, select,\n            install { create }\n            upgrade { upgrade }\n            update { update }\n          } the {existing, select,\n            true { app}\n            false { chart}\n          }. Start by setting some basic information used by {vendor} to manage the App.\n        nsCreationDescription: \"To install the app into a new namespace enter it's name and select it in the Namespace field.\"\n        createNamespace: \"Namespace <code>{namespace}</code> will be created.\"\n      helmValues:\n        label: Values\n        subtext: Change how the App works\n        description: Configure Values used by Helm that help define the App.\n        chartInfo:\n          button: View Chart Info\n          label: Chart Info\n      helmCli:\n        checkbox: Customize Helm options before install\n        label: Helm Options\n        subtext: Change how the app is deployed\n        description: Supply additional deployment options\n    version: Version\n    versions:\n      current: '{ver} (Current)'\n      linux: '{ver} (Linux-only)'\n      windows: '{ver} (Windows-only)'\n  operation:\n    tableHeaders:\n      action: Action\n      releaseName: Release Name\n      releaseNamespace: Release Namespace\n  repo:\n    action:\n      refresh: Refresh\n    all: All\n    gitBranch:\n      label: Git Branch\n      placeholder: e.g. master\n    gitRepo:\n      label: Git Repo URL\n      placeholder: 'e.g. https://github.com/your-company/charts.git'\n    name:\n      rancher-charts: '{vendor}'\n      rancher-partner-charts: Partners\n      rancher-rke2-charts: RKE2\n    target:\n      git: Git Repository containing Helm chart definitions\n      http: http(s) URL to an index generated by Helm\n      label: Target\n    url:\n      label: Index URL\n      placeholder: 'e.g. https://charts.rancher.io'\n  tools:\n    header: Cluster Tools\n    action:\n      install: Install\n      upgrade: Upgrade/Edit\n      edit: Edit\n      remove: Remove\n      manage: Manage\n\nchangePassword:\n  title: Change Password\n  cancel: Cancel\n  deleteKeys:\n    label: Delete all existing API keys\n  changeOnLogin:\n    label: Ask user to change their password on first login\n  generatePassword:\n    label: Generate a random password\n  currentPassword:\n    label: Current Password\n  userGen:\n    newPassword:\n      label: New Password\n    confirmPassword:\n      label: Confirm Password\n  randomGen:\n    generated:\n      label: Generated Password\n  newGeneratedPassword: Suggest a password\n  errors:\n    missmatchedPassword: Passwords do not match\n    failedToChange: Failed to change password\n    failedDeleteKey: Failed to delete key\n    failedDeleteKeys: Failed to delete keys\n\nchartHeading:\n  overview: Overview\n  poweredBy: \"Powered by:\"\n\ncis:\n  addTest: Add Test ID\n  alertNeeded: |-\n    Alerting must be enabled within the CIS chart values.yaml.\n    This requires that the <a tabindex=\"0\" href=\"{link}\">{vendor} Monitoring and Alerting app</a> is installed\n    and the Receivers and Routes are <a target=\"_blank\" rel='noopener noreferrer nofollow' href='https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/configuration/#alertmanager-config'> configured to send out alerts.</a>\n  alertOnComplete: Alert on scan completion\n  alertOnFailure: Alert on scan failure\n  benchmarkVersion: Benchmark Version\n  clusterProvider: Cluster Provider\n  cronSchedule:\n    label: Schedule\n    placeholder: \"e.g. 0 * * * *\"\n  customConfigMap: Custom Benchmark ConfigMap\n  deleteBenchmarkWarning: |-\n    {count, plural,\n      =1 { Any profiles using this benchmark version will no longer work. }\n      other { Any profiles using these benchmark versions will no longer work }\n    }\n  deleteProfileWarning: |-\n    {count, plural,\n      =1 { Any scheduled scans using this profile will no longer work. }\n      other { Any scheduled scans using either of these profiles will no longer work. }\n    }\n  downloadAllReports: Download All Saved Reports\n  downloadLatestReport: Download Latest Report\n  downloadReport: Download Report\n  maxKubernetesVersion: Maximum allowed Kubernetes version\n  minKubernetesVersion: Minimum required Kubernetes version\n  noProfiles: There are no valid ClusterScanProfiles for this cluster type to select.\n  noReportFound: No scan report found\n  profile: Profile\n  reports: Reports\n  retention: Retention Count\n  scan:\n    description: Description\n    fail: Fail\n    lastScanTime: Last Scan Time\n    notApplicable: N/A\n    number: Number\n    pass: Pass\n    remediation: Remediation\n    scanDate: Scan Date\n    scanReport: Scan Report\n    skip: Skip\n    total: Total\n    warn: Warn\n  scheduling:\n    disable: Run scan once\n    enable: Run scan on a schedule\n  scoreWarning:\n    label: Scan state for \"warn\" results\n    protip: Scans with no failures will be marked \"Pass\" by default even if some of the tests generate \"warn\" output. This behavior can be changed by selecting the \"fail\" option from this section.\n  testID: Test ID\n  testsSkipped: Tests Skipped\n  testsToSkip: Tests to Skip\n\ncluster:\n  agentEnvVars:\n    label: Agent Environment\n    detail: Add additional environment variables to the agent container.  This is most commonly useful for configuring a HTTP proxy.\n  custom:\n    nodeRole:\n      label: Node Role\n      detail: Choose what roles the node will have in the cluster.  The cluster needs to have at least one node with each role.\n    advanced:\n      label: Advanced\n      detail: Additional control over how the node will be registered.  These values will often need to be different for each node registered.\n    registrationCommand:\n      label: Registration Command\n      detail: Run this command on each of the existing machines you want to register.\n      insecure: \"Insecure: Select this to skip TLS verification if your server has a self-signed certificate.\"\n  credential:\n    aws:\n      accessKey:\n        label: Access Key\n        placeholder: Your AWS Access Key\n      defaultRegion:\n        help: The default region to use when creating clusters.  Also contacted to verify that this credential works.\n        label: Default Region\n      secretKey:\n        label: SecretKey\n        placeholder: Your AWS Secret Key\n    digitalocean:\n      accessToken:\n        help: Paste in a Personal Access Token from the DigitalOcean <a href=\"https://cloud.digitalocean.com/settings/api/tokens\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">Applications & API</a> screen.\n        label: Access Token\n        placeholder: Your DigitalOcean API Access Token\n    label: Cloud Credential\n    name:\n      label: Credential Name\n      placeholder: Name for this credential (optional)\n    vmwarevsphere:\n        server:\n          label: vCenter or ESXi Server\n          placeholder: vcenter.domain.com\n        port:\n          label: Port\n        username:\n          label: Username\n        password:\n          label: Password\n        note: 'Note: The free ESXi license does not support API access. Only servers with a valid or evaluation license are supported.'\n  description:\n    label: Cluster Description\n    placeholder: Any text you want that better describes this cluster\n  import:\n    commandInstructions: 'Run the <code>kubectl</code> command below on an existing Kubernetes cluster running a supported Kubernetes version to import it into {vendor}:'\n    commandInstructionsInsecure: 'If you get a &quot;certificate signed by unknown authority&quot; error, your {vendor} installation has a self-signed or untrusted SSL certificate.  Run the command below instead to bypass the certificate verification:'\n    clusterRoleBindingInstructions: 'If you get permission errors creating some of the resources, your user may not have the <code>cluster-admin</code> role.  Use this command to apply it:'\n    clusterRoleBindingCommand: 'kubectl create clusterrolebinding cluster-admin-binding --clusterrole cluster-admin --user <your username from your kubeconfig>'\n\n  importAction: Import Existing\n  kubernetesVersion:\n    label: Kubernetes Version\n  toolsTip: Use the new Cluster Tools to manage and install Monitoring, Logging and other tools\n  name:\n    label: Cluster Name\n    placeholder: A unique name for the cluster\n  machineConfig:\n    aws:\n      sizeLabel: |-\n        {apiName}: {cpu}, {memory, number} GiB Memory, {storageSize, plural,\n          =0 {EBS-Only}\n          other {{storageSize, number} GiB {storageType}}\n        }\n    digitalocean:\n      sizeLabel: |-\n        {plan, select,\n          s {Basic: }\n          g {General: }\n          gd {General: }\n          c {CPU: }\n          m {Memory: }\n          so {Storage: }\n          standard {Standard: }\n          other {}\n        }{memoryGb} GB, {vcpus, plural,\n          =1 {# vCPU}\n          other {# vCPUs}\n        }, {disk} GB Disk ({value})\n\n  machinePool:\n    name:\n      label: Pool Name\n      placeholder: A random one will be generated by default\n    nodeTotals:\n      label:\n        controlPlane: '{count} Control Plane'\n        etcd: '{count} etcd'\n        worker: '{count} Worker'\n      tooltip:\n        controlPlane: |-\n          {count, plural,\n            =0 { A cluster needs at least one control plane node to be usable. }\n            =1 { A cluster with only one control plane node is not fault-tolerant. }\n            other {}\n          }\n        etcd: |-\n          {count, plural,\n            =0 { A cluster needs at least one etcd node to be usable. }\n            =1 { A cluster with only one etcd node is not fault-tolerant. }\n            =2 { Clusters should have an odd number of nodes.  A cluster with 2 etcd nodes is not fault-tolerant. }\n            =3 {}\n            =4 { Clusters should have an odd number of nodes. }\n            =5 {}\n            =6 { Clusters should have an odd number of nodes. }\n            =7 {}\n            other { More than 7 etcd nods is not recommended. }\n          }\n        worker: |-\n          {count, plural,\n            =0 { A cluster needs at least one worker node to be usable. }\n            =1 { A cluster with only one worker node is not fault-tolerant. }\n            other {}\n          }\n  provider:\n    aliyunecs: Aliyun ECS\n    aliyunkubernetescontainerservice: Alibaba ACK\n    aliyun:  Alibaba ACK\n    amazonec2: Amazon EC2\n    amazoneks: Amazon EKS\n    aws: Amazon AWS\n    azure: Azure\n    azureaks: Azure AKS\n    aks: Azure AKS\n    baiducloudcontainerengine: Baidu CCE\n    baidu: Baidu CCE\n    cloudca: Cloud.ca\n    custom: Custom\n    digitalocean: DigitalOcean\n    docker: Docker\n    eks: Amazon EKS\n    exoscale: Exoscale\n    google: Google GCE\n    googlegke: Google GKE\n    gke: Google GKE\n    huaweicce: Huawei CCE\n    import: Generic\n    imported: Imported\n    k3s: K3s\n    kubeAdmin: KubeADM\n    linode: Linode\n    local: Local\n    minikube: Minikube\n    oci: Oracle Cloud Infrastructure\n    openstack: OpenStack\n    opentelekomcloudcontainerengine: Open Telekom Cloud CCE\n    otccce: Open Telekom Cloud CCE\n    oracleoke: Oracle OKE\n    otc: Open Telekom Cloud\n    other: Other\n    packet: Packet\n    pinganyunecs: Pinganyun ECS\n    rackspace: RackSpace\n    rancherkubernetesengine: RKE\n    rke2: RKE2\n    rke: RKE1\n    rkeWindows: Windows\n    softlayer: SoftLayer\n    tencenttke: Tencent TKE\n    upcloud: UpCloud\n    vmwarevsphere: vSphere\n    zstack: ZStack\n  providerGroup:\n    create-custom1: Use existing nodes and create a cluster using RKE\n    create-custom2: Use existing nodes and create a cluster using RKE2\n    create-kontainer: Create a cluster in a hosted Kubernetes provider\n    register-kontainer: Register an existing cluster in a hosted Kubernetes provider\n    create-rke1: Provision new nodes and create a cluster using RKE\n    create-rke2: Provision new nodes and create a cluster using RKE2\n    create-template: Use a Catalog Template to create a cluster\n    register-custom: Import any Kubernetes cluster\n  rke2:\n    systemService:\n      rke2-coredns: 'CoreDNS'\n      rke2-ingress-nginx: 'NGINX Ingress Controller'\n      rke2-kube-proxy: 'Kube Proxy'\n      rke2-metrics-server: 'Metrics Server'\n  tabs:\n    ace: Authorized Endpoint\n    advanced: Advanced\n    agentEnv: Agent Environment Vars\n    basic: Basics\n    cluster: Cluster Configuration\n    etcd: etcd\n    networking: Networking\n    machinePools: Machine Pools\n    registry: Private Registry\n    upgrade: Upgrade Strategy\n\nclusterIndexPage:\n  hardwareResourceGauge:\n    consumption: \"{useful} of {total} {units} {suffix}\"\n    cores: Cores\n    pods: Pods\n    ram: Memory\n    used: Used\n    reserved: Reserved\n  header: Cluster Dashboard\n  resourceGauge:\n    totalResources: Total Resources\n  sections:\n    capacity:\n      label: Capacity\n    events:\n      label: Events\n      resource:\n        label: Resource\n      date:\n        label: Date\n    alerts:\n      label: Alerts\n    clusterMetrics:\n      label: Cluster Metrics\n    etcdMetrics:\n      label: Etcd Metrics\n    k8sMetrics:\n      label: Kubernetes Components Metrics\n    gatekeeper:\n      buttonText: Configure Gatekeeper\n      disabled: OPA Gatekeeper is not configured.\n      label: OPA Gatekeeper Constraint Violations\n      noRows: There are no constraints with violations to show.\n    nodes:\n      label: Unhealthy Nodes\n      noRows: There are no unhealthy nodes to show.\n\nconfigmap:\n  tabs:\n    data:\n      label: Data\n      protip: Use this area for anything that's UTF-8 text data\n    binaryData:\n      label: Binary Data\n\ncontainerResourceLimit:\n  cpuPlaceholder: e.g. 1000\n  helpText: Configure how much resources the container can consume by default.\n  helpTextDetail: The amount of resources the container can consume by default.\n  label: Container Default Resource Limit\n  limitsCpu: CPU Limit\n  limitsMemory: Memory Limit\n  memPlaceholder: e.g. 128\n  requestsCpu: CPU Reservation\n  requestsMemory: Memory Reservation\n\ncruResource:\n  backToForm: Back to Form\n  backBody: You will lose any changes made to the YAML.\n  cancelBody: You will lose any changes made to the YAML.\n  confirmBack: \"Okay\"\n  confirmCancel: \"Okay\"\n  reviewForm: \"Keep editing YAML\"\n  reviewYaml: \"Keep editing YAML\"\n  previewYaml: Edit as YAML\n\ndetailText:\n  collapse: Hide\n  binary: '<Binary Data: {n, number} bytes>'\n  empty: '<Empty>'\n  unsupported: '<Value not supported by UI, see YAML>'\n  plusMore: |-\n    {n, plural,\n      =1 {+ 1 more char}\n      other {+ {n, number} more chars}\n    }\n\netcdInfoBanner:\n  hasLeader: \"Etcd has a leader:\"\n  leaderChanges: \"Number of leader changes:\"\n  failedProposals: \"Number of failed proposals:\"\n\nfleet:\n  cluster:\n    summary: Resource Summary\n    nonReady: Non-Ready Bundles\n  fleetSummary:\n    state:\n      success: 'Ready'\n      info: 'Transitioning'\n      warning: 'Warning'\n      error: 'Error'\n      unknown: 'Unknown'\n  gitRepo:\n    tabs:\n      resources: Resources\n      unready: Non-Ready\n    auth:\n      label: Authentication\n      git: Git Authentication\n      helm: Helm Authentication\n    caBundle:\n      label: Certificates\n      placeholder: \"Paste in one or more certificates, starting with -----BEGIN CERTIFICATE----\"\n    paths:\n      label: Paths\n      placeholder: e.g. /directory/in/your/repo\n      addLabel: Add Path\n      empty: The root of the repo is used by default.  To use one or more different directories, add them here.\n    repo:\n      label: Repository URL\n      placeholder: 'e.g. https://github.com/rancher/fleet-examples.git'\n    ref:\n      label: Watch\n      branch: A Branch\n      revision: A Revision\n      branchLabel: Branch Name\n      branchPlaceholder: e.g. master\n      revisionLabel: Tag or Commit Hash\n      revisionPlaceholder: e.g. v1.0.0\n    serviceAccount:\n      label: Service Account Name\n      placeholder: \"Optional: Use a service account in the target clusters\"\n    targetNamespace:\n      label: Target Namespace\n      placeholder: \"Optional: Require all resources to be in this namespace\"\n    target:\n      selectLabel: Target\n      advanced: Advanced\n      cluster: Cluster\n      clusterGroup: Cluster Group\n      label: Deploy To\n      labelLocal: Deploy With\n    targetDisplay:\n      advanced: Advanced\n      cluster: \"Cluster\"\n      clusterGroup: \"Group\"\n      all: All\n      none: None\n      local: Local\n    tls:\n      label: TLS Certificate Verification\n      verify: Require a valid certificate\n      specify: Specify additional certificates to be accepted\n      skip: Accept any certificate (insecure)\n    workspace:\n      label: Workspace\n  clusterGroup:\n    selector:\n      label: Cluster Selectors\n      matchesAll: Matches all {total, number} existing clusters\n      matchesNone: Matches no existing clusters\n      matchesSome: |-\n        {matched, plural,\n          =1 {Matches 1 of {total, number} existing clusters: \"{sample}\"}\n          other {Matches {matched, number} of {total, number} existing clusters, including \"{sample}\"}\n        }\nfooter:\n  docs: Docs\n  download: Download CLI\n  forums: Forums\n  issue: File an Issue\n  slack: Slack\n\ngatekeeperConstraint:\n  match:\n    title: Match\n  tab:\n    enforcementAction:\n      title: Enforcement Action\n    rules:\n      title: Rules\n      sub:\n        labelSelector:\n          addLabel: Add Label\n          title: Label Selector\n    namespaces:\n      sub:\n        excludedNamespaces: Excluded Namespaces\n        namespaces: Namespaces\n        namespaceSelector:\n          addNamespace: Add Namespace\n          title: Namespace Selector\n        scope:\n          title: Scope\n      title: Namespaces\n    parameters:\n      addParameter: Add Parameter\n      editAsForm: Edit as Form\n      editAsYaml: Edit as YAML\n      title: Parameters\n  template: Template\n  violations:\n    title: Violations\n\ngatekeeperIndex:\n  poweredBy: OPA Gatekeeper\n  unavailable: OPA + Gatekeeper is not available in the system-charts catalog.\n  violations: Violations\n\nglance:\n  created: Created\n  cpu: CPU Usage\n  memory: Memory\n  nodes:\n    total:\n      label: |-\n        {count, plural,\n          =1 { Node }\n          other { Total Nodes }\n        }\n  pods: Pods\n  provider: Provider\n  version: Kubernetes Version\n  monitoringDashboard: Monitoring Dashboard\n  installMonitoring: Install Monitoring\n  v1MonitoringInstalled: V1 Monitoring Installed\n\ngrafanaDashboard:\n  failedToLoad: Failed to load graph\n  reload: Reload\n  grafana: Grafana\n\ngraphOptions:\n  detail: Detail\n  summary: Summary\n  refresh: Refresh\n  range: Range\n\nhpa:\n  detail:\n    currentMetrics:\n      header: Current Metrics\n      noMetrics: No Current Metrics\n    metricHeader: '{source} Metric'\n  metricIdentifier:\n    name:\n      label: Metric Name\n      placeholder: e.g. packets-per-second\n    selector:\n      label: Add Selector\n  metricTarget:\n    averageVal:\n      label: Average Value\n    quantity:\n      label: Quantity\n    type:\n      label: Type\n    utilization:\n      label: Average Utilization\n    value:\n      label: Value\n  metrics:\n    headers:\n      metricName: Name\n      objectKind: Object Kind\n      objectName: Object Name\n      quantity: Quantity\n      resource: Resource Name\n      targetName: Target Name\n      value: Value\n    source: Source\n  objectReferance:\n    api:\n      label: Referent API Version\n      placeholder: e.g. apps/v1beta1\n    kind:\n      label: Referent Kind\n      placeholder: e.g. Deployment\n    name:\n      label: Referent Name\n      placeholder: e.g. php-apache\n  tabs:\n    labels: Labels\n    metrics: Metrics\n    target: Target\n    workload: Workload\n  types:\n    cpu: CPU\n    memory: Memory\n  warnings:\n    custom: In order to use custom metrics with HPA, you need to deploy the custom metrics server such as prometheus adapter.\n    external: In order to use external metrics with HPA, you need to deploy the external metrics server such as prometheus adapter.\n    noMetric: In order to use resource metrics with HPA, you need to deploy the metrics server.\n    resource: The selected target reference does not have the correct resource requests on the spec. Without this the HPA metric will have no effect.\n  workloadTab:\n    current: Current Replicas\n    last: Last Scale Time\n    max: Maximum Replicas\n    min: Minimum Replicas\n    targetReference: Target Reference\n\nimport:\n  title: Import YAML\n  defaultNamespace:\n    label: Default Namespace\n  success: |-\n    Applied {count, plural,\n    =1 {1 Resource}\n    other {# Resources}\n    }\n\ningress:\n  certificates:\n    addCertificate: Add Certificate\n    addHost: Add Host\n    certificate:\n      label: Certificate - Secret Name\n      doesntExist: The selected certificate does not exist\n    defaultCertLabel: Default Ingress Controller Certificate\n    headers:\n      certificate: Certificate\n      hosts: Hosts\n    host:\n      label: Host\n      placeholder: e.g. example.com\n    label: Certificates\n    removeHost: Remove\n  defaultBackend:\n    label: Default Backend\n    noServiceSelected: No default backend is configured.\n    port:\n      label: Port\n      placeholder: e.g. 80 or http\n    targetService:\n      label: Target Service\n      doesntExist: The selected service does not exist\n    warning: \"Warning: Default backend is used globally for the entire cluster.\"\n  rules:\n    addPath: Add Path\n    addRule: Add Rule\n    headers:\n      pathType: Path Type\n      path: Path\n      port: Port\n      target: Target Service\n      certificates: Certificates\n    hostname: Hostname\n    path:\n      label: Path\n      placeholder: e.g. /foo\n    port:\n      label: Port\n      placeholder: e.g. 80 or http\n    removePath: Remove\n    requestHost:\n      label: Request Host\n      placeholder: e.g. example.com\n    target:\n      label: Target Service\n      doesntExist: The selected service does not exist\n    title: Rules\n  rulesAndCertificates:\n    title: Rules and Certificates\n    defaultCertificate: default\n  target:\n    default: Default\n\ninternalExternalIP:\n  none: None\n\nistio:\n  links:\n    kiali:\n      label: Kiali\n      description: 'Visualization of services within a service mesh and how they are connected. For Kiali to display data, you need Prometheus installed. If you need a monitoring solution, install <a tabindex=\"0\" href=\"{link}\">{vendor} monitoring</a>.'\n    jaeger:\n      label: Jaeger\n      description: Monitor and Troubleshoot microservices-based distributed systems.\n    disabled: '{app} is not installed'\n  cni: Enabled CNI\n  customOverlayFile:\n    label: Custom Overlay File\n    tip: 'The <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://istio.io/latest/docs/setup/install/istioctl/#customizing-the-configuration\">overlay file</a> allows for additional configuration on top of the base {vendor} Istio installation. You can utilize the <a href=\"https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/\" target=\"_blank\" rel=\"noopener noreferrer nofollow\" >IstioOperator API</a> to make changes and additions for all components and apply those changes via this overlay YAML file.'\n  description: '{vendor} Istio helm chart installs a minimal Istio configuration for you to get started integrating with your applications.\n  If you would like to get additional information about Istio, visit <a target=\"_blank\" href=\"https://istio.io/latest/docs/concepts/what-is-istio\" rel=\"noopener noreferrer nofollow\">https://istio.io/latest/docs/concepts/what-is-istio/</a>'\n  egressGateway: Enabled Egress Gateway\n  ingressGateway: Enabled Ingress Gateway\n  istiodRemote: Enabled istiodRemote\n  kiali: Enabled Kiali\n  pilot: Enabled Pilot\n  policy: Enabled Policy\n  poweredBy: Powered by <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href='https://istio.io/latest/'>Istio</a>\n  telemetry: Enabled Telemetry\n  titles:\n    components: Components\n    customAnswers: Custom Answers\n    advanced: Advanced Settings\n    description: Description\n  tracing: Enabled Jaeger Tracing (limited)\n  v1Warning: Please uninstall the current Istio version in the <code>istio-system</code> namespace before attempting to install this version.\n\nlabels:\n  addLabel: Add Label\n  addSetLabel: Add/Set Label\n  addAnnotation: Add Annotation\n  labels:\n    title: Labels\n  annotations:\n    title: Annotations\n\nlanding:\n  clusters:\n    title: Clusters\n    provider: Provider\n    kubernetesVersion: Kubernetes Version\n    explorer: Explorer\n    explore: Explore\n    cores: |-\n      {count, plural,\n      =1 {core}\n      other {cores}}\n  seeWhatsNew: Learn more about the improvements and new capabilities in this version.\n  whatsNewLink: \"What's new in 2.5\"\n  learnMore: Learn More\n  gettingStarted:\n    title: Getting Started\n    body: Take a look at the quick getting started guide. For Cluster Manager users, learn more about where you can find you favorite features in the Dashboard UI.\n  community:\n    title: Community Support\n    docs: Docs\n    forums: Forums\n  commercial:\n    title: Commercial Support\n    body: Learn about commercial support\n  landingPrefs:\n    title: What do you want to see when you log in?\n    body: \"You can change where you land when you login:\"\n    options:\n      homePage: Take me to the home page\n      lastVisited: Take me to the area I last visited\n      custom: \"Take me to cluster:\"\n  welcomeToRancher: 'Welcome to {vendor}'\n\nlogging:\n  clusterFlow:\n    noOutputsBanner: There are no cluster outputs in the selected namespace.\n  flow:\n    clusterOutputs:\n      doesntExistTooltip: This cluster output doesn't exist\n      label: Cluster Outputs\n    matches:\n      label: Matches\n      addSelect: Add Include Rule\n      addExclude: Add Exclude Rule\n    filters:\n      label: Filters\n    outputs:\n      doesntExistTooltip: This output doesn't exist\n      label: Outputs\n  install:\n    k3sContainerEngine: K3S Container Engine\n    enableAdditionalLoggingSources: Enable enhanced cloud provider logging\n    dockerRootDirectory: Docker Root Directory\n  elasticsearch:\n    host: Host\n    scheme: Scheme\n    port: Port\n    indexName: Index Name\n    user: User\n    password: Password from Secret\n    caFile:\n      label: CA File from Secret\n    clientCert:\n      label: Client Cert from Secret\n      placeholder: Paste in the CA certificate\n    clientKey:\n      label: Client Key from Secret\n      placeholder: Paste in the client key\n    clientKeyPass: Client Key Pass from Secret\n  kafka:\n    brokers: Brokers\n    defaultTopic: Default Topic\n    saslOverSsl: SASL Over SSL\n    scramMechanism: Scram Mechanism\n    username: Username from Secret\n    password: Password from Secret\n    sslCaCert:\n      label: CA Cert from Secret\n      placeholder: Paste in the CA certificate\n    sslClientCert:\n      label: Cert from Secret\n      placeholder: Paste in the client cert\n    sslClientCertChain:\n      label: Cert Chain from Secret\n      placeholder: Paste in the client cert chain\n    sslClientCertKey: Cert Key from Secret\n  loki:\n    url: URL\n    tenant: Tenant\n    username: User from Secret\n    password: Password from Secret\n    configureKubernetesLabels: Configure Kubernetes metadata in a Prometheus like format\n    extractKubernetesLabels: Extract Kubernetes labels as Loki labels\n    dropSingleKey: If a record only has 1 key, then just set the log line to the value and discard the key\n    caCert: CA Cert from Secret\n    cert: Cert from Secret\n    key: Key from Secret\n  awsElasticsearch:\n    url: URL\n    keyId: Key ID from Secret\n    secretKey: Secret Key from Secret\n  azurestorage:\n    storageAccount: Account from Secret\n    accessKey:  Access Key from Secret\n    container: Container\n    path: Path\n    storeAs: Store As\n  cloudwatch:\n    keyId: Key ID from Secret\n    secretKey: Secret Key from Secret\n    endpoint: Endpoint\n    region: Region\n  datadog:\n    apiKey: API Key from Secret\n    useSSL: Use SSL\n    useCompression: Use Compression\n    host: Host\n  file:\n    path: Path\n  gcs:\n    project: Project\n    credentialsJson: Credentials from Secret\n    bucket: Bucket\n    path: Path\n    overwriteExistingPath: Overwrite Existing Path\n  kinesisStream:\n    streamName: Stream Name\n    keyId: Key ID from Secret\n    secretKey: Secret Key from Secret\n  logdna:\n    apiKey: API Key\n    hostname: Hostname\n    app: App\n  logz:\n    url: URL\n    port: Port\n    token: Api Token from Secret\n    enableCompression: Enable Compression\n  newrelic:\n    apiKey: API Key from Secret\n    licenseKey: License Key from Secret\n    baseURI: Base URI\n  sumologic:\n    endpoint: Endpoint from Secret\n    sourceName: Source Name\n  syslog:\n    host: Host\n    port: Port\n    transport: Transport\n    insecure: insecure\n    trustedCaPath: CA Path from Secret\n    format:\n      title: Format\n      type: Type\n      addNewLine: Add New Line\n      messageKey: Message Key\n    buffer:\n      title: Buffer\n      tags: Tags\n      chunkLimitSize: Chunk Limit Size\n      chunkLimitRecords: Chunk Limit chunkLimitRecords\n      totalLimitSize: Total Limit Size\n      flushInterval: Flush Interval\n      timekey: Timekey\n      timekeyWait: Timekey Wait\n      timekeyUseUTC: Timekey Use UTC\n  s3:\n    keyId: Key ID from Secret\n    secretKey: Secret Key from Secret\n    endpoint: Endpoint\n    bucket: Bucket\n    path: Path\n    overwriteExistingPath: Overwrite Existing Path\n  output:\n    selectOutputs: Select Outputs\n    selectBanner: Select to configure an output\n    sections:\n      target: Target\n      access: Access\n      certificate: Connection\n      labels: Labels\n  outputProviders:\n    elasticsearch: Elasticsearch\n    splunkHec: Splunk\n    kafka: Kafka\n    forward: Fluentd\n    loki: Loki\n    awsElasticsearch: Amazon Elasticsearch\n    azurestorage: Azure Storage\n    cloudwatch: Cloudwatch\n    datadog: Datadog\n    file: File\n    gcs: GCS\n    kinesisStream: Kinesis Stream\n    logdna: LogDNA\n    logz: LogZ\n    newrelic: New Relic\n    sumologic: SumoLogic\n    syslog: Syslog\n    s3: S3\n    unknown: Unknown\n  overview:\n    poweredBy: Banzai Cloud\n    clusterLevel: Cluster-Level\n    namespaceLevel: Namespace-Level\n  provider: Provider\n  splunk:\n    host: Host\n    port: Port\n    protocol: Protocol\n    index: Index\n    token: Token from Secret\n    insecureSsl: Insecure SSL\n    indexName: Index Name\n    source: Source\n    caFile: CA File from Secret\n    caPath: CA Path from Secret\n    clientCert: Client Cert from Secret\n    clientKey: Client Key from Secret\n  forward:\n    host: Host\n    port: Port\n    sharedKey: Shared Key from Secret\n    username: Username from Secret\n    password: Password from Secret\n    clientCertPath: Client Cert Path from Secret\n    clientPrivateKeyPath: Client Private Key Path from Secret\n    clientPrivateKeyPassphrase: Client Private Key Passphrase from Secret\n\nlonghorn:\n  overview:\n    title: Overview\n    subtitle: \"Powered By: <a href='https://github.com/longhorn' target='_blank' rel='noopener nofollow noreferrer'>Longhorn</a>\"\n    linkedList:\n      longhorn:\n        label: 'Longhorn'\n        description: 'Manage storage system via UI'\n        na: Resource Unavailable\n\nlogin:\n  howdy: Howdy!\n  welcome: Welcome to {vendor}\n  loggedOut: You have been logged out.\n  loginAgain: Log in again to continue.\n  error: An error occurred logging in. Please try again.\n  useLocal: Use a local user\n  loginWithProvider: Log in with {provider}\n  username: Username\n  password: Password\n  loggingIn: Logging in...\n  loggedIn: Logged in\n  loginWithLocal: Log in with Local User\n  useProvider: Use a {provider} user\n\nmembers:\n  clusterMembers: Cluster Members\n  createActionLabel: Add\n  clusterPermissions:\n    noDescription: User created - no description\n    label: Cluster Permissions\n    description: Controls what access users have to the Cluster\n    createProjects: Create Projects\n    manageClusterBackups: Manage Cluster Backups\n    manageClusterCatalogs: Manage Cluster Catalogs\n    manageClusterMembers: Manage Cluster Members\n    manageNodes: Manage Nodes\n    manageStorage: Manage Storage\n    viewAllProjects: View All Projects\n    viewClusterCatalogs: View Cluster Catalogs\n    viewClusterMembers: View Cluster Members\n    viewNodes: View Nodes\n    owner:\n      label: Owner\n      description: Owners have full control over the Cluster and all resources inside it.\n    member:\n      label: Member\n      description: Members can manage the resources inside the Cluster but not change the Cluster itself.\n    custom:\n      label: Custom\n      description: Choose individual roles for this user.\n\nmonitoring:\n  accessModes:\n    many: ReadWriteMany\n    once: ReadWriteOnce\n    readOnlyMany: ReadOnlyMany\n  aggregateDefaultRoles:\n    label: Aggregate to Default Kubernetes Roles\n    tip: 'Adds labels to the ClusterRoles deployed by the Monitoring chart to <a target=\"_blank\" rel=\"noopener nofollow noreferrer\" href=\"https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles\"> aggregate to the corresponding default k8s admin, edit, and view ClusterRoles.</a>'\n  alerting:\n    config:\n      label: Alert Manager Config\n    enable:\n      label: Deploy Alertmanager\n    secrets:\n      additional:\n        info: Secrets should be mounted at <pre class='inline-block m-0'>/etc/alertmanager/secrets/</pre>\n        label: Additional Secrets\n      existing: Choose an existing config secret\n      info: |\n        <span class=\"text-bold\">Create default config</span>: A Secret containing your Alertmanager Config will be created in the <pre class='inline-block m-0'>cattle-monitoring-system</pre> namespace on deploying this chart under the name <pre class='inline-block m-0'>alertmanager-rancher-monitoring-alertmanager</pre>. By default, this Secret will never be modified on an uninstall or upgrade of this chart. <br />\n        Once you have deployed this chart, you should edit the Secret via the UI in order to add your custom notification configurations that will be used by Alertmanager to send alerts. <br /> <br />\n        <span class=\"text-bold\">Choose an existing config secret</span>: You must specify a Secret that exists in the <pre class='inline-block m-0'>cattle-monitoring-system</pre> namespace. If the namespace does not exist, you will not be able to select an existing secret.\n      label: Alertmanager Secret\n      new: Create default config\n      radio:\n        label: Config Secret\n    templates:\n      keyLabel: File Name\n      label: Template Files\n      valueLabel: YAML Template\n    title: Configure Alertmanager\n  clusterType:\n    label: Cluster Type\n    placeholder: Select cluster type\n  createDefaultRoles:\n    label: Create Default Monitoring Cluster Roles\n    tip: 'Creates <code>monitoring-admin</code>, <code>monitoring-edit</code>, and <code>monitoring-view</code> ClusterRoles that can be assigned to users to provide permissions to CRDs installed by the Monitoring chart.'\n  etcdNodeDirectory:\n    label: etcd Node Certificate Directory\n    tooltip: 'For clusters that use RancherOS for the etcd nodes, this option should be set to <pre class=''inline-block m-0''>/opt/rke/etc/kubernetes/ssl</pre>. Hybrid environments that require specifying multiple certificate directories (e.g. an etcd plane composed of both RancherOS and Ubuntu hosts) are not supported.'\n  grafana:\n    storage:\n      annotations: PVC Annotations\n      className: Storage Class Name\n      existingClaim: Use Existing Claim\n      finalizers: PVC Finalizers\n      label: Grafana Storage\n      mode: Access Mode\n      selector: Selector\n      size: Size\n      subpath: Use Subpath\n      type: Persistent Storage Types\n      types:\n        existing: Enable With Existing PVC\n        statefulset: Enable with StatefulSet Template\n        template: Enable with PVC Template\n      volumeMode: Volume Mode\n      volumeName: Volume Name\n    title: Configure Grafana\n  hostNetwork:\n    label: Use Host Network For Prometheus Operator\n    tip: If you are using a managed Kubernetes cluster with custom CNI (e.g. Calico), you must enable this option to allow a managed control plane to contact the admission webhook exposed by Prometheus Operator to mutate or validate incoming PrometheusRules.\n  overview:\n    alertsList:\n      ends:\n        label: Ends At\n      label: Active Alerts\n      message:\n        label: Message\n      severity:\n        label: Severity\n      start:\n        label: Starts At\n    linkedList:\n      alertManager:\n        description: Active Alerts\n        label: Alertmanager\n      grafana:\n        description: Metrics Dashboards\n        label: Grafana\n      na: Resource Unavailable\n      prometheusPromQl:\n        description: PromQL Graph\n        label: Prometheus Graph\n      prometheusRules:\n        description: Configured Rules\n        label: Prometheus Rules\n      prometheusTargets:\n        description: Configured Targets\n        label: Prometheus Targets\n    subtitle: 'Powered By: <a href=\"https://github.com/coreos/prometheus-operator\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">Prometheus</a>'\n    title: Dashboard\n  prometheus:\n    config:\n      adminApi: Admin API\n      evaluation: Evaluation Interval\n      ignoreNamespaceSelectors:\n        help: 'Ignoring Namespace Selectors allows Cluster Admins to limit teams from monitoring resources outside of namespaces they have permissions to but can break the functionality of Apps that rely on setting up Monitors that scrape targets across multiple namespaces, such as Istio.'\n        label: Namespace Selectors\n        radio:\n          enforced: 'Use: Monitors can access resources based on namespaces that match the namespace selector field'\n          ignored: 'Ignore: Monitors can only access resources in the namespace they are deployed in'\n      limits:\n        cpu: CPU Limit\n        memory: Memory Limit\n      requests:\n        cpu: Requested CPU\n        memory: Requested Memory\n      resourceLimits: Resource Limits\n      retention: Retention\n      retentionSize: Retention Size\n      scrape: Scrape Interval\n    storage:\n      className: Storage Class Name\n      label: Persistent Storage for Prometheus\n      mode: Access Mode\n      selector: Selector\n      selectorWarning: 'If you are using a dynamic provisioner (e.g. Longhorn), no Selectors should be specified since a PVC with a non-empty selector can''t have a PV dynamically provisioned for it.'\n      size: Size\n      volumeMode: Volume Mode\n      volumeName: Volume Name\n    title: Configure Prometheus\n    warningInstalled: |\n      Warning: Prometheus Operators are currently deployed. Deploying multiple Prometheus Operators onto one cluster is not currently supported. Please remove all other Prometheus Operator deployments from this cluster before trying to install this chart.\n      If you are migrating from an older version of {vendor} with Monitoring enabled, please disable Monitoring on this cluster completely before attempting to install this chart.\n  receiver:\n    fields:\n      name: Name\n    tls:\n      label: SSL\n      caFilePath:\n        label: CA File Path\n        placeholder: e.g. ./ca-file.csr\n      certFilePath:\n        label: Cert File Path\n        placeholder: e.g. ./cert-file.crt\n      keyFilePath:\n        label: Key File Path\n        placeholder: e.g. ./key-file.pfx\n      secretsBanner: The file paths below must be referenced in <pre class=\"inline-block m-0 p-0 vertical-middle\">alertmanager.alertmanagerSpec.secrets</pre> when deploying the Monitoring chart. For more information see our <a href=\"https://rancher.com/docs/rancher/v2.5/en/monitoring-alerting/\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">documentation</a>.\n\n  route:\n    fields:\n      groupBy: Group By\n      groupInterval: Group Interval\n      groupWait: Group Wait\n      receiver: Receiver\n      repeatInterval: Repeat Interval\n  routesAndReceivers: Routes and Receivers\n  monitors: Monitors\n  installSteps:\n    uninstallV1:\n      stepTitle: Uninstall V1\n      stepSubtext: Uninstall Previous Monitoring\n      warning1: V1 Monitoring is currently deployed. This needs to be uninstalled before V2 monitoring can be installed.\n      warning2: <a target=\"blank\" href=\"https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/migrating/#migrating-from-monitoring-v1-to-monitoring-v2\" target='_blank' rel='noopener nofollow'>Learn more</a> about migrating to V2 Monitoring.\n      success1: V1 monitoring successfully uninstalled.\n      success2: Press Next to continue\n  tabs:\n    alerting: Alerting\n    general: General\n    grafana: Grafana\n    prometheus: Prometheus\n  v1Warning: 'Monitoring is currently deployed from Cluster Manager. If you are migrating from an older version of {vendor} with monitoring enabled, please disable monitoring in Cluster Manager before attempting to install the new {vendor} Monitoring chart in Cluster Explorer.'\n  volume:\n    modes:\n      block: Block\n      file: Filesystem\n\nmonitoringReceiver:\n  addButton: Add {type}\n  custom:\n    label: Custom\n    title: Custom Config\n    info: The YAML provided here will be directly appended to your receiver within the Alertmanager Config Secret.\n  email:\n    label: Email\n    title: Email Config\n  opsgenie:\n    label: Opsgenie\n    title: Opsgenie Config\n  pagerduty:\n    label: PagerDuty\n    title: PagerDuty Config\n    info: \"See additional info on <a href='https://www.pagerduty.com/docs/guides/prometheus-integration-guide/' target='_blank' rel='noopener nofollow' class='flex-right'>creating an Integration Key for PagerDuty</a>.\"\n  slack:\n    label: Slack\n    title: Slack Config\n    info: \"See additional info on <a href='https://rancher.slack.com/apps/A0F7XDUAZ-incoming-webhooks' target='_blank' rel='noopener noreferrer nofollow'>creating Incoming Webhooks for Slack</a> .\"\n  webhook:\n    label: Webhook\n    title: Webhook Config\n    urlTooltip: For some webhooks this a url that points to the service DNS\n    modifyNamespace: If <pre class=\"inline-block m-0 p-0 vertical-middle\">rancher-alerting-drivers</pre> default values were changed, please update the url below in the format http://&lt;new_service_name&gt;.&lt;new_namespace&gt.svc.&lt;port&gt/&lt;path&gt\n    banner: To use MS Teams or SMS you will need to have <pre class=\"inline-block m-0 p-0 vertical-middle\">rancher-alerting-drivers</pre> installed first.\n    add:\n      generic: Generic\n      msTeams: MS Teams\n      alibabaCloudSms: SMS\n  auth:\n    label: Auth\n    authType: Auth Type\n    username: Username\n    password: Password\n    none:\n      label: None\n    bearerToken:\n      label: Bearer Token\n      placeholder: e.g. secret-token\n    basicAuth:\n      label: Basic Auth\n    bearerTokenFile:\n      label: Bearer Token File\n      placeholder: e.g. ./user_token\n  shared:\n    proxyUrl:\n      label: Proxy URL\n      placeholder: e.g. http://my-proxy/\n    sendResolved:\n      label: Enable send resolved alerts\n\nmonitoringRoute:\n  groups:\n    label: Group By\n  info: This is the top-level Route used by Alertmanager as the default destination for any Alerts that do not match any other Routes. This Route must exist and cannot be deleted.\n  interval:\n    label: Group Interval\n  matching:\n    info: The root route has to match everything so matching cannot be configured.\n    label: Match\n  receiver:\n    label: Receiver\n  regex:\n    label: Match Regex\n  repeatInterval:\n    label: Repeat Interval\n  wait:\n    label: Group Wait\n\nmoveModal:\n  title: Move to a new project?\n  description: 'You are moving the following namespaces:'\n  moveButtonLabel: Move\n\nnameNsDescription:\n  name:\n    label: Name\n    placeholder: 'A unique name'\n  namespace:\n    label: Namespace\n    placeholder:\n  workspace:\n    label: Workspace\n    placeholder:\n  description:\n    label: Description\n    placeholder: Any text you want that better describes this resource\n\nnamespace:\n  containerResourceLimit: Container Resource Limit\n  project:\n    label: Project\n  resources: Resources\n  enableAutoInjection: Enable Istio Auto Injection\n  disableAutoInjection: Disable Istio Auto Injection\n  move: Move\n\nnamespaceFilter:\n  selected:\n    label: \"{total} items selected\"\n\nnamespaceList:\n  selectLabel: Namespace\n  addLabel: Add Namespace\n\nnode:\n  detail:\n    detailTop:\n      containerRuntime: Container Runtime\n      internalIP: Internal IP\n      externalIP: External IP\n      os: OS\n      version: Version\n    glance:\n      consumptionGauge:\n        used: Used\n        amount: \"{used} of {total} {unit}\"\n        cpu: CPU\n        memory: MEMORY\n        pods: PODS\n      diskPressure: Disk Pressure\n      kubelet: kubelet\n      memoryPressure: Memory Pressure\n      pidPressure: PID Pressure\n    tab:\n      conditions: Conditions\n      images: Images\n      metrics: Metrics\n      info:\n        label: Info\n        key:\n          architecture: Architecture\n          bootID: Boot ID\n          containerRuntimeVersion: Container Runtime Version\n          kernelVersion: Kernel Version\n          kubeProxyVersion: Kube Proxy Version\n          kubeletVersion: Kubelet Version\n          machineID: Machine ID\n          operatingSystem: Operating System\n          osImage: Image\n          systemUUID: System UUID\n      pods: Pods\n      taints: Taints\n\npersistentVolume:\n  pluginConfiguration:\n    label: Plugin configuration\n  customize:\n    label: Customize\n    affinity:\n      label: Node Selectors\n      addLabel: Add Node Selector\n    assignToStorageClass:\n      label: Assign to Storage Class\n    mountOptions:\n      label: Mount Options\n      addLabel: Add Option\n    accessModes:\n      label: Access Modes\n      readWriteOnce: Single Node Read-Write\n      readOnlyMany: Many Nodes Read-Only\n      readWriteMany: Many Nodes Read-Write\n  shared:\n    partition:\n      label: Partition\n      placeholder: e.g. 1; 0 for entire device\n    readOnly:\n      label: Read Only\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    secretName:\n      label: Secret Name\n      placeholder: e.g. secret\n    secretNamespace:\n      label: Secret Namespace\n      placeholder: e.g. default\n    monitors:\n      add: Add Monitor\n  vsphereVolume:\n    label: VMWare vSphere Volume\n    volumePath:\n      label: Volume Path\n      placeholder: e.g. /\n    storagePolicyName:\n      label: Storage Policy Name\n      placeholder: e.g. sp\n    storagePolicyId:\n      label: Storage Policy ID\n      placeholder: e.g. sp1\n  csi:\n    label: CSI (Unsupported)\n    driver:\n      label: Driver\n      placeholder: e.g. driver.longhorn.io\n    volumeHandle:\n      label: Volume Handle\n      placeholder: e.g. pvc-xxxx\n    volumeAttributes:\n      add: Add Volume Attribute\n    nodePublishSecretName:\n      label: Node Publish Secret Name\n      placeholder: e.g. secret\n    nodePublishSecretNamespace:\n      label: Node Publish Secret Namespace\n      placeholder: e.g. default\n    nodeStageSecretName:\n      label: Node Stage Secret Name\n      placeholder: e.g. secret\n    nodeStageSecretNamespace:\n      label: Node Stage Secret Namespace\n      placeholder: e.g. default\n    controllerExpandSecretName:\n      label: Controller Expand Secret Name\n      placeholder: e.g. secret\n    controllerExpandSecretNamespace:\n      label: Controller Expand Secret Namespace\n      placeholder: e.g. default\n    controllerPublishSecretName:\n      label: Controller Publish Secret Name\n      placeholder: e.g. secret\n    controllerPublishSecretNamespace:\n      label: Controller Publish Secret Namespace\n      placeholder: e.g. default\n  cephfs:\n    label: Ceph Filesystem (Unsupported)\n    path:\n      label: Path\n      placeholder: e.g. /var\n    user:\n      label: User\n      placeholder: e.g. root\n    secretFile:\n      label: Secret File\n      placeholder: e.g. secret\n  rbd:\n    label: Ceph RBD (Unsupported)\n    user:\n      label: User\n      placeholder: e.g. root\n    keyRing:\n      label: Key Ring\n      placeholder: e.g. /etc/ceph/keyring\n    pool:\n      label: Pool\n      placeholder: e.g. rbd\n    image:\n      label: Image\n      placeholder: e.g. image\n  fc:\n    label: Fibre Channel (Unsupported)\n    targetWWNS:\n      add: Add Target WWN\n    wwids:\n      add: Add WWID\n    lun:\n      label: Lun\n      placeholder: e.g. 2\n  flexVolume:\n    label: Flex Volume (Unsupported)\n    driver:\n      label: Driver\n      placeholder: e.g. driver\n    options:\n      add: Add Option\n  flocker:\n    label: Flocker (Unsupported)\n    datasetName:\n      label: Dataset Name\n      placeholder: e.g. dataset\n    datasetUUID:\n      label: Dataset UUID\n      placeholder: e.g. uuid\n  glusterfs:\n    label: Gluster Volume (Unsupported)\n    endpoints:\n      label: Endpoints\n      placeholder: e.g. glusterfs-cluster\n    path:\n      label: Path\n      placeholder: e.g. kube-vol\n  iscsi:\n    label: iSCSI Target (Unsupported)\n    initiatorName:\n      label: Initiator Name\n      placeholder: iqn.1994-05.com.redhat:1df7a24fcb92\n    iscsiInterface:\n      label: iSCSI Interface\n      placeholder: e.g. interface\n    chapAuthDiscovery:\n      label: Chap Auth Discovery\n    chapAuthSession:\n      label: Chap Auth Session\n    iqn:\n      label: IQN\n      placeholder: iqn.2001-04.com.example:storage.kube.sys1.xyz\n    lun:\n      label: Lun\n      placeholder: e.g. 2\n    targetPortal:\n      label: Target Portal\n      placeholder: e.g. portal\n    portals:\n      add: Add Portal\n  cinder:\n    label: Openstack Cinder Volume (Unsupported)\n    volumeId:\n      label: Volume ID\n      placeholder: e.g. vol\n  quobyte:\n    label: Quobyte Volume (Unsupported)\n    volume:\n      label: Volume\n      placeholder: e.g. vol\n    user:\n      label: User\n      placeholder: e.g. root\n    group:\n      label: Group\n      placeholder: e.g. abc\n    registry:\n      label: Registry\n      placeholder: e.g. abc\n  photonPersistentDisk:\n    label: Photon Volume (Unsupported)\n    pdId:\n      label: PD ID\n      placeholder: e.g. abc\n  portworxVolume:\n    label: Portworx Volume (Unsupported)\n    volumeId:\n      label: Volume ID\n      placeholder: e.g. abc\n  scaleIO:\n    label: ScaleIO Volume (Unsupported)\n    volumeName:\n      label: Volume Name\n      placeholder: e.g. vol-0\n    gateway:\n      label: Gateway\n      placeholder: e.g. https://localhost:443/api\n    protectionDomain:\n      label: Protection Domain\n      placeholder: e.g. pd01\n    storageMode:\n      label: Storage Mode\n      placeholder: e.g. ThinProvisioned\n    storagePool:\n      label: Storage Pool\n      placeholder: e.g. sp01\n    system:\n      label: System\n      placeholder: e.g. scaleio\n    sslEnabled:\n      label: SSL Enabled\n  storageos:\n    label: StorageOS (Unsupported)\n    volumeName:\n      label: Volume Name\n      placeholder: e.g. vol\n    volumeNamespace:\n      label: Volume Namespace\n      placeholder: e.g. default\n  nfs:\n    label: NFS Share\n    path:\n      label: Path\n      placeholder: e.g. /var\n    server:\n      label: Server\n      placeholder: e.g. 10.244.1.4\n  longhorn:\n    label: Longhorn\n    volumeHandle:\n      label: Volume Handle\n      placeholder: e.g. pvc-xxxx\n    options:\n      label: Options\n      addLabel: Add\n  local:\n    label: Local\n    path:\n      label: Path\n      placeholder: e.g. /mnt/disks/ssd1\n  hostPath:\n    label: HostPath\n    pathOnTheNode:\n      label: Path on the Node\n      placeholder: /mnt/disks/ssd1\n    mustBe:\n      label: The Path on the Node must be\n      anything: 'Anything: do not check the target path'\n      directory: A directory, or create if it does not exist\n      file: A file, or create if it does not exist\n      existingDirectory: An existing directory\n      existingFile: An existing file\n      existingSocket: An existing socket\n      existingCharacter: An existing character device\n      existingBlock: An existing block device\n  gcePersistentDisk:\n    label: Google Persistent Disk\n    persistentDiskName:\n      label: Persistent Disk Name\n      placeholder: e.g. abc\n  awsElasticBlockStore:\n    label: Amazon EBS Disk\n    volumeId:\n      label: Volume ID\n      placeholder: e.g. volume1\n  azureFile:\n    label: Azure Filesystem\n    shareName:\n      label: Share Name\n      placeholder: e.g. abc\n  azureDisk:\n    label: Azure Disk\n    diskName:\n      label: Disk Name\n      placeholder: e.g. kubernetes-pvc\n    diskURI:\n      label: Disk URI\n      placeholder: e.g. https://example.com/disk\n    kind:\n      label: Kind\n      dedicated: Dedicated\n      managed: Managed\n      shared: Shared\n    cachingMode:\n      label: Caching Mode\n      none: None\n      readOnly: Read Only\n      readWrite: Read Write\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    readOnly:\n      label: Read Only\n\npersistentVolumeClaim:\n  accessModes: Access Modes\n  capacity: Capacity\n  storageClass: Storage Class\n  useDefault: Use the default class\n  volumes: Persistent Volumes\n  volumeName: Persistent Volume Name\n  source:\n    label: Source\n    options:\n      new: Use a Storage Class to provision a new Persistent Volume\n      existing: Use an existing Persistent Volume\n  volumeClaim:\n    label: Volume Claim\n    storageClass: Storage Class\n    requestStorage: Request Storage\n    persistentVolume: Persistent Volume\n  customize:\n    label: Customize\n    accessModes:\n      readWriteOnce: Single Node Read-Write\n      readOnlyMany: Many Nodes Read-Only\n      readWriteMany: Many Nodes Read-Write\n  status:\n    label: Status\nprefs:\n  title: Preferences\n  theme:\n    label: Theme\n    light: Light\n    auto: Auto\n    dark: Dark\n    autoDetail: Auto uses OS preference if available, or dark from {pm} to {am}\n  landing:\n    label: Login Landing Page\n    vue: Cluster Explorer\n    ember: Cluster Manager\n  formatting: Formatting\n  clusterToShow:\n    label: Number of clusters to show in side menu\n    value: |-\n      {count, number}\n  dateFormat:\n    label: Date Format\n  timeFormat:\n    label: Time Format\n  perPage:\n    label: Table Rows per Page\n    value: |-\n      {count, number}\n  keymap:\n    label: YAML Editor Key Mapping\n    sublime: 'Normal human'\n    emacs: 'Emacs'\n    vim: 'Vim'\n  advanced: Advanced\n  dev:\n    label: Enable Developer Tools & Features\n  hideDesc:\n    label: Hide All Type Description Boxes\n  helm:\n    'true': Include Prerelease Versions\n    'false': Show Releases Only\n    label: Helm Charts\n  experimental: Experimental\n  onlyFromVentura_x64: This setting requires macOS 13.0 (Ventura) or later.\n  onlyFromVentura_arm64: This setting requires macOS 13.3 (Ventura) or later.\n  onlyWithVZ_x64: This setting requires using the VZ emulation mode. VZ is only available on macOS 13.0 (Ventura) or later.\n  onlyWithVZ_arm64: This setting requires using the VZ emulation mode. VZ is only available on macOS 13.3 (Ventura) or later.\n\nprincipal:\n  loading: Loading&hellip;\n  error: Unable to fetch principal info\n  name: Name\n  loginName: Username\n  type: Type\n\nprobe:\n  checkInterval:\n    label: Check Interval\n    placeholder: 'Default: 10'\n  command:\n    label: Command to run\n    placeholder: e.g. cat /tmp/health\n  failureThreshold:\n    label: Failure Threshold\n    placeholder: 'Default: 3'\n  httpGet:\n    headers:\n      label: Request Headers\n    path:\n      label: Request Path\n      placeholder: e.g. /healthz\n    port:\n      label: Check Port\n      placeholder: e.g. 80\n      placeholderDuex: e.g. 25\n  initialDelay:\n    label: Initial Delay\n    placeholder: 'Default: 0'\n  successThreshold:\n    label: Success Threshold\n    placeholder: 'Default: 1'\n  timeout:\n    label: Timeout\n    placeholder: 'Default: 3'\n  type:\n    label: Type\n    placeholder: Select a check type\n\nproject:\n  containerDefaultResourceLimit: Container Default Resource Limit\n  resourceQuotas: Resource Quotas\n\nprojectNamespaces:\n  createNamespace: Create Namespace\n  createProject: Create Project\n  label: Projects/Namespaces\n  noNamespaces: There are no namespaces defined.\n\nprometheusRule:\n  alertingRules:\n    addLabel: Add Alert\n    annotations:\n      description:\n        input: Description Annotation Value\n        label: Description\n      label: Annotations\n      message:\n        input: Message Annotation Value\n        label: Message\n      runbook:\n        input: Runbook URL Annotation Value\n        label: Runbook URL\n      summary:\n        input: Summary Annotation Value\n        label: Summary\n    bannerText: 'When firing alerts, the annotations and labels will be passed to the configured AlertManagers to allow them to construct the notification that will be sent to any configured Receivers.'\n    for:\n      label: Wait to fire for\n      placeholder: '60'\n    label: Alerting Rules\n    labels:\n      label: Labels\n      severity:\n        choices:\n          critical: critical\n          label: Severity Label Value\n          none: none\n          warning: warning\n        label: Severity\n    name: Alert Name\n    removeAlert: Remove Alert\n  groups:\n    add: Add Rule Group\n    groupRowLabel: Rule Group {index}\n    groupInterval:\n      label: Override Group Interval\n      placeholder: '60'\n    label: Rule Groups\n    name: Group Name\n    none: Please add at least one rule group that contains at least one alerting or one recording rule.\n    removeGroup: Remove Group\n    responseStrategy:\n      label: Partial Response Strategy\n  promQL:\n    label: PromQL Expression\n  recordingRules:\n    addLabel: Add Record\n    label: Recording Rules\n    labels: Labels\n    name: Time Series Name\n    removeRecord: Remove Record\n\npromptRemove:\n  andOthers: |-\n    {count, plural,\n    =0 {.}\n    =1 { and <b>one other</b>.}\n    other { and <b>{count} others</b>.}\n    }\n  attemptingToRemove: \"You are attempting to delete the {type}\"\n  protip: \"Tip: Hold the {alternateLabel} key while clicking delete to bypass this confirmation\"\n  confirmName: \"Enter <b>{nameToMatch}</b> below to confirm:\"\n\npromptRestore:\n  title: Restore Snapshot\n\npromptSaveAsRKETemplate:\n  title: Create RKE Template from {cluster}\n  name: Cluster Template Name\n  description: Create a new RKE cluster template and initial revision from the current cluster configuration.\n  warning: This will modify the cluster, setting it up to use the newly created cluster template and revision.\n\nrancherAlertingDrivers:\n  msTeams: Enable Microsoft Teams\n  sms: Enable SMS\n  selectOne: You must select at least one of the options below.\n\nrbac:\n  roleBinding:\n    noData: There are no members associated with this resource.\n    user:\n      label: User\n    role:\n      label: Role\n    add: Add Member\n  displayRole:\n    fleetworkspace-admin: Admin\n    fleetworkspace-member: Member\n    fleetworkspace-readonly: Read-Only\n  members:\n    label: Members\n  roletemplate:\n    label: Roles\n    newUserDefault:\n      no: No\n      tooltip: This does not affect any bindings to the role that already exist.\n    locked:\n      label: Locked\n      yes: 'Yes: New bindings are not allowed to use this role'\n      no: No\n    tabs:\n      grantResources:\n        label: Grant Resources\n        tableHeaders:\n          verbs: Verbs\n          resources: Resource\n          nonResourceUrls: Non-Resource URLs\n          apiGroups: API Groups\n    subtypes:\n      GLOBAL:\n        createButton: Create Global Role\n        label: Global\n        yes: \"Yes: Default role for new users\"\n        defaultLabel: New User Default\n      CLUSTER:\n        createButton: Create Cluster Role\n        label: Cluster\n        yes: \"Yes: Default role for new cluster creation\"\n        defaultLabel: Cluster Creator Default\n      NAMESPACE:\n        createButton: Create Project/Namespaces Role\n        label: Project/Namespaces\n        yes: \"Yes: Default role for new project creation\"\n        defaultLabel: Project Creator Default\n      RBAC_ROLE:\n        label: Role\n      RBAC_CLUSTER_ROLE:\n        label: Cluster Role\n      noContext:\n        label: No Context\n  globalRoles:\n    types:\n      global:\n        label: Global Permissions\n        description: |-\n          Controls what access the {isUser, select,\n          true {user}\n          false {group}} has to administer the overall {appName} installation.\n      custom:\n        label: Custom\n        description: 'Roles not created by {vendor}.'\n      builtin:\n        label: Built-in\n        description: Additional roles to define more fine-grain permissions model.\n    unknownRole:\n        description: No description provided\n    assignOnlyRole: This role is already assigned\n    role:\n      admin:\n        label: Administrator\n        description: Administrators have full control over the entire installation and all resources in all clusters.\n      restricted-admin:\n        label: Restricted Administrator\n        description: Restricted Admins have full control over all resources in all downstream clusters but no access to the local cluster.\n      user:\n        label: Standard User\n        description: Standard Users can create new clusters and manage clusters and projects they have been granted access to.\n      user-base:\n        label: User-Base\n        description: User-Base users have login-access only.\n      clusters-create:\n        label: Create new Clusters\n        description: Allows the user to create new clusters and become the owner of them.  Standard Users have this permission by default.\n      clustertemplates-create:\n        label: Create new RKE Cluster Templates\n        description: Allows the user to create new RKE cluster templates and become the owner of them.\n      authn-manage:\n        label: Configure Authentication\n        description: Allows the user to enable, configure, and disable all Authentication provider settings.\n      catalogs-manage:\n        label: Configure Catalogs\n        description: Allows the user to add, edit, and remove Catalogs.\n      clusters-manage:\n        label: Manage all Clusters\n        description: Allows the user to manage all clusters, including ones they are not a member of.\n      clusterscans-manage:\n        label: Manage CIS Cluster Scans\n        description: Allows the user to launch new and manage CIS cluster scans.\n      kontainerdrivers-manage:\n        label: Create new Cluster Drivers\n        description: Allows the user to create new cluster drivers and become the owner of them.\n      features-manage:\n        label: Configure Feature Flags\n        description: Allows the user to enable and disable custom features via feature flag settings.\n      nodedrivers-manage:\n        label: Configure Node Drivers\n        description: Allows the user to enable, configure, and remove all Node Driver settings.\n      nodetemplates-manage:\n        label: Manage Node Templates\n        description: Allows the user to define, edit, and remove Node Templates.\n      podsecuritypolicytemplates-manage:\n        label: Manage Pod Security Policies (PSPs)\n        description: Allows the user to define, edit, and remove PSPs.\n      roles-manage:\n        label: Manage Roles\n        description: Allows the user to define, edit, and remove Role definitions.\n      settings-manage:\n        label: Manage Settings\n        description: 'Allows the user to manage {vendor} Settings.'\n      users-manage:\n        label: Manage Users\n        description: Allows the user to create, remove, and set passwords for all Users.\n      catalogs-use:\n        label: Use Catalogs\n        description: Allows the user to see and deploy Templates from the Catalog.  Standard Users have this permission by default.\n      nodetemplates-use:\n        label: Use Node Templates\n        description: Allows the user to deploy new Nodes using any existing Node Templates.\n      view-rancher-metrics:\n        label: 'View {vendor} Metrics'\n        description: Allows the user to view Metrics through the API.\n      base:\n        label: Login Access\n\nresourceDetail:\n  detailTop:\n    annotations: Annotations\n    created: Created\n    deleted: Deleted\n    description: Description\n    labels: Labels\n    ownerReferences: |-\n      {count, plural,\n      =1 {Owner}\n      other {Owners}}\n    hideAnnotations: |-\n      {annotations, plural,\n      =1 {Hide 1 annotation}\n      other {Hide {annotations} annotations}}\n    showAnnotations: |-\n      {annotations, plural,\n      =1 {Show 1 annotation}\n      other {Show {annotations} annotations}}\n    name: Name\n  header:\n    clone: \"Clone from {subtype} {name}\"\n    create: Create {subtype}\n    import: Import {subtype}\n    edit: \"{subtype} {name}\"\n    stage: \"Stage from {subtype} {name}\"\n    view: \"{subtype} {name}\"\n  masthead:\n    age: Age\n    defaultBannerMessage:\n      error: This resource is currently in an error state, but there isn't a detailed message available.\n      transitioning: This resource is currently in a transitioning state, but there isn't a detailed message available.\n    sensitive:\n      hide: Hide Sensitive Values\n      show: Show Sensitive Values\n    namespace: Namespace\n    workspace: Workspace\n    project: Project\n    detail: Detail\n    config: Config\n    yaml: YAML\n    managedWarning: |-\n      This {type} is managed by {hasName, select,\n        no {a {managedBy} app}\n        yes {the {managedBy} app {appName}}}; changes made here will likely be overwritten the next time the app is changed.\nresourceList:\n  head:\n    create: Create\n    createFromYaml: Create from YAML\n    createResource: \"Create {resourceName}\"\n\nresourceTable:\n  groupBy:\n    none: Flat List\n    namespace: Group by Namespace\n    project: Group by Project\n  groupLabel:\n    cluster: \"<span>Cluster:</span> {name}\"\n    namespace: \"<span>Namespace:</span> {name}\"\n    machinePool: \"<span>Machine Pool:</span> {name}\"\n    notInANamespace: Not Namespaced\n    notInAProject: Not in a Project\n    project: \"<span>Project:</span> {name}\"\n    notInAWorkspace: Not in a Workspace\n    workspace: \"<span>Workspace:</span> {name}\"\n\nresourceTabs:\n  conditions:\n    tab: Conditions\n  events:\n    tab: Recent Events\n  related:\n    tab: Related Resources\n    from: Referred To By\n    to: Refers To\n\n\nresourceYaml:\n  errors:\n    namespaceRequired: This resource is namespaced, so a namespace must be provided.\n  buttons:\n    continue: Continue Editing\n    edit: Edit YAML\n    diff: Show Diff\n    unified: Unified\n    split: Split\n\nrioConfig:\n  configure:\n    description: Description\n    helpText:\n      listItem1: The application deployment engine for Kubernetes.\n      listItem2: \"Rio makes it faster and easier for DevOps to build, test, deploy, scale and version stateless applications\"\n    requirements:\n      header: Requirements\n      helpText:\n        listItem1: 1 CPU Core\n        listItem2: 2 GiB of Memory\n  header: Rio\n  yaml:\n    buttonText: Customize\n\nsecret:\n  authentication: Authentication\n  certificate:\n    certificate: Certificate\n    cn: Domain Name\n    expires: Expires\n    issuer: Issuer\n    plusMore: \"+ {n} more\"\n    privateKey: Private Key\n  data: Data\n  registry:\n    address: Registry\n    domainName: Registry Domain Name\n    password: Password\n    username: Username\n  basic:\n    password: Password\n    username: Username\n  ssh:\n    keys: Keys\n    public: Public Key\n    private: Private Key\n  serviceAcct:\n    ca: CA Certificate\n    token: Token\n  type: Type\n  types:\n    'opaque': 'Opaque'\n    'kubernetes.io/service-account-token': 'Svc Acct Token'\n    'kubernetes.io/dockercfg': 'Registry'\n    'kubernetes.io/dockerconfigjson': 'Registry'\n    'kubernetes.io/basic-auth': 'HTTP Basic Auth'\n    'kubernetes.io/ssh-auth': 'SSH Key'\n    'kubernetes.io/tls': 'TLS Certificate'\n    'bootstrap.kubernetes.io/token': 'Bootstrap Token'\n    'istio.io/key-and-cert': 'Istio Certificate'\n    'helm.sh/release.v1': 'Helm Release'\n    'fleet.cattle.io/cluster-registration-values': 'Fleet Cluster'\n    'provisioning.cattle.io/cloud-credential': 'Cloud Credential'\n  initials:\n    'opaque': 'O'\n    'kubernetes.io/service-account-token': 'SAT'\n    'kubernetes.io/dockercfg': 'R'\n    'kubernetes.io/dockerconfigjson': 'R'\n    'kubernetes.io/basic-auth': 'HTTP'\n    'kubernetes.io/ssh-auth': 'SSH'\n    'kubernetes.io/tls': 'TLS'\n    'bootstrap.kubernetes.io/token': 'Boot'\n    'istio.io/key-and-cert': 'Ist'\n    'helm.sh/release.v1': 'Helm'\n    'fleet.cattle.io/cluster-registration-values': 'F'\n    'provisioning.cattle.io/cloud-credential': 'CC'\n  relatedWorkloads: Related Workloads\n\nselectOrCreateAuthSecret:\n  label: Authentication\n  options:\n    none: None\n    basic: HTTP Basic Auth\n    ssh: SSH Key\n    aws: AWS/S3\n    custom: Secret Name\n  aws:\n    accessKey: Access Key\n    secretKey: Secret Key\n  ssh:\n    publicKey: Public Key\n    privateKey: Private Key\n  basic:\n    username: Username\n    password: Password\n  namespaceGroup: \"Namespace: {name}\"\n  chooseExisting: \"Choose an existing secret:\"\n  createSsh: Create a SSH Key Secret\n  createBasic: Create a HTTP Basic Auth Secret\n  createAws: Create an AWS/S3 Auth Secret\n\nservicePorts:\n  header:\n    label: Port Rules\n  rules:\n    listening:\n      label: Listening Port\n      placeholder: e.g. 8080\n    name:\n      label: Port Name\n      placeholder: e.g. myport\n    node:\n      label: Node Port\n      placeholder: e.g. 30000\n    protocol:\n      label: Protocol\n    target:\n      label: Target Port\n      placeholder: e.g. 80 or http\n\nserviceTypes:\n  clusterip: Cluster IP\n  externalname: External Name\n  headless: Headless\n  loadbalancer: Load Balancer\n  nodeport: Node Port\n\nservicesPage:\n  anyNode: Any Node\n  labelsAnnotations:\n    label: Labels & Annotations\n  affinity:\n    actionLabels:\n      clientIp: ClientIP\n      none: There is no session affinity configured.\n    helpText: Map connections to a consistent target based on their source IP.\n    label: Session Affinity\n    timeout:\n      label: Session Sticky Time\n      placeholder: e.g. 10800\n  externalName:\n    define: External Name\n    helpText: \"External Name is intended to specify a canonical DNS name. This is a required field. To hardcode an IP address, use a Headless service.\"\n    label: External Name\n    placeholder: e.g. my.database.example.com\n    input:\n      label: DNS Name\n  ips:\n    define: Service Ports\n    clusterIpHelpText: The Cluster IP address must be within the CIDR range configured for the API server.\n    external:\n      label: External IPs\n      placeholder: e.g. 1.1.1.1\n      protip: List of IP addresses for which nodes in the cluster will also accept traffic for this service.\n    input:\n      label: Cluster IP\n      placeholder: e.g. 10.43.xxx.xxx\n    label: IP Addresses\n  pods:\n    label: Pods\n  ports:\n    label: Ports\n  selectors:\n    helpText: \"\"\n    label: Selectors\n    matchingPods:\n      matchesSome: |-\n        {matched, plural,\n          =0 {Matches 0 of {total, number} pods. If no selector is created, manual endpoints must be made.}\n          =1 {Matches 1 of {total, number} pods: \"{sample}\"}\n          other {Matches {matched, number} of {total, number} existing pods, including \"{sample}\"}\n        }\n  serviceTypes:\n    clusterIp:\n      abbrv: IP\n      description: Exposes the service on a cluster-internal IP. Choosing this value makes the service only reachable from within the cluster. This is the default type.\n      label: Cluster IP\n    externalName:\n      abbrv: EN\n      description: \"Maps the service to the contents of the `externalName` field (e.g. foo.bar.example.com), by returning a CNAME record with its value. No proxying of any kind is set up.\"\n      label: External Name\n    headless:\n      abbrv: H\n      description: Neither a cluster IP or load balancer is defined. These are used to interface with other service discovery mechanisms outside of Kubernetes implementation. A cluster IP is not allocated and kube-proxy does not handle these services.\n      label: Headless\n    loadBalancer:\n      abbrv: LB\n      description: Exposes the service externally using a cloud provider's load balancer.\n      label: Load Balancer\n    nodePort:\n      abbrv: NP\n      description: \"Exposes the service on each node's IP at a static port (the `NodePort`). You'll be able to contact this type of service, from outside the cluster, by requesting `<NodeIP>:<NodePort>`.\"\n      label: Node Port\n  typeOpts:\n    label: Service Type\n\nsetup:\n  welcome: Welcome to {vendor}!\n  setPassword: The first order of business is to set a strong password for the default <code>admin</code> user. We suggest using this random one generated just for you, but enter your own if you like.\n  newPassword: New Password\n  confirmPassword: Confirm New Password\n  useRandom: Use a randomly generated password\n  useManual: Set a specific password to use\n  defaultPasswordError: It looks like this is your first time visiting the Rancher UI, but the local admin account password is already set to something unique. Log in with that account below to continue the setup process.\n  telemetry:\n    label: Allow collection of anonymous statistics to help us improve Rancher\n    tip:  'Rancher Labs would like to collect a bit of anonymized information\n          about the configuration of your installation to help make Rio better.\n          Your data will not be shared with anyone else, and no information about\n          what specific resources or endpoints you are deploying is included.\n          Once enabled you can view exactly what data will be sent at <code>/v1-telemetry</code>.\n          <a href=\"https://rancher.com/docs/rancher/v2.x/en/faq/telemetry/\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">More Info</a>'\n  eula: I agree to the <a href=\"https://rancher.com/eula\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">terms and conditions</a> for using Rancher.\n  serverUrl:\n    label: Server URL\n    tip:  What URL should be used for this Rancher installation? All the nodes in your clusters will need to be able to reach this. You can skip setting this for now, and update it later in General Settings>Advanced Settings.\n    skip: Skip\n\nsortableTable:\n  bulkActions:\n    collapsed:\n      label: Actions\n  actionAvailability:\n    selected: \"{actionable} selected\"\n    some: \"Available for {actionable} of the {total} selected\"\n  noData: There are no rows which match your search query.\n  noRows: There are no rows to show.\n  noActions: No actions available\n  paging:\n    generic: |-\n      {pages, plural,\n      =0 {No Items}\n      =1 {{count} {count, plural, =1 {Item} other {Items}}}\n      other {{from} - {to} of {count} Items}}\n    resource: |-\n      {pages, plural,\n      =0 {No {pluralLabel}}\n      =1 {{count} {count, plural, =1 {{singularLabel}} other {{pluralLabel}}}}\n      other {{from} - {to} of {count} {pluralLabel}}}\n  search: Filter\n  in: in\n  addFilter: Add Filter\n  filterFor: Filter for...\n  selectCol: Select a column\n  resetFilters: Reset\n  add: Add\n  tableHeader:\n    noFilter: This column cannot be filtered by\n    groupBy: Group by\n    show: Show\n\nstorageClass:\n  actions:\n    setAsDefault: Set as Default\n    resetDefault: Reset Default\n  parameters:\n    label: Parameters\n  customize:\n    label: Customize\n    reclaimPolicy:\n      label: Reclaim Policy\n      delete: Delete volumes and underlying device when volume claim is deleted\n      retain: Retain the volume for manual cleanup\n    allowVolumeExpansion:\n      label: Allow Volume Expansion\n      enabled: Enabled\n      disabled: Disabled\n    volumeBindingMode:\n      label: Volume Binding Mode\n      now: Bind and provision a persistent volume once the PersistentVolumeClaim is created\n      later: Bind and provision a persistent volume once a Pod using the PersistentVolumeClaim is created\n    mountOptions:\n      label: Mount Options\n      addlabel: Add Option\n  aws-ebs:\n    title: Amazon EBS Disk\n    volumeType:\n      label: Volume Type\n      gp2: GP2 - General Purpose SSD\n      io1: IO1 - Provisioned IOPS SSD\n      st1: ST1 - Throughput-Optimized HDD\n      sc1: SC1 - Cold-Storage HDD\n      provisionedIops:\n        label: Provisioned IOPS\n        suffix: per second, per GB\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    availabilityZone:\n      label: Availability Zone\n      automatic: 'Automatic: Zones the cluster has a node in'\n      manual: 'Manual: Choose specific zones'\n      placeholder: us-east-1d, us-east-1c\n    encryption:\n      label: Encryption\n      enabled: Enabled\n      disabled: Disabled\n    keyId:\n      label: KMS Key ID for Encryption\n      automatic: 'Automatic: Generate a key'\n      manual: 'Manual: Use a specific key (full ARN)'\n  azure-disk:\n    title: Azure Disk\n    storageAccountType:\n      label: Storage Account Type\n      placeholder: e.g. Standard_LRS\n    kind:\n      label: Kind\n      shared: Shared (unmanaged disk)\n      dedicated: Dedicated (unmanaged disk)\n      managed: Managed\n  azure-file:\n    title: Azure File\n    skuName:\n      label: Sku Name\n      placeholder: e.g. Standard_LRS\n    location:\n      label: Location\n      placeholder: e.g. eastus\n    storageAccount:\n      label: Storage Account\n      placeholder: e.g. azure_storage_account_name\n  gce-pd:\n    title: Google Persistent Disk\n    volumeType:\n      label: Volume Type\n      standard: Standard\n      ssd: SSD\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    availabilityZone:\n      label: Availability Zone\n      automatic: 'Automatic: Zones the cluster has a node in'\n      manual: 'Manual: Choose specific zones'\n      placeholder: us-east-1d, us-east-1c\n    replicationType:\n      label: Replication Type\n      zonal: Zonal\n      regional: Regional\n  longhorn:\n    title: Longhorn\n    addLabel: Add Parameter\n  vsphere-volume:\n    title: VMWare vSphere Volume\n    diskFormat:\n      label: Disk Format\n      thin: Thin\n      zeroedthick: Zeroed Thick\n      eagerzeroedthick: Eager Zeroed Thick\n    storagePolicyName:\n      label: Storage Policy Name\n      placeholder: e.g. gold\n    datastore:\n      label: Datastore\n      placeholder: e.g. VSANDatastore\n    hostFailuresToTolerate:\n      label: Host Failures To Tolerate\n      placeholder: e.g. 2\n    cacheReservation:\n      label: Cache Reservation\n      placeholder: e.g. 20\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext3\n  custom:\n    addLabel: Add Parameter\n  glusterfs:\n    title: Gluster Volume (Unsupported)\n    restUrl:\n      label: REST URL\n      placeholder: e.g. http://127.0.0.1:8081\n    restUser:\n      label: REST User\n      placeholder: e.g. admin\n    restUserKey:\n      label: REST User Key\n      placeholder: e.g. password\n    secretNamespace:\n      label: Secret Namespace\n      placeholder: e.g. default\n    secretName:\n      label: Secret Name\n      placeholder: e.g. heketi-secret\n    clusterId:\n      label: Cluster ID\n      placeholder: e.g. 630372ccdc720a92c681fb928f27b53f\n    gidMin:\n      label: GID MIN\n      placeholder: e.g. 40000\n    gidMax:\n      label: GID MAX\n      placeholder: e.g. 50000\n    volumeType:\n      label: Volume Type\n      placeholder: \"e.g. replicate:3\"\n  cinder:\n    title: Openstack Cinder Volume (Unsupported)\n    volumeType:\n      label: Volume Type\n      placeholder: e.g. fast\n    availabilityZone:\n      label: Availability Zone\n      automatic: \"Automatic: Zones the cluster has a node in\"\n      manual:\n        label: \"Manual: Choose specific zones\"\n        placeholder: e.g. nova\n  rbd:\n    title: Ceph RBD (Unsupported)\n    monitors:\n      label: Monitors\n      placeholder: e.g. 10.16.153.105:6789\n    adminId:\n      label: Admin ID\n      placeholder: e.g. kube\n    adminSecretNamespace:\n      label: Admin Secret Namespace\n      placeholder: e.g. kube-system\n    adminSecret:\n      label: Admin Secret\n      placeholder: e.g. Secret\n    pool:\n      label: Pool\n      placeholder: e.g. kube\n    userId:\n      label: User ID\n      placeholder: e.g. kube\n    userSecretNamespace:\n      label: User Secret Namespace\n      placeholder: e.g. default\n    userSecretName:\n      label: User Secret Name\n      placeholder: e.g. ceph-secret-user\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    imageFormat:\n      label: Image Format\n      placeholder: e.g. 2\n    imageFeatures:\n      label: Image Features\n      placeholder: e.g. layering\n  quobyte:\n    title: Quobyte Volume (Unsupported)\n    quobyteApiServer:\n      label: Quobyte API Server\n      placeholder: \"e.g. http://138.68.74.142:7860\"\n    registry:\n      label: Registry\n      placeholder: e.g. 138.68.74.142:7861\n    adminSecretNamespace:\n      label: Admin Secret Namespace\n      placeholder: e.g. kube-system\n    adminSecretName:\n      label: Admin Secret Name\n      placeholder: e.g. quobyte-admin-secret\n    user:\n      label: User\n      placeholder: e.g. root\n    group:\n      label: Group\n      placeholder: e.g. root\n    quobyteConfig:\n      label: Quobyte Config\n      placeholder: e.g. BASE\n    quobyteTenant:\n      label: Quobyte Tenant\n      placeholder: e.g. DEFAULT\n  portworx-volume:\n    title: Portworx Volume (Unsupported)\n    filesystem:\n      label: Filesystem\n      placeholder: e.g. ext4\n    blockSize:\n      label: Block Size\n      placeholder: e.g. 32\n    repl:\n      label: Repl\n      placeholder: e.g.1; 0 for entire device\n    ioPriority:\n      label: I/O Priority\n      placeholder: e.g. low\n    snapshotsInterval:\n      label: Snapshots Interval\n      placeholder: e.g. 70\n    aggregationLevel:\n      label: Aggregation Level\n      placeholder: e.g. 0\n    ephemeral:\n      label: Ephemeral\n      placeholder: e.g. true\n  scaleio:\n    title: ScaleIO Volume (Unsupported)\n    gateway:\n      label: Gateway\n      placeholder: e.g. https://192.168.99.200:443/api\n    system:\n      label: System\n      placeholder: e.g. scaleio\n    protectionDomain:\n      label: Protection Domain\n      placeholder: e.g. pd0\n    storagePool:\n      label: Storage Pool\n      placeholder: e.g. sp1\n    storageMode:\n      label: StorageMode\n      thin: Thin Provisioned\n      thick: Thick Provisioned\n    secretRef:\n      label: Secret Ref\n      placeholder: e.g. sio-secret\n    readOnly:\n      label: Read Only\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. xfs\n  storageos:\n    title: StorageOS (Unsupported)\n    pool:\n      label: Pool\n      placeholder: e.g. default\n    description:\n      label: Description\n      placeholder: e.g. Kubernetes volume\n    filesystemType:\n      label: Filesystem Type\n      placeholder: e.g. ext4\n    adminSecretNamespace:\n      label: Admin Secret Namespace\n      placeholder: e.g. default\n    adminSecretName:\n      label: Admin Secret Name\n      placeholder: e.g. storageos-secret\n  no-provisioner:\n    title: Local Storage (Unsupported)\n\ntableHeaders:\n  accessKey: Access Key\n  address: Address\n  age: Age\n  apiGroup: API Groups\n  authRoles:\n    globalDefault: New User Default\n    clusterDefault: Cluster Creator Default\n    projectDefault: Project Creator Default\n  branch: Branch\n  builtIn: Built In\n  bundlesReady: Bundles\n  bundleDeploymentsReady: Deployments\n  builtin: Built-In\n  chart: Chart\n  clusterCreatorDefault: Cluster Creator Default\n  clusterFlow: Cluster Flow\n  clusterOutput: Cluster Output\n  clusters: Clusters\n  clustersReady: Clusters Ready\n  clusterGroups: Cluster Groups\n  commit: Commit\n  condition: Condition\n  customVerbs: Custom Verbs\n  description: Description\n  expires: Expires\n  providers: Providers\n  cpu: CPU\n  date: Date\n  default: Default\n  destination: Target\n  download: Download\n  effect: Effect\n  endpoints: Endpoints\n  flow: Flow\n  gitRepos: Git Repos\n  host: |-\n    {count, plural,\n      one { Host }\n      other { Hosts }\n    }\n  image: Image\n  imageSize: Size\n  ingressDefaultBackend: Default\n  ingressTarget: Target\n  internalExternalIp: External/Internal IP\n  jobs: Jobs\n  key: Key\n  keys: Data\n  lastUpdated: Last Updated\n  lastSeen: Last Seen\n  loggingOutputProviders: Provider\n  machines: Machines\n  manual: Manual\n  matches: Matches\n  maxKubernetesVersion: Max Kubernetes Version\n  message: Message\n  minKubernetesVersion: Min Kubernetes Version\n  memory: Memory\n  name: Name\n  nameDisplay: Display Name\n  nameUnlinked: Name\n  namespace: Namespace\n  namespaceName: Name\n  namespaceNameUnlinked: Name\n  node: Node\n  nodeName: Name\n  nodesReady: Nodes Ready\n  nodePort: Node Port\n  object: Object\n  output: Output\n  p95: 95%tile\n  persistentVolumeSource: Source\n  podImages: Image\n  pods: Pods\n  port: Port\n  protocol: Protocol\n  provider: Provider\n  publicPorts: Public Ports\n  ram: RAM\n  rbac:\n    create: Create\n    delete: Delete\n    get: Get\n    list: List\n    patch: Patch\n    update: Update\n    watch: Watch\n  ready: Ready\n  reason: Reason\n  repo: Repo\n  reposReady: Repos Ready\n  replicas: Replicas\n  reqRate: Req Rate\n  resource: Resource\n  resources: Resources\n  restarts: Restarts\n  rioImage: Image\n  role: Role\n  roles: Roles\n  scale: Scale\n  scope: Scope\n  selector: Selector\n  simpleName: Name\n  simpleScale: Scale\n  simpleType: Type\n  size: Size\n  started: Started\n  state: State\n  status: Status\n  storage_class_provisioner: Provisioner\n  subject: Subject\n  subType: Kind\n  success: Success\n  summary: Summary\n  target: Target\n  targetKind: Target Type\n  targetPort: Target\n  type: Type\n  updated: Updated\n  upgrade: Upgradable\n  url: URL\n  userDisplayName: Display Name\n  userId: ID\n  userStatus: Status\n  username: Local Username\n  value: Value\n  version: Version\n  weight: Weight\n\ntarget:\n  router:\n    label: Router\n    placeholder: Select a router\n  service:\n    label: Service\n    placeholder: Select a service\n  title: Target\n  version:\n    label: Version\n    placeholder: Select a version\n\nuser:\n  detail:\n    username: Username\n    globalPermissions:\n      label: Global Permissions\n      description: Access to manage resources that affect the entire installation\n      adminMessage: This user is an administrator and has all permissions\n      tableHeaders:\n        permission: Permission\n    clusterRoles:\n      label: Cluster Roles\n      description: Roles granted to this user for individual clusters\n      tableHeaders:\n        cluster: Cluster\n    projectRoles:\n      label: Project Roles\n      description: Roles granted to this user for individual projects\n      tableHeaders:\n        project: Project\n    generic:\n      tableHeaders:\n        role: Role\n        granted: Granted\n  edit:\n    credentials:\n      label: Credentials\n      username:\n        label: Username\n        placeholder: e.g. jsmith\n        exists: 'Username is already in use. Please choose a new username'\n      displayName:\n        label: Display Name\n        placeholder: e.g. John Smith\n      userDescription:\n        label: Description\n        placeholder: e.g. This account is for John Smith\n  list:\n    errorRefreshingGroupMemberships: Error refreshing group memberships\nvalidation:\n  arrayLength:\n    between: '\"{key}\" should contain between {min} and {max} {max, plural, =1 {item} other {items}}'\n    exactly: '\"{key}\" should contain {count, plural, =1 {# item} other {# items}}'\n    max: '\"{key}\" should contain at most {count} {count, plural, =1 {item} other {items}}'\n    min: '\"{key}\" should contain at least {count} {count, plural, =1 {item} other {items}}'\n  boolean: '\"{key}\" must be a boolean value.'\n  chars: '\"{key}\" contains {count, plural, =1 {an invalid character} other {# invalid characters}}: {chars}'\n  custom:\n    missing: 'No validator exists for { validatorName }! Does the validator exist in custom-validators? Is the name spelled correctly?'\n  dns:\n    doubleHyphen: '\"{key}\" Cannot contain two or more consecutive hyphens'\n    hostname:\n      empty: '\"{key}\" must be at least one character'\n      emptyLabel: '\"{key}\" cannot contain two consecutive dots'\n      endDot: '\"{key}\" cannot end with a dot'\n      endHyphen: '\"{key}\" cannot end with a hyphen'\n      startDot: '\"{key}\" cannot start with a dot'\n      startHyphen: '\"{key}\" cannot start with a hyphen'\n      startNumber: '\"{key}\" cannot start with a number'\n      tooLong: '\"{key}\" cannot be longer than {max} characters'\n      tooLongLabel: '\"{key}\" cannot contain a section longer than {max} characters'\n    label:\n      emptyLabel: '\"{key}\" cannot be empty'\n      endHyphen: '\"{key}\" cannot end with a hyphen'\n      startHyphen: '\"{key}\" cannot start with a hyphen'\n      startNumber: '\"{key}\" cannot start with a number'\n      tooLongLabel: '\"{key}\" cannot be more than {max} characters'\n  flowOutput:\n    both: Requires \"Output\" or \"Cluster Output\" to be selected.\n    global: Requires \"Cluster Output\" to be selected.\n  output:\n    logdna:\n      apiKey: Required an \"Api Key\" to be set.\n  invalidCron: Invalid cron schedule\n  k8s:\n    identifier:\n      emptyLabel: '\"{key}\" cannot have an empty key'\n      emptyPrefix: '\"{key}\" cannot have an empty prefix'\n      endLetter: '\"{key}\" must end with a letter or number'\n      startLetter: '\"{key}\" must start with a letter or number'\n      tooLongKey: '\"{key}\" cannot have a key longer than {max} characters'\n      tooLongPrefix: '\"{key}\" cannot have a prefix longer than {max} characters'\n  noSchema: No schema found to validate\n  noType: No type to validate\n  number:\n    between: '\"{key}\" should be between {min} and {max}'\n    exactly: '\"{key}\" should be exactly {val}'\n    max: '\"{key}\" should be at most {val}'\n    min: '\"{key}\" should be at least {val}'\n  podAffinity:\n    affinityTitle: Pod Affinity\n    antiAffinityTitle: Pod Anti-Affinity\n    requiredDuringSchedulingIgnoredDuringExecution: required rules\n    preferredDuringSchedulingIgnoredDuringExecution: preferred rules\n    topologyKey: Rule [{index}] of {group} {rules} - Topology key is required.\n    matchExpressions:\n      operator: Rule [{index}] of {group} {rules} - operator must be one of 'In', 'NotIn', 'Exists', 'DoesNotExist'\n      valueMustBeEmpty: Rule [{index}] of {group} {rules} - value must be empty if operator is 'Exists' or 'DoesNotExist'\n      valuesMustBeDefined: Rule [{index}] of {group} {rules} - value must be defined if operator is 'In' or 'NotIn'\n  port: A port must be a number between 1 and 65535.\n  prometheusRule:\n    groups:\n      required: At least one rule group is required.\n      singleAlert: A rule may contain alert rules or recording rules but not both.\n      valid:\n        name: 'Name is required for rule group {index}.'\n        rule:\n          alertName: 'Rule group {groupIndex} rule {ruleIndex} requires a Alert Name.'\n          expr: 'Rule group {groupIndex} rule {ruleIndex} requires a PromQL Expression.'\n          labels: 'Rule group {groupIndex} rule {ruleIndex} requires at least one label. Severity is recommended.'\n          recordName: 'Rule group {groupIndex} rule {ruleIndex} requires a Time Series Name.'\n        singleEntry: 'At least one alert rule or one recording rule is required in rule group {index}.'\n  required: '\"{key}\" is required'\n  requiredOrOverride: '\"{key}\" is required or must allow override'\n  roleTemplate:\n    roleTemplateRules:\n      missingVerb: You must specify at least one verb for each resource grant\n      missingResource: You must specify a Resource for each resource grant\n      missingApiGroup: You must specify an API Group for each resource grant\n      missingOneResource: You must specify at least one Resource, Non-Resource URL or API Group for each resource grant\n  service:\n    externalName:\n      none: External Name is required on an ExternalName Service.\n    ports:\n      name:\n        required: 'Port Rule [{position}] - Name is required.'\n      nodePort:\n        requriedInt: 'Port Rule [{position}] - Node Port must be integer values if included.'\n      port:\n        required: 'Port Rule [{position}] - Port is required.'\n        requriedInt: 'Port Rule [{position}] - Port must be integer values if included.'\n      targetPort:\n        between: 'Port Rule [{position}] - Target Port must be between 1 and 65535'\n        iana: 'Port Rule [{position}] - Target Port must be an IANA Service Name or Integer'\n        ianaAt: 'Port Rule [{position}] - Target Port '\n        required: 'Port Rule [{position}] - Target Port is required'\n  stringLength:\n    between: '\"{key}\" should be between {min} and {max} {max, plural, =1 {character} other {characters}}'\n    exactly: '\"{key}\" should be {count, plural, =1 {# character} other {# characters}}'\n    max: '\"{key}\" should be at most {count} {count, plural, =1 {character} other {characters}}'\n    min: '\"{key}\" should be at least {count} {count, plural, =1 {character} other {characters}}'\n  targets:\n    missingProjectId: A target must have a project selected.\n  monitoring:\n    route:\n      match: At least one Match or Match Regex must be selected\n      interval: '\"{key}\" must be of a format with digits followed by a unit i.e. 1h, 2m, 30s'\n\nwizard:\n  previous: Previous\n  finish: Finish\n  next: Next\n  step: \"Step {number}\"\n\nwm:\n  connection:\n    connected: Connected\n    connecting: Connecting&hellip;\n    disconnected: Disconnected\n    error: Error\n  containerLogs:\n    clear: Clear\n    containerName: \"Container: {label}\"\n    download: Download\n    follow: Follow\n    noData: There are no log entries to show in the current range.\n    noMatch: No lines match the current filter.\n    previous: Use Previous Container\n    range:\n      all: Everything\n      hours: |-\n        {value, number}\n        {value, plural,\n        =1 {Hour}\n        other {Hours}\n        }\n      label: Show the last\n      lines: \"{value, number} Lines\"\n      minutes: |-\n        {value, number} {value, plural,\n        =1 {Minute}\n        other {Minutes}\n        }\n    search: Filter\n    timestamps: Show Timestamps\n    wrap: Wrap Lines\n  containerShell:\n    clear: Clear\n    containerName: \"Container: {label}\"\n  kubectlShell:\n    title: \"Kubectl: {name}\"\n\nworkload:\n  container:\n    command:\n      addEnvVar: Add Variable\n      args: Arguments\n      as: as\n      command: Command\n      env: Environment Variables\n      fromResource:\n        key:\n          label: Key\n          placeholder: \"e.g. metadata.labels['<KEY>']\"\n        name:\n          label: Variable Name\n          placeholder: \"e.g. FOO\"\n        prefix: Prefix\n        source:\n          label: Source\n          placeholder: e.g. my-container\n        secret: Secret\n        configMap: ConfigMap\n        containerName: Container Name\n        type: Type\n        value:\n          label: Value\n          placeholder: e.g. bar\n      tty: TTY\n      workingDir: WorkingDir\n      stdin: Stdin\n    containerName: Container Name\n    healthCheck:\n      checkInterval: Check Interval\n      command:\n        command: Command to run\n      failureThreshold: Failure Threshold\n      httpGet:\n        headers: Request Headers\n        path: Request Path\n        port: Check Port\n      initialDelay: Initial Delay\n      livenessProbe: Liveness Check\n      livenessTip: Containers will be restarted when this check is failing.  Not recommended for most uses.\n      noHealthCheck: \"There is not a Readiness Check, Liveness Check or Startup Check configured.\"\n      readinessProbe: Readiness Check\n      readinessTip: Containers will be removed from service endpoints when this check is failing.  Recommended.\n      startupProbe: Startup Check\n      startupTip: Containers will wait until this check succeeds before attempting other health checks.\n      successThreshold: Success Threshold\n      timeout: Timeout\n      kind:\n        none:  None\n        HTTP:  HTTP request returns a successful status (200-399)\n        HTTPS: HTTPS request returns a successful status\n        tcp:   TCP connection opens successfully\n        exec:  Command run inside the container exits with status 0\n    image: Container Image\n    imagePullPolicy: Pull Policy\n    imagePullSecrets: Pull Secrets\n    init: Init Container\n    name: Container Name\n    noResourceLimits: There are no resource requirements configured.\n    noPorts: There are no ports configured.\n    ports:\n      createService: Service Type\n      noCreateService: Do not create a service\n      containerPort: Private Container Port\n      hostIP: Host IP\n      hostPort: Public Host Port\n      name: Name\n      protocol: Protocol\n      listeningPort: Listening Port\n    removeContainer: Remove Container\n    security:\n      addCapabilities: Add Capabilities\n      addGroupIDs: Add Group IDs\n      allowPrivilegeEscalation:\n        label: Privilege Escalation\n        'false': No\n        'true': \"Yes: container can gain more privileges than its parent process\"\n      dropCapabilities: Drop Capabilities\n      fsGroup: Filesystem Group\n      hostIPC: Use Host IPC Namespace\n      hostPID: Use Host PID Namespace\n      privileged:\n        label: Privileged\n        'false': No\n        'true': \"Yes: container has full access to the host\"\n      readOnlyRootFilesystem:\n        label: Read-Only Root Filesystem\n        'false': No\n        'true': \"Yes: container has a read-only root filesystem\"\n      runAsGroup: Run as Group ID\n      runAsNonRoot:\n        label: Run as Non-Root\n        'false': No\n        'true': \"Yes: container must run as a non-root user\"\n      runAsNonRootOptions:\n        noOption: \"No\"\n        yesOption: \"Yes: containers must run as non-root-user\"\n      runAsUser: Run as User ID\n      shareProcessNamespace: Share single process namespace\n      supplementalGroups: Additional Group IDs\n      sysctls: Sysctls\n      sysctlsKey: Name\n    standard: Standard Container\n    titles:\n      container: Container\n      command: Command\n      containers: Containers\n      env: Environment Variables\n      events: Events\n      general: General\n      healthCheck: Health Check\n      image: Image\n      networking: Networking\n      networkSettings: Network Settings\n      podAnnotations: Pod Annotations\n      podLabels: Pod Labels\n      metrics: Metrics\n      podScheduling: Pod Scheduling\n      nodeScheduling: Node Scheduling\n      ports: Ports\n      resources: Resources\n      securityContext: Security Context\n      status: Status\n      volumeClaimTemplates: Volume Claim Templates\n      upgrading: Scaling and Upgrade Policy\n  cronSchedule: Schedule\n  detail:\n    pods:\n      title: Pods\n  detailTop:\n    node: Node\n    podIP: Pod IP\n    podRestarts: Pod Restarts\n    workload: Workload\n    pods: Pods by State\n    runs: Runs\n  gaugeStates:\n    active: Active\n    transitioning: Transitioning\n    warning: Warning\n    error: Error\n    succeeded: Successful\n    running: Running\n    failed: Failed\n  hideTabs: 'Hide Advanced Options'\n  job:\n    activeDeadlineSeconds:\n      label: Active Deadline\n      tip: The duration that the job may be active before the system tries to terminate it.\n    backoffLimit:\n      label: Back Off Limit\n      tip: The number of retries before marking this job failed.\n    completions:\n      label: Completions\n      tip: The number of successfully finished pods the job should be run with.\n    failedJobsHistoryLimit:\n      label: Failed Job History Limit\n      tip: The number of failed finished jobs to retain.\n    parallelism:\n      label: Parallelism\n      tip: The maximum number of pods the job should run at any given time.\n    startingDeadlineSeconds:\n      label: Starting Deadline Seconds\n      tip: The deadline in seconds for starting the job if it misses scheduled time\n    successfulJobsHistoryLimit:\n      label: Successful Job History Limit\n      tip: The number of successful finished jobs to retain.\n    suspend: Suspend\n  metrics:\n    pod: Pod Metrics\n    metricsView: Metrics View\n  networking:\n    dnsPolicy:\n      label: DNS Policy\n      options:\n        clusterFirst: Cluster First\n        clusterFirstWithHostNet: Cluster First With Host Network\n        default: Default\n        none: None\n      placeholder: Select a Policy...\n    hostAliases:\n      add: Add Alias\n      keyLabel: IP Address\n      keyPlaceholder: e.g. 1.1.1.1\n      label: Host Aliases\n      tip: Additional /etc/hosts entries to be injected in the container.\n      valueLabel: Hostname\n      valuePlaceholder: \"e.g. foo.com, bar.com\"\n    hostname:\n      label: Hostname\n      placeholder: e.g. web\n    nameservers:\n      add: Add Nameserver\n      label: Nameservers\n      placeholder: e.g. 1.1.1.1\n    networkMode:\n      label: Network Mode\n      options:\n        hostNetwork: Host Network\n        normal: Normal\n      placeholder: Select a Mode...\n    dns: DNS\n    resolver:\n      label: Resolver Options\n      add: Add Option\n    searches:\n      add: Add Search Domain\n      label: Search Domains\n      placeholder: e.g. mycompany.com\n    subdomain:\n      label: Subdomain\n      placeholder: e.g. web\n  validation:\n    containers: Containers\n    containerImage: Container {name} - \"Container Image\" is required.\n  replicas: Replicas\n  showTabs: 'Show Advanced Options'\n  scheduling:\n    activeDeadlineSeconds: Pod Active Deadline\n    activeDeadlineSecondsTip: The duration that the pod may be active before the system tries to mark it failed and kill associated containers.\n    affinity:\n      addNodeSelector: Add Node Selector\n      anyNode: Run pods on any available node\n      affinityTitle: Run pods on nodes with pods matching these selectors\n      antiAffinityTitle: Run pods on nodes without pods matching these selectors\n      affinityOption: Affinity\n      antiAffinityOption: Anti-Affinity\n      matchExpressions:\n        addRule: Add Rule\n        doesNotExist: Does Not Exist\n        exists: Exists\n        greaterThan: \">\"\n        in: =\n        inNamespaces: \"Pods in these namespaces:\"\n        key: Key\n        lessThan: <\n        namespaces: Namespaces\n        notIn: ≠\n        operator: Operator\n        value: Value\n        weight: Weight\n      noPodRules: There are no pod scheduling rules configured.\n      nodeName: Node Name\n      priority: Priority\n      preferAny: \"Prefer any of:\"\n      preferred: Preferred\n      required: Required\n      requireAny: \"Require any of:\"\n      schedulingRules: Run pods on node(s) matching scheduling rules\n      specificNode: Run pods on specific node(s)\n      thisPodNamespace: This pod's namespace\n      topologyKey:\n        label: Topology Key\n        placeholder: e.g. failure-domain.beta.kubernetes.io/zone\n      type: Type\n    priority:\n      className: Priority Class Name\n      priority: Priority\n    terminationGracePeriodSeconds: Termination Grace Period\n    terminationGracePeriodSecondsTip: The duration that the pod needs to terminate gracefully.\n    titles:\n      advanced: Advanced\n      nodeScheduling: Node Scheduling\n      nodeSelector: Nodes with these labels\n      podScheduling: Pod Scheduling\n      priority: Priority\n      tab: Scheduling\n      tolerations: Tolerations\n      limits: Limits and Reservations\n    tolerations:\n      addToleration: Add Toleration\n      effect: Effect\n      effectOptions:\n        all: All\n        noExecute: NoExecute\n        noSchedule: \"NoSchedule,\"\n        preferNoSchedule: PreferNoSchedule\n      labelKey: Label Key\n      operator: Operator\n      operatorOptions:\n        equal: =\n        exists: Exists\n      tolerationSeconds: Toleration Seconds\n      value: Value\n  serviceName: Service Name\n  storage:\n    subtypes:\n      secret: Secret\n      configMap: ConfigMap\n      hostPath: Bind-Mount\n      persistentVolumeClaim: Persistent Volume Claim\n      createPVC: Create Persistent Volume Claim\n      csi: CSI\n      nfs: NFS\n      awsElasticBlockStore: Amazon EBS Disk\n      azureDisk: Azure Disk\n      azureFile: Azure File\n      gcePersistentDisk: Google Persistent Disk\n      driver.longhorn.io: Longhorn\n      vsphereVolume: VMWare vSphere Volume\n    addClaim: Add Claim\n    addMount: Add Mount\n    addVolume: Add Volume\n    certificate: Certificate\n    csi:\n      diskName: Disk Name\n      diskURI: Disk URI\n      cachingMode:\n        label: Caching Mode\n        options:\n          none: None\n          readOnly: Read Only\n          readWrite: Read Write\n      kind:\n        label: Kind\n        options:\n          dedicated: Dedicated\n          managed: Managed\n          shared: Shared\n      drivers:\n        driver.longhorn.io: Longhorn\n      fsType: Filesystem Type\n      shareName: Share Name\n      secretName: Secret Name\n      volumeID: Volume ID\n      partition: Partition\n      pdName: Persistent Disk Name\n      storagePolicyID: Storage Policy ID\n      storagePolicyName: Storage Policy Name\n      volumePath: Volume Path\n    defaultMode: Default Mode\n    driver: driver\n    hostPath:\n      label: The Path on the Node must be\n      options:\n        default: 'Anything: do not check the target path'\n        directoryOrCreate: A directory, or create if it doesn't exist\n        directory: An existing directory\n        fileOrCreate: A file, or create if it doesn't exist\n        file: An existing file\n        socket: An existing socket\n        charDevice: An existing character device\n        blockDevice: An existing block device\n    mountPoint: Mount Point\n    nodePath: Path on Node\n    optional:\n      label: Optional\n      'no': 'No'\n      'yes': 'Yes'\n    path: Path\n    readOnly: Read Only\n    server: Server\n\n    subPath: Sub Path in Volume\n    title: 'Storage'\n    volumeName: Volume Name\n    volumePath: Volume Path\n  typeDescriptions:\n    apps.daemonset: DaemonSets run exactly one pod on every eligible node. When new nodes are added to the cluster, DaemonSets automatically deploy to them. Recommended for system-wide or vertically-scalable workloads that never need more than one pod per node.\n    apps.deployment: Deployments run a scalable number of replicas of a pod distributed among the eligible nodes. Changes are rolled out incrementally and can be rolled back to the previous revision when needed. Recommended for stateless & horizontally-scalable workloads.\n    apps.statefulset: StatefulSets manage stateful applications and provide guarantees about the ordering and uniqueness of the pods created. Recommended for workloads with persistent storage or strict identity, quorum, or upgrade order requirements.\n    batch.cronjob: CronJobs create Jobs, which then run Pods, on a repeating schedule. The schedule is expressed in standard Unix cron format, and uses the timezone of the Kubernetes control plane (typically UTC).\n    batch.job: Jobs create one or more pods to reliably perform a one-time task by running a pod until it exits successfully. Failed pods are automatically replaced until the specified number of completed runs has been reached. Jobs can also run multiple pods in parallel or function as a batch work queue.\n  upgrading:\n    activeDeadlineSeconds:\n      label: Pod Active Deadline\n      tip: The duration the pod may be active before the system will try to mark it failed and kill associated containers.\n    concurrencyPolicy:\n      label: Concurrency\n      options:\n        allow: Allow CronJobs to run concurrently\n        forbid: Skip next run if current run hasn't finished\n        replace: Replace run if current run hasn't finished\n    maxSurge:\n      label: Max Surge\n      tip: The maximum number of pods allowed beyond the desired scale at any given time.\n    maxUnavailable:\n      label: Max Unavailable\n      tip: The maximum number of pods which can be unavailable at any given time.\n    minReadySeconds:\n      label: Minimum Ready\n      tip: The minimum duration a pod should be ready without containers crashing for it to be considered available.\n    podManagementPolicy:\n      label: Pod Management Policy\n    progressDeadlineSeconds:\n      label: Progress Deadline\n      tip: The minimum duration to wait for a deployment to progress before marking it failed.\n    revisionHistoryLimit:\n      label: Revision History Limit\n      tip: The number of old ReplicaSets to retain for rollback.\n    strategies:\n      labels:\n        delete: \"On Delete: New pods are only created when old pods are manually deleted.\"\n        recreate: \"Recreate: Kill ALL pods, then start new pods.\"\n        rollingUpdate: \"Rolling Update: Create new pods, until max surge is reached, before deleting old pods. Don't stop more pods than max unavailable.\"\n    terminationGracePeriodSeconds:\n      label: Termination Grace Period\n      tip: The duration the pod needs to terminate successfully.\n    title: Upgrading\n\n\n##############################\n# Model Properties\n##############################\nmodel:\n  account:\n    kind:\n      admin: Admin\n      agent: Agent\n      project: Environment\n      registeredAgent: Registered Agent\n      service: Service\n      user: User\n  \"catalog.cattle.io.app\":\n    firstDeployed: First Deployed\n    lastDeployed: Last Deployed\n  authConfig:\n    description:\n      ldap: LDAP\n      saml: SAML\n      oauth: OAuth\n      oidc: OIDC\n    name:\n      keycloak: Keycloak (SAML)\n      keycloakoidc: Keycloak (OIDC)\n    provider:\n      system: System\n      local: Local\n      multiple: Multiple\n      activedirectory: ActiveDirectory\n      azuread: AzureAD\n      github: GitHub\n      keycloak: Keycloak\n      ldap: LDAP\n      openldap: OpenLDAP\n      shibboleth: Shibboleth\n      ping: Ping Identity\n      adfs: ADFS\n      okta: Okta\n      freeipa: FreeIPA\n      googleoauth: Google\n      oidc: OIDC\n      keycloakoidc: Keycloak\n\n  cluster:\n    name: Cluster Name\n  ingress:\n    displayKind: L7 Ingress\n  machine:\n    role:\n      controlPlane: Control Plane\n      etcd: etcd\n      worker: Worker\n  openldapconfig:\n    domain:\n      help: Only users below this base will be used.\n      label: User Search Base\n      placeholder: \"e.g. ou=Users,dc=mycompany,dc=com\"\n    server:\n      label: Hostname or IP Address\n    serviceAccountPassword:\n      label: Service Account Password\n    serviceAccountUsername:\n      label: Service Account Username\n  projectMember:\n    role:\n      member: Member\n      owner: Owner\n      readonly: Read-Only\n      restricted: Restricted\n  service:\n    displayKind:\n      generic: Service\n      loadBalancer: L4 Balancer\n\ntypeDescription:\n  # Map of\n  # type: Description to be shown on the top of list view describing the type.\n  #       Should fit on one line.\n  #       If you link to anything external, it MUST have\n  #       target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n  cis.cattle.io.clusterscanbenchmark: A benchmark version is the name of benchmark to run using kube-bench as well as the valid configuration parameters for that benchmark.\n  cis.cattle.io.clusterscanprofile: A profile is the configuration for the CIS scan, which is the benchmark versions to use and any specific tests to skip in that benchmark.\n  cis.cattle.io.clusterscan: A scan is created to trigger a CIS scan on the cluster based on the defined profile. A report is created after the scan is completed.\n  cis.cattle.io.clusterscanreport: A report is the result of a CIS scan of the cluster.\n  management.cattle.io.feature: Feature Flags allow certain {vendor} features to be toggled on and off.  Features that are off by default should be considered experimental functionality.\n  resources.cattle.io.backup: A backup is created to perform one-time backups or schedule recurring backups based on a ResourceSet.\n  resources.cattle.io.restore: A restore is created to trigger a restore to the cluster based on a backup file.\n  resources.cattle.io.resourceset: A resource set defines which CRDs and resources to store in the backup.\n  monitoring.coreos.com.servicemonitor: A service monitor defines the group of services and the endpoints that Prometheus will scrape for metrics. This is the most common way to define metrics collection.\n  monitoring.coreos.com.podmonitor: A pod monitor defines the group of pods that Prometheus will scrape for metrics. The common way is to use service monitors, but pod monitors allow you to handle any situation where a service monitor wouldn't work.\n  monitoring.coreos.com.prometheusrule: A Prometheus Rule resource defines both recording and/or alert rules. A recording rule can pre-compute values and save the results. Alerting rules allow you to define conditions on when to send notifications to AlertManager.\n  monitoring.coreos.com.prometheus: A Prometheus server is a Prometheus deployment whose scrape configuration and rules are determined by selected ServiceMonitors, PodMonitors, and PrometheusRules and whose alerts will be sent to all selected Alertmanagers with the custom resource's configuration.\n  monitoring.coreos.com.alertmanager: An alert manager is deployment whose configuration will be specified by a secret in the same namespace, which determines which alerts should go to which receiver.\n  catalog.cattle.io.clusterrepo: 'A chart repository is a Helm repository or {vendor} git based application catalog. It provides the list of available charts in the cluster.'\n  catalog.cattle.io.operation: An operation is the list of recent Helm operations that have been applied to the cluster.\n  catalog.cattle.io.app: An installed application is a Helm 3 chart that was installed either via our charts or through the Helm CLI.\n  logging.banzaicloud.io.clusterflow: Logs from the cluster will be collected and logged to the selected Cluster Output.\n  logging.banzaicloud.io.clusteroutput: A cluster output defines which logging providers that logs can be sent to and is only effective when deployed in the namespace that the logging operator is in.\n  logging.banzaicloud.io.flow: A flow defines which logs to collect and filter as well as which output to send the logs. The flow is a namespaced resource, which means logs will only be collected from the namespace that the flow is deployed in.\n  logging.banzaicloud.io.output: An output defines which logging providers that logs can be sent to. The output needs to be in the same namespace as the flow that is using it.\n  group.principal: Assigning global roles to a group only works with external auth providers that support groups. Local authorization does not support groups.\n\ntypeLabel:\n  management.cattle.io.token: |-\n    {count, plural,\n      one { API Key }\n      other { API Keys }\n    }\n  cis.cattle.io.clusterscan: |-\n    {count, plural,\n      one { Scan }\n      other { Scans }\n    }\n  cis.cattle.io.clusterscanprofile: |-\n    {count, plural,\n      one { Profile }\n      other { Profiles }\n    }\n  cis.cattle.io.clusterscanbenchmark: |-\n    {count, plural,\n      one { Benchmark Version }\n      other { Benchmark Versions }\n    }\n  catalog.cattle.io.operation: |-\n    {count, plural,\n      one { Recent Operation }\n      other { Recent Operations }\n    }\n  catalog.cattle.io.app: |-\n    {count, plural,\n      one { Installed App }\n      other { Installed Apps }\n    }\n  catalog.cattle.io.clusterrepo: |-\n    {count, plural,\n      one { Chart Repository }\n      other { Chart Repositories }\n    }\n  catalog.cattle.io.repo: |-\n    {count, plural,\n      one { Namespaced Repo }\n      other { Namespaced Repos }\n    }\n  chartInstallAction: |-\n    {count, plural,\n      one { App }\n      other { Apps }\n    }\n  chartUpgradeAction: |-\n    {count, plural,\n      one { App }\n      other { Apps }\n    }\n  endpoints: |-\n    {count, plural,\n      one { Endpoint }\n      other { Endpoints }\n    }\n  fleet.cattle.io.cluster: |-\n    {count, plural,\n      =1 { Cluster }\n      other {Clusters }\n    }\n  fleet.cattle.io.clustergroup: |-\n    {count, plural,\n      one { Cluster Group }\n      other {Cluster Groups }\n    }\n  management.cattle.io.clusterroletemplatebinding: |-\n    {count, plural,\n      one { Cluster Member }\n      other { Cluster Members }\n    }\n  fleet.cattle.io.gitrepo: |-\n    {count, plural,\n      one { Git Repo }\n      other {Git Repos }\n    }\n  management.cattle.io.authconfig: |-\n    {count, plural,\n      one { Authentication Provider }\n      other { Authentication Providers }\n    }\n  management.cattle.io.feature: |-\n    {count, plural,\n      one { Feature Flag }\n      other { Feature Flags }\n    }\n  management.cattle.io.setting: |-\n    {count, plural,\n      one { Advanced Setting }\n      other { Advanced Settings }\n    }\n  management.cattle.io.fleetworkspace: |-\n    {count, plural,\n      one { Workspace }\n      other { Workspaces }\n    }\n  # pruh-mee-thee-eyes https://www.prometheus.io/docs/introduction/faq/#what-is-the-plural-of-prometheus\n  monitoring.coreos.com.prometheus: |-\n    {count, plural,\n      one { Prometheus }\n      other { Prometheis }\n    }\n  monitoring.coreos.com.servicemonitor: |-\n    {count, plural,\n      one { Service Monitor }\n      other { Service Monitors }\n    }\n  monitoring.coreos.com.alertmanager: |-\n    {count, plural,\n      one { Alert Manager }\n      other { Alert Managers }\n    }\n  monitoring.coreos.com.podmonitor: |-\n    {count, plural,\n      one { Pod Monitor }\n      other { Pod Monitors }\n    }\n  monitoring.coreos.com.prometheusrule: |-\n    {count, plural,\n      one { Prometheus Rule }\n      other { Prometheus Rules }\n    }\n  monitoring.coreos.com.thanosruler: |-\n    {count, plural,\n      one { Thanos Rule }\n      other { Thanos Rules }\n    }\n  monitoring.coreos.com.receiver: |-\n    {count, plural,\n      one { Receiver }\n      other { Receivers }\n    }\n  monitoring.coreos.com.route: |-\n    {count, plural,\n      one { Route }\n      other { Routes }\n    }\n  'management.cattle.io.cluster': |-\n    {count, plural,\n      one { Mgmt Cluster }\n      other { Mgmt Clusters }\n    }\n  'cluster.x-k8s.io.cluster': |-\n    {count, plural,\n      one { CAPI Cluster }\n      other { CAPI Clusters }\n    }\n  'provisioning.cattle.io.cluster': |-\n    {count, plural,\n      one { Cluster }\n      other { Clusters }\n    }\n  'management.cattle.io.user': |-\n    {count, plural,\n      one { User }\n      other { Users }\n    }\n  namespace: |-\n    {count, plural,\n      one { Namespace }\n      other { Namespaces }\n    }\n  group.principal: |-\n    {count, plural,\n      one { Group }\n      other { Groups }\n    }\n  token: |-\n    {count, plural,\n      one { API Key }\n      other { API Keys }\n    }\n\naction:\n  clone: Clone\n  disable: Disable\n  download: Download YAML\n  edit: Edit Config\n  editYaml: Edit YAML\n  enable: Enable\n  openLogs: View Logs\n  refresh: Refresh\n  remove: Delete\n  view: View Config\n  viewInApi: View in API\n  viewYaml: View YAML\n  activate: Activate\n  deactivate: Deactivate\n  show: Show\n  hide: Hide\n  copy: Copy\n  unassign: 'Unassign'\n  uninstall: Uninstall\n\nunit:\n  sec: secs\n  min: mins\n  hour: |-\n    {count, plural,\n      one { hour }\n      other { hours }\n    }\n  day: |-\n    {count, plural,\n      one { day }\n      other { days }\n    }\nworkloadPorts:\n  addPort: Add Port\n  remove: Remove\n  addHost: Add Host\n\npodAffinity:\n  addLabel: Add Pod Selector\n\nkeyValue:\n  keyPlaceholder: e.g. foo\n  valuePlaceholder: e.g. bar\n\n##############################\n### Advanced Settings\n##############################\n\nadvancedSettings:\n  label: Advanced Settings\n  subtext: Typical users will not need to change these. Proceed with caution, incorrect values can break your {appName} installation. Settings which have been customized from default settings are tagged 'Modified'.\n  show: Show\n  hide: Hide\n  none: None\n  edit:\n    label: Edit Setting\n    changeSetting: \"Change Setting:\"\n    trueOption: \"True\"\n    falseOption: \"False\"\n    value: Value\n    useDefault: Copy the default value\n    invalidJSON: Invalid JSON - please check and correct your input before saving\n  descriptions:\n    'cacerts': \"CA Certificates needed to verify the server's certificate.\"\n    'cluster-defaults': 'Override RKE Defaults when creating new clusters.'\n    'engine-install-url': 'Default Docker engine installation URL (for most node drivers).'\n    'engine-iso-url': 'Default OS installation URL (for vSphere driver).'\n    'engine-newest-version': 'The newest supported version of Docker at the time of this release.  A Docker version that does not satisfy supported docker range but is newer than this will be marked as untested.'\n    'engine-supported-range': 'Semver range for supported Docker engine versions.  Versions which do not satisfy this range will be marked unsupported in the UI.'\n    'ingress-ip-domain': 'Wildcard DNS domain to use for automatically generated Ingress hostnames. <ingress-name>.<namespace-name>.<ip address of ingress controller> will be added to the domain.'\n    'server-url': 'Default {appName} install url. Must be HTTPS. All nodes in your cluster must be able to reach this.'\n    'system-default-registry': 'Private registry to be used for all system Docker images.'\n    'ui-index': 'HTML index location for the Cluster Manager UI.'\n    'ui-dashboard-index': 'HTML index location for the {appName} UI.'\n    'ui-offline-preferred': 'Controls whether UI assets are served locally by the server container or from the remote URL defined in the ui-index and ui-dashboard-index settings. The `Dynamic` option will use local assets in production builds of {appName}.'\n    'ui-pl': 'Private-Label company name.'\n    'ui-issues': \"Use a url address to send new 'File an Issue' reports instead of sending users to the GitHub issues page.\"\n    'telemetry-opt': 'Telemetry reporting opt-in.'\n    'auth-user-info-max-age-seconds': 'The maximum age of a users auth tokens before an auth provider group membership sync will be performed.'\n    'auth-user-info-resync-cron': 'Default cron schedule for resyncing auth provider group memberships.'\n    'cluster-template-enforcement': 'Non-admins will be restricted to launching clusters via preapproved RKE Templates only.'\n    'auth-user-session-ttl-minutes': 'Custom TTL (in minutes) on a user auth session.'\n    'auth-token-max-ttl-minutes': 'Custom max TTL (in minutes) on an auth token.'\n    'kubeconfig-generate-token': 'Automatically generate kubeconfig tokens for users.'\n    'kubeconfig-token-ttl-minutes': 'Custom max TTL (in minutes) on a kubeconfig token.'\n    'rke-metadata-config': 'Configure RKE metadata refresh parameters.'\n    'ui-banners': 'Classification banner is used to display a custom fixed banner in the header, footer, or both.'\n    'ui-default-landing': 'The default page users land on after login.'\n    'brand': Folder name for an alternative theme defined in '/assets/brand'\n  editHelp:\n    'ui-banners': This setting takes a JSON object containing 3 root parameters; <code>banner</code>, <code>showHeader</code>, <code>showFooter</code>. <code>banner</code> is an object containing; <code>textColor</code>, <code>background</code>, and <code>text</code>, where <code>textColor</code> and <code>background</code> are any valid CSS color value.\n  enum:\n    'ui-default-landing':\n      ember: Cluster Manager\n      vue: Cluster Explorer\n    'telemetry-opt':\n      prompt: Prompt\n      in: Opt-in to Telemetry\n      out: Opt-out of Telemetry\n    'ui-offline-preferred':\n      dynamic: Dynamic\n      true: Local\n      false: Remote\n\nfeatureFlags:\n  label: Feature Flags\n  warning: |-\n    Feature flags allow {vendor} to gate certain features behind flags.\n    Features that are off by default should be considered experimental functionality.\n    Some features require a restart of the {vendor} server to change.\n    This will result in a short outage of the API and UI, but not affect running clusters or workloads.\n  restartRequired: \"Note: Updating this feature flag requires a restart\"\n  restart:\n    title: Waiting for Restart\n    wait: This may take a few moments\n\nbranding:\n  label: Branding\n  directoryName: Brand Asset Directory Name\n  logos:\n    label: Logo\n    tip: 'Upload a logo to replace the Rancher logo in the top-level navigation header. Image height should be 21 pixels with a max width of 200 pixels. Max file size is 20KB'\n    lightPreview: Light Theme Preview\n    darkPreview: Dark Theme Preview\n    uploadLight: Upload Light Logo\n    uploadDark: Upload Dark Logo\n    useCustom: Use a Custom Logo\n  options:\n    default: Default Rancher Theme\n    suse: SUSE Theme\n    custom: Define a Custom Theme\n  uiPL:\n    label: Private Label Company Name\n  uiIssues:\n    label: Issue Reporting URL\n  uiBanner:\n    label: Fixed Banners\n    text: Text\n    textColor: Text Color\n    background: Background Color\n    showHeader: Show Banner in Header\n    showFooter: Show Banner in Footer\n  color:\n    label: Primary Color\n    tip: You can override the primary color used throughout the UI with a custom color of your choice.\n    useCustom: Use a Custom Color\n\nresourceQuota:\n  configMaps: Config Maps\n  limitsCpu: CPU Limit\n  limitsMemory: Memory Limit\n  persistentVolumeClaims: Persistent Volume Claims\n  pods: Pods\n  replicationControllers: Replication Controllers\n  requestsCpu: CPU Reservation\n  requestsMemory: Memory Reservation\n  requestsStorage: Storage Reservation\n  secrets: Secrets\n  services: Services\n  servicesLoadBalancers: Services Load Balancers\n  servicesNodePorts: Service Node Ports\n  projectLimit:\n    label: Project Limit\n    cpuPlaceholder: e.g. 2000\n    memoryPlaceholder: e.g. 2048\n    storagePlaceholder: e.g. 50\n    unitlessPlaceholder: e.g. 50\n  namespaceDefaultLimit:\n    label: Namespace Default Limit\n    cpuPlaceholder: e.g. 500\n    memoryPlaceholder: e.g. 1024\n    storagePlaceholder: e.g. 10\n    unitlessPlaceholder: e.g. 10\n  resourceType:\n    label: Resource Type\n\nsnapshots:\n  title: Snapshots\n  action:\n    create: Create Snapshot\n  card:\n    created: \"Created on '<b>'{date}'</b>' at '<b>'{time}'</b>'\"\n    action:\n      restore: Restore\n      remove: Delete\n  create:\n    title: 'Create a new Snapshot'\n    name:\n      label: Snapshot Name\n    description:\n      label: Description\n    actions:\n      submit: Create\n      back: Cancel\n    info: 'Rancher Desktop will be temporarily unavailable while creating a new Snapshot'\n  empty:\n    icon: icon-search\n    heading: No snapshots found\n    body: Click on Create Snapshot to get started\n  dialog:\n    restore:\n      header: Restore snapshot?\n      info: 'Restoring this snapshot will replace your current installation, including preferences. If the Preferences window is open, it will be automatically closed and any unsaved changes will be discarded.'\n      actions:\n        ok: 'Restore'\n        cancel: 'Cancel'\n      error:\n        header: 'Error restoring snapshot'\n        description: \"Failed to restore snapshot '<b>'{snapshot}'</b>'.'<br/><br/>'Rancher Desktop has performed a Factory Reset and will now exit. Please try to address the issue that led to this error before restarting Rancher Desktop and attempting to restore the snapshot again.\"\n        buttonText: 'Quit Rancher Desktop'\n    delete:\n      header: Permanently delete snapshot?\n      info: 'text'\n      actions:\n        ok: 'Delete'\n        cancel: 'Cancel'\n    restoring:\n      header: 'Restoring {snapshot}'\n      message: \"Please wait while snapshot '<b>'{snapshot}'</b>' is being restored. This may take some time depending on your machine's resources.<br/><br/>Rancher Desktop will be temporarily unavailable during this operation. Once completed, the VM will restart with the {snapshot} snapshot active.\"\n      actions:\n        cancel: 'Cancel'\n    creating:\n      header: 'Creating {snapshot}'\n      message: \"Please wait while snapshot '<b>'{snapshot}'</b>' is being created.'<br/>'This may take some time depending on your machine's resources.'<br/>'Rancher Desktop will be temporarily unavailable during this operation.'<br/><br/>'Once the snapshot is complete, the VM will restart.\"\n      actions:\n        cancel: 'Cancel'\n    buttons:\n      error: Close\n    generic:\n      header: Snapshot operation in progress\n      message: \"Please wait while the snapshot operation is active.<br/>This may take some time depending on your machine's resources.<br/>Rancher Desktop will be temporarily unavailable during this operation.<br/><br/>Once the snapshot process is complete, the VM will restart.\"\n    showLogs: Show logs\n  info:\n    when: \" at {time}\"\n    restore:\n      success: \"Restored '<b>'{snapshot}'</b>'\"\n      cancel: 'Cancelled restoration of {snapshot}'\n    create:\n      success: \"Created '<b>'{snapshot}'</b>'\"\n      cancel: 'Cancelled creation of {snapshot}'\n    delete:\n      success: \"Deleted '<b>'{snapshot}'</b>'\"\n      error: 'Delete error: {error}'\n    lock:\n      info: \"A snapshot lock file has been detected for an unusually long time. If you believe this is an error, you can try to remove the lock file by running <code>rdctl snapshot unlock</code>.\"\n\n\n##############################\n### Troubleshooting Page\n##############################\ntroubleshooting:\n  title: Troubleshooting\n  description: Use these tools to help identify and resolve issues.\n  kubernetes:\n    title: Kubernetes\n    resetKubernetes:\n      title: Reset Kubernetes\n      description: Resetting Kubernetes will delete all workloads and configuration.\n      buttonText: Reset Kubernetes\n      messageBox:\n        title: Rancher Desktop - Reset Kubernetes\n        message: Reset Kubernetes?\n        checkboxLabel: Delete container images\n        ok: Reset\n        cancel: Cancel\n    resetContainer:\n      title: 'Reset Kubernetes & Container Images'\n      description: All images will be lost and Kubernetes will be reset.\n      buttonText: Reset Container Images\n  general:\n    title: General\n    logs:\n      title: Logs\n      description: Show Rancher Desktop logs\n      buttonText: Show Logs\n    factoryReset:\n      title: Factory Reset\n      description: Factory Reset will remove all Rancher Desktop Configurations.\n      buttonText: Factory Reset\n      messageBox:\n        title: Rancher Desktop - Factory Reset\n        message: Perform a factory reset?\n        detail: <p>Doing a factory reset will remove your cluster and all Rancher Desktop settings, and shut down Rancher Desktop. If you intend to continue using Rancher Desktop, you will need to manually start it and go through the initial set up again.</p><p>Are you sure you want to reset everything?</p>\n        checkboxLabel: Keep cached Kubernetes images\n        ok: Factory Reset\n        cancel: Cancel\n  needHelp: 'Still having problems? Start a discussion in the <b>#rancher-desktop</b> channel on the <a href=\"https://slack.rancher.io/\">Rancher Users Slack</a> or <a href=\"https://github.com/rancher-sandbox/rancher-desktop/issues\">Report an Issue</a>.'\n\n##############################\n### Diagnostics Page\n##############################\ndiagnostics:\n  results:\n    muted:\n      icon: icon-search\n      heading: No results found\n      body: Try showing muted diagnostics.\n    success:\n      icon: icon-checkmark\n      heading: No problems detected\n      body: Rancher Desktop appears to be functioning correctly.\n\n\n##############################\n### Extensions Page\n##############################\nextensions:\n  icon: icon-extension\n  installed:\n    list:\n      upgrade: Upgrade\n      uninstall: Remove\n    emptyState:\n      icon: icon-extension\n      heading: No extensions installed\n      body: It looks like you don't have any extensions installed yet. Browse the extensions catalog to get started.\n      button:\n        text: Browse Extensions\n  view:\n    emptyState:\n      heading: Extension removed\n      body: '{extensionId} was removed. Please visit the extensions catalog to reinstall the extension or browse for other extensions to enhance your Rancher Desktop experience.'\n\n##############################\n### Preferences Page\n##############################\npreferences:\n  actions:\n    banner:\n      reset: Kubernetes will reset after applying changes.\n      restart: Kubernetes will restart after applying changes.\n      error: \"There's a problem with the preferences selection.\"\n  locked:\n    tooltip: Locked due to organization's policy\n  incompatibleTypeWarningPre: 'The option requires'\n  incompatibleTypeWarningPostSelected: 'to be selected.'\n  incompatibleTypeWarningPostDisabled: 'to be unselected.'\n  incompatiblePrefWarningOr: 'or'\n##############################\n### Integrations Page\n##############################\nintegrations:\n  windows:\n    title: WSL Integrations\n    description: \"Expose Rancher Desktop's Kubernetes configuration and Docker socket to Windows Subsystem for Linux (WSL) distros\"\n\n##############################\n### Support Page\n##############################\n\nsupport:\n  community:\n    title: SUSE Rancher provides world-class support\n    linksTitle: Community Support\n    learnMore: Find out more about SUSE Rancher Support\n    pricing: Contact us for pricing\n  subscription:\n    haveSupport: Already have support?\n    addSubscription: Add a Subscription ID\n    removeSubscription: Remove your Subscription ID\n    addTitle: Add your SUSE Subscription ID\n    addLabel: \"Please enter a valid Subscription ID:\"\n    removeTitle: Remove your ID?\n    removeBody: \"Note: This will not affect your subscription.\"\n  suse:\n    title: \"Great News - You're covered\"\n    editBrand: Customize UI Theme\n    access:\n      title: Get Support\n      text: Login to SUSE Customer Center to access support for your subscription\n      action: SUSE Customer Center\n  promos:\n    one:\n      title: 24x7 Support\n      text: We provide tightly defined SLAs, and offer round the clock support options.\n    two:\n      title: Issue Resolution\n      text: Run SUSE Rancher products with confidence, knowing that the developers who built them are available to quickly resolve issues.\n    three:\n      title: Troubleshooting\n      text: We focus on uncovering the root cause of any issue, whether it is related to Rancher Labs products, Kubernetes, Docker or your underlying infrastructure.\n    four:\n      title: Innovate with Freedom\n      text: Take advantage of our certified compatibility with a wide range of Kubernetes providers, operating systems, and open source software.\n\nembedding:\n  retry: Retry\n  unavailable: Cluster Manager UI is not available\n\nv1ClusterTools:\n  monitoring:\n    label: Monitoring (Legacy)\n    description: 'Legacy V1 monitoring. V1 Monitoring is deprecated since Rancher 2.5.0. <a target=\"blank\" href=\"https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/migrating/#migrating-from-monitoring-v1-to-monitoring-v2\">Learn more</a> about migrating to V2 Monitoring.'\n  logging:\n    label: Logging (Legacy)\n    description: 'Legacy V1 logging. V1 Logging is deprecated since Rancher 2.5.0. <a target=\"blank\" href=\"https://rancher.com/docs/rancher/v2.x/en/logging/v2.5/migrating/\">Learn more</a> about migrating to V2 Logging.'\n\nlegacy:\n  alerts: Alerts\n  apps: Apps\n  catalogs: Catalogs\n  globalDnsEntries: Global DNS Entries\n  globalDnsProviders: Global DNS Providers\n  logging: Logging\n  notifiers: Notifiers\n  monitoring: Monitoring\n  psps: Pod Security Policies\n  project:\n    label: Project\n    select: \"Use the Project/Namespace filter at the top of the page to select a Project in order to see legacy Project features.\"\n\nharvester:\n  tableHeaders:\n    actions: Actions\n"
  },
  {
    "path": "pkg/rancher-desktop/assets/translations/zh-hans.yaml",
    "content": "##############################\n# Special stuff\n##############################\ngeneric:\n  add: 添加\n  back: 返回\n  cancel: 取消\n  close: 关闭\n  comingSoon: 即将推出\n  copy: 复制\n  create: 创建\n  created: 创建时间\n  customize: 定制\n  default: 默认\n  disabled: 禁用\n  done: 完成\n  enabled: 启用\n  ignored: 忽略\n  invalidCron: 无效的 cron 调度\n  labelsAndAnnotations: 标签和注释\n  loading: 正在加载中...\n  members: 成员\n  #na: n/a\n  name: 名称\n  never: 从不\n  none: 无\n  #number: '{prefix}{value, number}{suffix}'\n  overview: 概述\n  readFromFile: 从文件读取\n  register: 注册\n  remove: 移除\n\n  resource: |-\n    {count, plural,\n    one  {资源}\n    other {资源}\n    }\n  resourceCount: |-\n    {count, plural,\n    one  {1 resource}\n    other {# resources}\n    }\n  save: 保存\n  type: 类型\n  unknown: 未知\n  key: 键\n  value: 值\n  yes: 是\n  no: 否\n  units:\n    time:\n        5s: 5秒\n        10s: 10秒\n        30s: 30秒\n        1m: 1分钟\n        5m: 5分钟\n        15m: 15分钟\n        30m: 30分钟\n        1h: 1小时\n        2h: 2小时\n        6h: 6小时\n        1d: 1小时\n        7d: 7天\n        30d: 30天\n\n#locale:\n  #en-us: English\n  #zh-hans: 简体中文\n  #none: (None)\n\nnav:\n  title: 仪表盘\n  #backToRancher: Cluster Manager\n  clusterTools: 集群工具\n  shell: 命令行\n  import: 导入 YAML 文件\n  home: 返回首页\n  support: 帮助\n  group:\n    cluster: 集群\n    inUse: 更多资源\n    rbac: RBAC\n    serviceDiscovery: 服务发现\n    starred: 已收藏\n    storage: 存储\n    workload: 工作负载\n    monitoring: 监控\n  ns:\n    all: 全部命名空间\n    clusterLevel: 集群资源\n    #namespace: \"{name}\"\n    namespaced: 命名空间资源\n    orphan: 不在项目中\n    project: \"项目名称: {name}\"\n    system: 系统命名空间\n    user: 用户命名空间\n  apps: 应用商店\n  categories:\n    explore: 浏览集群\n    multiCluster: 全局应用\n    configuration: 配置\n  search:\n    placeholder: 输入关键词，搜索集群\n    noResults: 没有与关键词匹配的集群\n  resourceSearch:\n    label: 资源搜索\n    placeholder: 输入关键词，搜索资源\n\nproduct:\n  clusterGroup: 集群应用\n  globalGroup: 全局应用\n  apps: 应用市场\n  auth: 用户及认证方式\n  backup: 备份\n  cis: CIS 基线测试\n  ecm: 集群管理员\n  explorer: 集群浏览器\n  fleet: Fleet\n  #longhorn: Longhorn\n  manager: 管理集群\n  #gatekeeper: OPA Gatekeeper\n  #istio: Istio\n  logging: 日志\n  #rio: Rio\n  settings: 全局设置\n  monitoring: 监控\n\nsuffix:\n  #percent: \"%\"\n  #cpus: CPUs\n  #ib: iB\n  revisions: |-\n    {count, plural,\n      =1 { 版本 }\n      other { 版本 }\n    }\n  seconds: |-\n    {count, plural,\n      =1 { 秒 }\n      other { 秒 }\n    }\n  sec: Sec\n  times: |-\n    {count, plural,\n      =1 { 次 }\n      other { 次 }\n    }\n##############################\n# Components & Pages\n##############################\naccountAndKeys:\n  title: 账户和API密钥\n  account:\n    title: 账户\n    change: 修改密码\n  apiKeys:\n    title: API密钥\n    notAllowed: 对不起，您没有权限编辑API密钥\n    add:\n      description:\n        label: 描述\n        placeholder: 可选择输入一个描述，以帮助您识别该API密钥。\n      label: 创建API密钥\n      expiry:\n        label: 自动过期\n        options:\n          never: 从不过期\n          day: 一天后过期\n          month: 一个月后过期\n          year: 一年后过期\n          custom: 自定义过期之间\n          maximum: \"{value} - 最大有效期\"\n      customExpiry:\n        options:\n          minute: 分钟\n          hour: 小时\n          day: 日\n          month: 月\n          year: 年\n      scope: 适用范围\n      noScope: 没有适用范围\n    info:\n      #accessKey: Access Key\n      #secretKey: Secret Key\n      #bearerToken: Bearer Token\n      saveWarning: 请在云端或本地妥善保存以上的信息! 如果丢失这些信息，你需要创建一个新的API密钥。\n      keyCreated: 已创建一个新的API密钥。\n      bearerTokenTip: \"Access Key 和 Secret Key 可以作为 HTTP Basic auth 的用户名和密码发送，以授权请求。您也可以将它们组合起来作为一个Bearer token使用。\"\n      ttlLimitedWarning: 由于系统配置的原因，该API密钥的到期时间缩短了。\n\nauthConfig:\n  accessMode:\n    label: '配置够登录和使用{vendor}的人员名单'\n    required: '只有授权用户和用户组能够访问。'\n    restricted: '允许集群和项目的成员，以及授权用户和用户组访问'\n    unrestricted: '允许所有用户访问'\n  allowedPrincipalIds:\n    title: 授权用户和用户组\n  associatedWarning: '注意：您认证为的{provider} 用户将作为您当前登录的 {vendor} 用户的替代登录方式(<code>{username}</code>)。'\n  github:\n    clientId:\n      label: 账户名\n    clientSecret:\n      label: 密码\n    form:\n      app:\n        label: 应用名称\n        value: '输入一个应用名称，例如：我的{vendor}'\n      calllback:\n        label: 授权回调URL\n      description:\n        label: 应用描述\n        value: '选填项，可留空'\n      homepage:\n        label: 主页URL地址\n      instruction: '请在表格中输入以下值：'\n      prefix: |-\n        <li><a href=\"{baseUrl}/settings/developers\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">点击这里</a>，在新窗口中进入GitHub应用设置。</li>\n        <li>点击 \"OAuth App \"标签。</li>\n        <li>点击 \"新建OAuth应用 \"按钮。</li>\n      suffix: |-\n        <li>点击 \"注册应用\"</li>\n        <li>复制并粘贴新创建的OAuth应用程序的客户ID和客户秘密到下面的字段中。</li>\n    host:\n      label: GitHub企业版\n      placeholder: 例如：github.mycompany.example\n    target:\n      label: 您想使用哪种GitHub呢？\n      private: GitHub企业版的私人安装\n      public: 公开的GitHub.com\n    table:\n      #server: Server\n      #clientId: Client ID\n  googleoauth:\n    adminEmail: 电子邮件地址\n    domain: 域名\n    oauthCredentials:\n      label: OAuth 认证信息\n      tip: 包含OAuth Credentials的JSON文件可以在Google API开发者控制台中找到。\n    serviceAccountCredentials:\n      label: Service Account 认证信息\n      tip: 包含Service Account 认证信息的JSON文件可以在Google API开发者控制台中找到。\n    steps:\n      1:\n        title: '第一步：对于标准的Google，点击<a href=\"https://console.developers.google.com/apis/credentials\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">这里</a>在新窗口中进入应用程序设置。'\n        body:\n         1: 登录到您的账户，然后导航到 \"APIs & Services\"，然后选择 \"OAuth consent screen\"。\n         2: 'Authorized domains:'\n         3: 'Application homepage link: '\n         4: '在Google APIs的作用域下，启用 \"email\"、\"profile \"和 \"openid\"。'\n         5: '单击保存，保存以上修改'\n        topPrivateDomain: '顶级域'\n      2:\n        title: '第二步：导航到 \"Credentials\"标签，创建你的OAuth客户端ID。'\n        body:\n          1: '选择 \"Create Credentials\"下拉菜单，选择 \"OAuth clientID\"，然后选择 \"Web application\"。'\n          2: 'Authorized JavaScript origins:'\n          3: 'Authorized redirect URIs:'\n          4: '点击 \"Create\"，然后点击 \"Download JSON\"按钮。'\n          5: '在OAuth凭证框中上传下载的JSON文件。'\n      3:\n        title: '第三步：创建服务账户凭证'\n        introduction: '按照<a href=\"https://rancher.com/docs/rancher/v2.x/en/admin-settings/authentication/google/#creating-service-account-credentials\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">这里</a>指南。'\n        body:\n          1: 创建一个 service account。\n          2: 为这个service account生成密钥。\n          3: 在你的google域名中添加service account作为OAuth客户端。\n  ldap:\n    freeipa: 配置 FreeIPA server\n    activedirectory: 配置 Active Directory 账户\n    openldap: 配置 OpenLDAP server\n    defaultLoginDomain:\n      label: 默认登录页面的域名\n      placeholder: 例如：mycompany\n      hint: 如果用户在没有指定域的情况下登录，将使用该域。\n    cert: 证书\n    disabledStatusBitmask: 禁用状态 比特掩码\n    groupDNAttribute: 用户组域名属性\n    groupMemberMappingAttribute: 用户组成员映射属性\n    groupMemberUserAttribute: 用户组成员属性\n    groupSearchBase:\n      label: 用户组内搜索\n      #placeholder: 'ou=groups,dc=mycompany,dc=com'\n    hostname: 主机名/IP\n    loginAttribute: 登录属性\n    nameAttribute: 名称属性\n    nestedGroupMembership:\n      label: 属于多个用户组的用户\n      options:\n        direct: 搜索只属于单个用户组的用户\n        nested: 搜索只属于单个用户组的用户和属于多个用户组的用户\n    objectClass: 对象类\n    password: 密码\n    port: 端口\n    customizeSchema: 自定义模式\n    users: 用户\n    groups: 组\n    searchAttribute: 搜索属性\n    searchFilter: 搜索过滤条件\n    serverConnectionTimeout: 服务器连接超时\n    serviceAccountDN: 服务账户的独特名称\n    serviceAccountPassword: Service Account 密码\n    serviceAccountInfo: Rancher需要一个对所有能够登录的域都有只读访问权的服务账户，这样我们就可以在用户使用API密钥进行请求时，确定用户是什么组的成员。\n    starttls:\n      label: Start TLS\n      tip: 通过在连接过程中使用 TLS 封装来升级非加密连接。不能与TLS结合使用。\n    tls: TLS\n    userEnabledAttribute: 用户启用属性\n    userMemberAttribute: 用户组员属性\n    userSearchBase:\n      label: 搜索用户\n      placeholder: '例如：ou=users,dc=mycompany,dc=com'\n    username: 用户名\n    usernameAttribute: 用户名属性\n    table:\n      server: Server\n      clientId: Client ID\n  saml:\n    entityID: Entity ID字段\n    UID: UID字段\n    adfs: 配置AD FS 账户\n    api: Rancher API Host\n    cert:\n      label: 证书\n      placeholder: 粘贴证书，以-----BEGIN CERTIFICATE----- 开始。\n    displayName: 显示名称字段\n    groups: 用户组字段\n    key:\n      label: 私钥\n      placeholder: 粘贴私钥，一般以-----RSA PRIVATE KEY----- 开始。\n    keycloak: 配置Keycloak账户\n    metadata:\n      label: Metadata XML\n      placeholder: 粘贴IDP Metadata XML\n    okta: 配置Okta账户\n    ping: 配置Ping账户\n    shibboleth: 配置shibboleth账户\n    showLdap: 配置OpenLDAP服务器\n    userName: 用户名字段\n  azuread:\n    tenantId: 租户ID\n    applicationId: 应用ID\n    endpoint: 端点\n    #graphEndpoint: Graph Endpoint\n    #tokenEndpoint: Token Endpoint\n    #authEndpoint: Auth Endpoint\n  stateBanner:\n    disabled: '已禁用{provider} 。'\n    enabled: '已启用{provider} 。'\n  testAndEnable: 测试和启用认证\n\nauthGroups:\n  actions:\n    refresh: 刷新用户组成员名单\n    assignRoles: 为当前用户组成员分配全局角色\n  assignEdit:\n    assignTitle: 为当前用户组分配全局角色\n\nassignTo:\n  title: |-\n    {count, plural,\n      =1 { 分配集群到&hellip; }\n      other { 分配 {count} 集群到&hellip; }\n    }\n  labelsTitle: |-\n    {count, plural,\n      =1 { 分配集群到&hellip; }\n      other { 分配 {count}集群到&hellip; }\n    }\n  workspace: 工作空间\n\nasyncButton:\n  apply:\n    action:  '应用'\n    success: '已应用'\n    waiting: '正在应用&hellip;'\n  continue:\n    action:  '继续'\n    success: '已保存'\n    waiting: '正在保存&hellip;'\n  copy:\n    action: 单击复制\n    success: 已复制\n  create:\n    action:  '创建'\n    success: '已创建'\n    waiting: '正在创建&hellip;'\n  default:\n    action: 正在执行\n    error: 错误\n    success: 成功\n    waiting: 等待中\n  delete:\n    action:  '删除'\n    success: '已删除'\n    waiting: '正在删除&hellip;'\n  disable:\n    action:  '禁用'\n    success: '已禁用'\n    waiting: '正在禁用&hellip;'\n  activate:\n    action:  激活\n    waiting: 正在激活&hellip;\n    success: 已激活\n  deactivate:\n    action:  停用\n    waiting: 正在停用&hellip;\n    success: 已停用\n  done:\n    action:  '完成'\n    waiting: '正在保存&hellip;'\n    success: '已保存'\n  download:\n    action:  '下载'\n    waiting: '正在下载&hellip;'\n    success: '下载完成'\n  edit:\n    action: 保存\n    success: 已保存\n    waiting: 正在保存&hellip;\n  enable:\n    action:  '启用'\n    waiting: '正在启用&hellip;'\n    success: '已启用'\n  finish:\n    action:  '完成'\n    waiting: '正在处理中&hellip;'\n    success: '已完成'\n  import:\n    action: 导入\n    success: 已导入\n    waiting: 正在导入&hellip;\n  install:\n    action:  '安装'\n    waiting: '开始安装&hellip;'\n    success: '安装完成'\n  refresh:\n    action: ''\n    actionIcon:  '刷新'\n    waiting: ''\n    waitingIcon: '刷新中'\n    success: ''\n    successIcon: '成功'\n    error: ''\n    errorIcon:   '出错啦'\n  remove:\n    action: 移除\n    success: 已移除\n    waiting: 正在移除&hellip;\n  upgrade:\n    action: 升级\n    success: 完成升级\n    waiting: 已开始升级&hellip;\n\nbackupRestoreOperator:\n  backupFilename: 备份文件名称\n  deleteTimeout:\n    label: 删除超时\n    tip: 在删除定标器强制删除之前，等待资源删除成功的秒数。\n  deployment:\n    rancherNamespace: Rancher 资源集命名空间\n    size: 大小\n    storage:\n        label: 默认存储位置\n        options:\n          defaultStorageClass: '使用({name})作为默认存储类'\n          none: 不使用默认存储类\n          pickPV: 使用已有的持久卷\n          pickSC: 使用已有的存储类\n          s3: 使用Amazon S3对象存储服务\n        persistentVolume:\n          label: 持久存储卷\n        storageClass:\n          label: 存储类\n        tip: '配置一个默认保存所有备份的存储位置。您可以选择对每个备份进行覆盖，但仅限于使用与 S3 兼容的对象存储。'\n        warning: '此 {type} 没有将其回收策略设置为 \"保留\"。 如果卷被更改或未绑定，您的备份可能会丢失。'\n  encryption: '这个{type}没有将其回收策略设置为 \"保留\"。 如果该卷被改变或变得不受约束，你的备份可能会丢失。'\n  encryptionConfigName:\n    backuptip: '<code>cattle-resource-system</code>命名空间中具有<code>encryption-provider-config.yaml</code>密钥的任何秘密。<br/>此文件的内容是从此备份中执行还原所必需的，Rancher Backup 不会存储这些内容。'\n    label: 加密配置密钥\n    options:\n      none: 存储未加密的备份内容。\n      secret: '使用 <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/#understanding-the-encryption-at-rest-configuration\">加密配置秘密</a>（推荐）对备份进行加密。'\n    restoretip: '如果备份是在启用加密的情况下进行的，则应在还原过程中使用包含相同加密提供者配置的秘密。'\n    warning: '该文件的内容是从该备份中执行还原所必需的，Rancher 备份不会存储。'\n  lastBackup: 上一次备份\n  nextBackup: 下一次备份\n  noResourceSet: 您必须在此命名空间中定义一个资源集来创建备份CR。\n  prune:\n    label: 修剪\n    tip: 删除备份中不存在的 Rancher 管理的资源。(推荐使用)\n  resourceSetName: 资源集\n  restoreFrom:\n    existing: 使用已有的备份配置恢复\n    default: 使用默认的存储目标恢复\n    s3: 使用一个 S3 兼容的对象存储恢复\n  retentionCount:\n    label: 备份保留数量\n    units: |-\n      {count, plural,\n        =1 { 文件 }\n        other { 文件 }\n      }\n  s3:\n    bucketName: 桶名称\n    credentialSecretName: 密钥凭证\n    endpoint: 端点\n    endpointCA: 端点 CA\n    folder: 文件夹\n    insecureTLSSkipVerify: 跳过TLS认证\n    region: 区域\n    storageLocation: 存储位置\n    titles:\n      backupLocation: 备份来源\n      location: 存储位置\n      s3: S3\n  schedule:\n    label: 定时备份策略\n    options:\n      disabled: 单次备份\n      enabled: 重复备份\n    placeholder: 例如：@midnight or 0 0 * * *\n  storageSource:\n    configureS3: 使用兼容 S3 的对象存储作为存储位置\n    useBackup: 使用备份 CR 上指定的 S3 位置\n    useDefault: 使用安装时配置的默认存储位置\n  targetBackup: 目标备份\n\n\n\ncatalog:\n  app:\n    managed: 管理\n    section:\n      notes: 发布说明\n      readme: Chart 自述\n      resources: 资源\n      values: YAML\n  charts:\n    all: 所有\n    categories:\n      all: 所有类别\n    #certified:\n      #other: Other\n      #partner: Partner\n      #rancher: Rancher\n    header: Chart Apps\n    noCharts: '没有可用的 chart，你有添加 chart 仓库吗？'\n    noWindows: 您的应用商店没有包含能部署在 Windows 集群上的 chart。\n    search: 过滤\n  install:\n    action:\n      goToUpgrade: 编辑/升级\n      ignoreWarning: 忽略警告，继续升级\n    appReadmeGeneric: 此 chart 没有针对 rancher 的自述文件。查看 Helm 自述文件，了解更多可用配置选项及其用法。\n    chart: Chart\n    error:\n      requiresFound: '必须先安装<a href=\"{url}\">${name}</a>，才能安装这个Chart。'\n      requiresMissing: '这个Chart需要另一个提供{name}的Chart，但没有找到。'\n      insufficientCpu: 'T这个Chart需要{need, number}个CPU核，但集群只有{have, number}个可用。'\n      insufficientMemory: '这个Chart需要{need}的内存，但集群只有{have}可用。'\n    header:\n      install: '安装 {name}'\n      installGeneric: 安装 Chart\n      upgrade: '升级 {name}'\n    helm:\n      #atomic: Atomic\n      cleanupOnFail: 失败时的清理\n      crds: 应用自定义资源定义\n      dryRun: 空运行\n      force: 强制\n      historyMax:\n        label: 保留最后一个\n        unit: |-\n          {value, plural,\n            =1 { 版本 }\n            other { 版本 }\n          }\n      hooks: 执行 chart 钩子\n      openapi: 验证 OpenAPI 模式\n      resetValues: 重置值\n      timeout:\n        label: 超时\n        unit: |-\n          {value, plural,\n            =1 { 秒 }\n            other { 秒 }\n          }\n      wait: 等待\n    namespaceIsInProject: \"这个Chart的目标命名空间<code>{namespace}</code>，已经存在，不能添加到不同的项目中。\"\n    project: 安装到项目\n    section:\n      appReadme: 自述\n      chartOptions: Chart 配置选项\n      helm: Helm 部署选项\n      readme: Helm 自述\n      #valuesYaml: Values YAML\n    version: 版本\n    versions:\n      current: '{ver} (current)'\n      linux: '{ver} (Linux-only)'\n      windows: '{ver} (Windows-only)'\n  operation:\n    tableHeaders:\n      #action: Action\n      releaseName: 版本名称\n      releaseNamespace: 版本命名空间\n  repo:\n    action:\n      refresh: 刷新\n    #all: All\n    gitBranch:\n      label: Git 分支\n      placeholder: 例如：master\n    gitRepo:\n      label: Git Repo URL\n      placeholder: '例如：https://github.com/your-company/charts.git'\n    name:\n      rancher-charts: Rancher\n      rancher-partner-charts: Partners\n    target:\n      git: 包含定义了 Helm chart 的 Git 仓库。\n      http: 指向 Helm 生成的索引 http(s) URL\n      label: 目标类型\n    url:\n      label: Index URL\n      placeholder: '例如：https://charts.rancher.io'\n  tools:\n    header: 集群工具\n    action:\n      install: 安装\n      upgrade: 升级\n      edit: 编辑\n      remove: 移除\n\nchangePassword:\n  title: 修改密码\n  cancel: 取消\n  deleteKeys:\n    label: 删除所有的API密钥\n  changeOnLogin:\n    label: 首次登陆账户时，要求用户立即修改密码。\n  generatePassword:\n    label: 为用户生成随机密码\n  currentPassword:\n    label: 当前在使用的密码\n  userGen:\n    newPassword:\n      label: 新密码\n    confirmPassword:\n      label: 确认新密码\n  randomGen:\n    generated:\n      label: 为用户生成随机密码\n  newGeneratedPassword: 推荐密码\n  errors:\n    missmatchedPassword: 前后两次输入的密码不匹配\n    failedToChange: 无法修改密码\n    failedDeleteKey: 无法删除单个API密钥\n    failedDeleteKeys: 无法删除多个API密钥\n\nchartHeading:\n  overview: 概述\n  #poweredBy: \"Powered by:\"\n\ncis:\n  addTest: 添加测试 ID\n  alertNeeded: Alerting must be enabled within the CIS chart questions.yaml. This requires that <a tabindex=\"0\" aria-label=\"Link to Rancher's Monitoring\" href=\"{link}\"> Rancher's Monitoring and Alerting app</a> is installed and the Receivers and Routes are <a target=\"_blank\" rel='noopener nofollow' href='https://rancher.com/docs/rancher/v2.x/en/monitoring-alerting/v2.5/configuration/#alertmanager-config'> configured to send out alerts.</a>\n  alertOnComplete: 扫描完成告警\n  alertOnFailure: 扫描失败告警\n  benchmarkVersion: Benchmark 版本\n  clusterProvider: 提供集群的厂商\n  cronSchedule:\n    label: 定时调度\n    placeholder: \"例如：0 * * * *\"\n  customConfigMap: 自定义 Benchmark 配置映射\n  deleteBenchmarkWarning: |-\n    {count, plural,\n      =1 { 任何使用该基准版本的配置文件将不再工作。 }\n      other { 任何使用这些基准版本的配置文件将不再工作。 }\n    }\n  deleteProfileWarning: |-\n    {count, plural,\n      =1 { 任何使用此配置文件的定时扫描将会失效。 }\n      other { 任何使用这些配置文件的定时扫描将会失效。 }\n    }\n  downloadAllReports: 下载所有保存的报告\n  downloadLatestReport: 下载最新报告\n  downloadReport: 下载报告\n  maxKubernetesVersion: 允许的最大 Kubernetes 版本\n  minKubernetesVersion: 允许的最小 Kubernetes 版本\n  noProfiles: 此集群类型没有有效的 ClusterScanProfiles 可供选择。\n  noReportFound: 未找到扫描报告\n  profile: 配置文件\n  retention: 保留数\n  reports: 报告\n  scan:\n    description: 描述\n    fail: 失败\n    lastScanTime: 最后扫描时间\n    notApplicable: 'N/A'\n    number: 序号\n    pass: 通过\n    remediation: 补救\n    scanDate: 扫描日期\n    scanReport: 扫描报告\n    skip: 跳过\n    total: 总共\n    warn: 警告\n  scheduling:\n    enable: 定时运行扫描\n    disable: 运行单次扫描\n  scoreWarning:\n    label: 扫描结果为 \"warn\" 状态\n    protip: 没有失败的扫描将被默认标记为 “通过”，即使一些测试生成 “warn” 输出。此行为可以通过从本节中选择 “fail” 选项来更改。\n  testID: Test ID\n  testsToSkip: 跳过测试\n  testsSkipped: 已跳过的测试\n\ncluster:\n  provider:\n      #aliyun: Alibaba ACK\n      #aliyunecs: Aliyun ECS\n      aws: Amazon AWS\n      #amazonec2: Amazon EC2\n      #amazoneks: Amazon EKS\n      #azure: Azure\n      #azureaks: Azure AKS\n      #baidu: Baidu CCE\n      #cloudca: Cloud.ca\n      custom: 自定义\n      #digitalocean: DigitalOcean\n      #docker: Docker\n      #exoscale: Exoscale\n      #googlegke: Google GKE\n      #huaweicce: Huawei CCE\n      import: 导入已有集群\n      #k3s: K3s\n      #kubeAdmin: KubeADM\n      #linode: Linode\n      local: Local\n      #minikube: Minikube\n      #oci: Oracle Cloud Infrastructure\n      #openstack: OpenStack\n      #oracleoke: Oracle OKE\n      #otc: Open Telekom Cloud\n      other: 其他\n      #packet: Packet\n      pinganyunecs: 平安云 ECS\n      #rackspace: RackSpace\n      #rancherkubernetesengine: RKE\n      #rke2: RKE Government\n      #rke: RKE\n      #rkeWindows: Windows\n      #softlayer: SoftLayer\n      #tencenttke: Tencent TKE\n      #upcloud: UpCloud\n      #vmwarevsphere: vSphere\n      #zstack: ZStack\n  providerGroup:\n    create-template: 使用模板创建集群\n    create-kontainer: 在托管的 Kubernetes 提供商中创建集群\n    create-machine: 在新建的节点上使用 RKE 创建集群\n    create-custom: 在现有的节点上使用 RKE 创建集群\n    register-kontainer: 在托管的 Kubernetes 提供商中注册一个现有的集群\n    register-custom: 导入 Kubernetes 集群\n  credential:\n    label: 云凭证\n    name:\n      label: 凭证名称\n      placeholder: 请为这个凭证输入一个名称\n    aws:\n      accessKey:\n        label: Access Key\n        placeholder: 请输入您的 AWS Access Key\n      secretKey:\n        label: SecretKey\n        placeholder: 请输入您的 AWS Secret Key\n      defaultRegion:\n        label: 默认区域\n        help: 创建群组时默认使用的区域。 也用于验证此凭证是否有效。\n    digitalocean:\n      accessToken:\n        #label: Access Token\n        placeholder: 请输入您的 DigitalOcean API Access Token\n        help: 从 DigitalOcean <a href=\"https://cloud.digitalocean.com/settings/api/tokens\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">Applications & API</a>中复制和粘贴个人访问令牌。\n  name:\n    label: 集群名称\n    placeholder: 请输入集群名称，该名称不能与其他集群名称相同\n  description:\n    label: 集群描述\n    placeholder: （选填项）请输入关于该集群的描述\n  import: 导入已有集群\n  kubernetesVersion:\n    label: Kubernetes 版本\n  machinePool:\n    nodeTotals:\n      label:\n        etcd: \"{count} 个etcd节点\"\n        controlPlane: \"{count} 个Control Plane节点\"\n        worker: \"{count} 个Worker节点\"\n      tooltip:\n        etcd: |-\n          {count, plural,\n            =0 { 一个集群至少需要一个etcd节点才能使用，请重新选择节点数量。 }\n            =1 { 只有1个etcd节点的集群是不具备容错能力的。 }\n            =2 { 集群的节点数应该是奇数。 具有2个etcd节点的集群是不具备容错能力的。 }\n            =3 {}\n            =4 { 集群内的节点数量应该为任意大于1的奇数，请重新选择节点数量。 }\n            =5 {}\n            =6 { 集群内的节点数量应该为任意大于1的奇数，请重新选择节点数量。 }\n            =7 {}\n            other { 我们不建议您在集群内创建多于7个节点。 }\n          }\n        controlPlane: |-\n          {count, plural,\n            =0 { 每个集群至少需要一个control plane节点才可以使用。 }\n            =1 { 只有一个control plane节点的集群是不具备容错能力的。 }\n            other {}\n          }\n        worker: |-\n          {count, plural,\n            =0 { 每个集群至少需要一个worker节点才可以使用 }\n            =1 { 只有一个worker节点的集群是不具备容错能力的。 }\n            other {}\n          }\n    name:\n      label: 节点池名称\n      placeholder: 默认情况下会随机生成一个节点池名称\n  #machineConfig:\n    #aws:\n      #sizeLabel: |-\n        #{apiName}: {cpu}, {memory, number} GiB Memory, {storageSize, plural,\n          #=0 {EBS-Only}\n          #other {{storageSize, number} GiB {storageType}}\n        #}\n    #digitalocean:\n      #sizeLabel: |-\n        #{plan, select,\n          #s {Basic: }\n          #g {General: }\n          #gd {General: }\n          #c {CPU: }\n          #m {Memory: }\n          #so {Storage: }\n          #standard {Standard: }\n          #other {}\n        #}{memoryGb} GB, {vcpus, plural,\n          #=1 {#vCPU}\n          #other {#vCPUs}\n        #}, {disk} GB Disk ({value})\n\nclusterIndexPage:\n  hardwareResourceGauge:\n    consumption: \"{suffix} {total} {units} 中的 {useful}\"\n    coresReserved: CPU 预留\n    coresUsed: CPU 使用\n    podsUsed: Pods 预留\n    ramReserved: Memory 预留\n    ramUsed: Memory 使用\n  header: 集群仪表盘\n  resourceGauge:\n    totalResources: 资源总额\n  sections:\n    events:\n      label: 事件\n      resource:\n        label: 事件详情\n      date:\n        label: 更新时间\n    clusterMetrics:\n      label: 集群指标\n    etcdMetrics:\n      label: Etcd 指标\n    k8sMetrics:\n      label: Kubernetes 组件指标\n    gatekeeper:\n      buttonText: 配置 OPA Gatekeeper\n      disabled: 未配置 OPA Gatekeeper\n      label:  违反 OPA Gatekeeper 的限制规定\n      noRows: 所有的 OPA Gatekeeper 限制都符合规定\n    nodes:\n      label: 节点不健康\n      noRows: 所有节点都处于健康状态\n\nconfigmap:\n  tabs:\n    data:\n      label: 数据\n      protip: 请在此处输入 UTF-8 文本数据\n    binaryData:\n      label: 二进制数据\n\ncontainers:\n  sortableTables:\n    noRows: 没有要显示的容器\n\n\nimages:\n  title: Images\n  sortableTables:\n    noRows: 没有要显示的镜像\n\nportForwarding:\n  sortableTables:\n    noRows: 没有可显示的端口转发规则\n\n\ncontainerResourceLimit:\n  cpuPlaceholder: 例如：1000\n  helpText: 请配置容器可以使用的默认资源配额\n  helpTextDetail: 容器可以使用的的默认资源配额\n  label: 容器默认资源限制\n  limitsCpu: CPU 限制\n  limitsMemory: 内存限制\n  memPlaceholder: 例如：128\n  requestsCpu: CPU 预留\n  requestsMemory: 内存预留\n\ncruResource:\n  backToForm: 返回表单编辑\n  backBody: 返回表单编辑不会保留对 YAML 做出的所有更改\n  cancelBody: 返回表单编辑不会保留对 YAML 做出的所有更改\n  confirmBack: \"确认\"\n  confirmCancel: \"确认\"\n  reviewForm: \"继续编辑 YAML\"\n  reviewYaml: \"继续编辑 YAML\"\n  previewYaml: 以 YAML 文件编辑\n\ndetailText:\n  collapse: 隐藏\n  binary: '<二进制数据：{n, number} bytes>'\n  empty: '<Empty>'\n  plusMore: |-\n    {n, plural,\n      =1 {+ 1 more char}\n      other {+ {n, number} 更多 Chars}\n    }\n\netcdInfoBanner:\n  hasLeader: \"Etcd有一个领导者\"\n  leaderChanges: \"领导者变化的次数\"\n  failedProposals: \"失败的proposal数量\"\n\nfleet:\n  cluster:\n    summary: 资源概要\n    nonReady: 非就绪包\n  fleetSummary:\n    state:\n      success: '就绪'\n      #info: 'Transitioning'\n      warning: '警告'\n      error: '错误'\n      unknown: '未知'\n  gitRepo:\n    tabs:\n      resources: 资源\n      unready: 未就绪\n    auth:\n      label: 认证\n    caBundle:\n      label: 证书\n      placeholder: \"粘贴一个或多个证书，以“-----BEGIN CERTIFICATE----” 作为开头。\"\n    paths:\n      label: 路径\n      placeholder: 例如：/directory/in/your/repo\n      addLabel: 添加路径\n      empty: 默认使用的是 repo 的根目录。 要使用一个或多个不同的目录，请在这里添加。\n    repo:\n      label: 代码库 URL 地址\n      placeholder: '例如：https://github.com/rancher/fleet-examples.git'\n    ref:\n      label: Watch\n      branch: 分支\n      revision: 修改\n      branchLabel: 分支名称\n      branchPlaceholder: 例如：master\n      revisionLabel: 标签或 Commit Hash\n      revisionPlaceholder: 例如：v1.0.0\n    serviceAccount:\n      label: Service Account 名称\n      placeholder: \"（选填项）在目标集群中使用Service Account\"\n    targetNamespace:\n      label: 目标命名空间\n      placeholder: \"（选填项）要求所有资源都在此命名空间内\"\n    target:\n      selectLabel: 目标类型\n      advanced: 高级选项\n      cluster: 集群\n      clusterGroup: 集群组\n      label: 部署到\n      labelLocal: 部署方式\n    targetDisplay:\n      advanced: 高级选项\n      cluster: \"集群\"\n      clusterGroup: \"组\"\n      all: 全部\n      none: None\n      local: 本地\n    tls:\n      label: TLS证书校验\n      verify: 需要提供有效的证书\n      specify: 指定接受的附加证书\n      skip: 接受任何证书（不安全）\n    workspace:\n      label: 工作空间\n  clusterGroup:\n    selector:\n      label: 集群选择器\n      matchesAll: 匹配到 {total, number} 个集群\n      matchesNone: 与现有的集群都不匹配\n      matchesSome: |-\n        {matched, plural,\n          =1 {与现有 {total, number} 个集群中的 1 个集群 \"{sample}\" 匹配}\n          other {现有 {total, number} 个集群，与其中的 {matched, number} 匹配，包括 \"{sample}\"}\n        }\nfooter:\n  docs: Rancher 官方文档\n  download: 下载 CLI\n  forums: 论坛\n  issue: 提交 GitHub Issue\n  slack: Slack 讨论群\n\ngatekeeperConstraint:\n  match:\n    title: 匹配\n  tab:\n    enforcementAction:\n      title: 执行动作\n    rules:\n      title: 规则\n      sub:\n        labelSelector:\n          addLabel: 添加\n          title: 标签选择器\n    namespaces:\n      sub:\n        excludedNamespaces: 排除命名空间\n        namespaces: 命名空间\n        namespaceSelector:\n          addNamespace: 添加命名空间\n          title: 命名空间选择器\n        scope:\n          title: 范围\n      title: 命名空间\n    parameters:\n      addParameter: 添加参数\n      editAsForm: 作为表格编辑\n      editAsYaml: 作为 YAML 编辑\n      title: 参数\n  template: 模板\n  violations:\n    title: 违反规定\n\ngatekeeperIndex:\n  #poweredBy: OPA Gatekeeper\n  unavailable: OPA Gatekeeper 不在 system-charts 应用商店中\n  violations: 违反规定\n\nglance:\n  created: 创建时间\n  cpu: CPU 使用量\n  memory: 内存\n  nodes:\n    total:\n      label: |-\n        {count, plural,\n          =1 { 节点数 }\n          other { 总节点 }\n        }\n  #pods: Pods\n  provider: 提供商\n  version: Kubernetes 版本\n\n  grafanaDashboard:\n  failedToLoad: 加载表格失败\n  reload: 重新加载\n  grafana: Grafana\n\ngraphOptions:\n  detail: 详情\n  summary: 概述\n  refresh: 刷新\n  range: 范围\n\nhpa:\n  detail:\n    currentMetrics:\n      header: 当前指标\n      noMetrics: 没有当前指标\n    metricHeader: '{source} 指标'\n  metricIdentifier:\n    name:\n      label: 指标名称\n      placeholder: 例如：packets-per-second\n    selector:\n      label: 添加 Selector\n  metricTarget:\n    averageVal:\n      label: 平均值\n    quantity:\n      label: 数量\n    type:\n      label: 类型\n    utilization:\n      label: 平均利用率\n    value:\n      label: 值\n  metrics:\n    headers:\n      metricName: 名称\n      objectKind: 对象类型\n      objectName: 对象名称\n      quantity: 数量\n      resource: 资源名称\n      targetName: 目标名称\n      value: 值\n    source: 数据源\n  objectReferance:\n    api:\n      label: 引用的API版本\n      placeholder: 例如：apps/v1beta1\n    kind:\n      label: 引用类型\n      placeholder: 例如：Deployment\n    name:\n      label: 引用名称\n      placeholder: 例如：php-apache\n  tabs:\n    labels: 标签\n    metrics: 指标\n    target: 目标\n    workload: 工作负载\n  types:\n    cpu: CPU\n    memory: 内存\n  warnings:\n    custom: 为了使用HPA的自定义指标，你需要部署自定义metric server，如prometheus适配器。\n    external: 为了使用HPA的外部指标，你需要部署外部metric server，如prometheus适配器。\n    noMetric: 为了使用HPA的资源指标，您需要部署metric server。\n    resource: 选定的目标参考在规格上没有正确的资源请求。否则，HPA指标将不会有任何影响。\n  workloadTab:\n    current: 当前的副本\n    last: 最后一个刻度时间\n    max: 最大副本数量\n    min: 最小副本数量\n    targetReference: 目标参考\n\nimport:\n  title: 导入 YAML\n  defaultNamespace:\n    label: 默认命名空间\n  success: |-\n    Applied {count, plural,\n    =1 {1 Resource}\n    other {#Resources}\n    }\n\ningress:\n  certificates:\n    addCertificate: 添加证书\n    addHost: 添加主机\n    certificate:\n      label: 证书 - 密钥名称\n      doesntExist: 所选证书不存在\n    defaultCertLabel: 默认 Ingress Controller 证书\n    headers:\n      certificate: 证书\n      hosts: 主机\n    host:\n      label: 主机\n      placeholder: 例如：example.com\n    label: 证书\n    removeHost: 移除\n  defaultBackend:\n    label: 默认后端\n    noServiceSelected: 没有配置默认后端\n    port:\n      label: 端口\n      placeholder: 例如 80 或 http\n    targetService:\n      label: 目标服务\n      doesntExist: 您选择的服务不存在\n    warning: \"警告：默认后端在整个集群中全局使用\"\n  rules:\n    addPath: 添加路径\n    addRule: 添加规则\n    headers:\n      pathType: 路径类型\n      path: 路径\n      port: 端口\n      target: 目标服务\n      certificates: 证书\n    hostname: 主机名\n    path:\n      label: 路径\n      placeholder: 例如：/foo\n    port:\n      label: 端口\n      placeholder: 例如：80 或 http\n    removePath: 删除路径\n    requestHost:\n      label: 请求主机\n      placeholder: 例如：example.com\n    target:\n      label: 目标服务\n      doesntExist: 您选择的服务不存在\n    title: 规则\n  rulesAndCertificates:\n    title: 规则和证书\n    defaultCertificate: 默认\n  target:\n    default: 默认\n\ninternalExternalIP:\n  none: 无\n\nistio:\n  links:\n    kiali:\n      label: Kiali\n      description: 可视化服务网状结构中的服务以及它们是如何连接的。要想让 Kiali 显示数据，需要安装 Prometheus。如果您需要监控解决方案，请安装 <a rel=\"noopener noreferrer nofollow\" href=\"{link}\"> Rancher 的监控</a>。\n    jaeger:\n      label: Jaeger\n      description: 监控并排除基于微服务的分布式系统的故障。\n    disabled: '没有安装{app}应用'\n  cni: 启用 CNI\n  customOverlayFile:\n    label: 自定义覆盖文件\n    tip: '<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href=\"https://istio.io/latest/docs/setup/install/istioctl/#customizing-the-configuration\">覆盖文件</a>允许在基本的 Rancher Istio 安装之上进行额外的配置。您可以利用<a href=\"https://istio.io/latest/docs/reference/config/istio.operator.v1alpha1/\" target=\"_blank\" rel=\"noopener noreferrer nofollow\" >IstioOperator API</a>对所有组件进行更改和添加，并通过此覆盖 YAML 文件应用这些更改。'\n  description: 'Rancher Istio Helm Chart 为您安装了一个最小的 Istio 配置，以便您开始与您的应用程序集成。\n  如果您想获得有关 Istio 的更多信息，请访问 <a target=\"_blank\" href=\"https://istio.io/latest/docs/concepts/what-is-istio\" rel=\"noopener nofollow\">https://istio.io/latest/docs/concepts/what-is-istio/</a>。'\n  egressGateway: 启用 Egress 网关\n  ingressGateway: 启用 Ingress 网关\n  istiodRemote: 启用 istiodRemote\n  kiali: 启用 Kiali\n  pilot: 启用 Pilot\n  policy: 启用 Policy\n  poweredBy: 由<a target=\"_blank\" rel=\"noopener noreferrer nofollow\" href='https://istio.io/latest/'>Istio</a>支持\n  telemetry: 启用遥测\n  titles:\n    components: 组件\n    customAnswers: 自定义回复\n    advanced: 高级选项\n    description: 描述\n  tracing: 启用 Jaeger 跟踪 (limited)\n  v1Warning: 请在安装这个版本之前卸载 <code>istio-system</code> 命名空间中的当前 Istio 版本。\n\nlabels:\n  addLabel: 添加\n  addSetLabel: 添加或配置标签\n  addAnnotation: 添加\n  labels:\n    title: 标签\n  annotations:\n    title: 注释\n\nlanding:\n  clusters: 集群\n# taken from Ember: https://github.com/rancher/ui/blob/master/app/components/modal-home/template.hbs\n  releaseNotes:\n    '<ul class=\"list-unstyled\">\n      <li class=\"mb-10\">\n        <b>Cluster Explorer:</b> 新的仪表盘提供了对Rancher管理的集群的更深入理解。\n        <ul>\n          <li>管理所有Kubernetes集群资源，包括来自Kubernetes运营商生态系统的定制资源</li>\n          <li>从我们新的Apps &amp; Marketplace部署和管理Helm Chart。</li>\n          <li>在一个新的类似IDE的查看器中查看日志并与kubectl shell互动</li>\n        </ul>\n      </li>\n      <li class=\"mb-10\"><b>由Prometheus提供的监控和警报：</b>允许管理定制的Grafana仪表盘，并为AlertManager提供定制。</li>\n      <li class=\"mb-10\"><b>由Banzai Cloud提供日志：</b> 自定义FluentBit和Fluentd的配置，并将日志运送到远程数据存储。</li>\n      <li class=\"mb-10\"><b>由kube-bench提供的CIS扫描：</b> 扩展支持为EKS和GKE平台定制的CIS扫描，并对任何Kubernetes发行版进行通用扫描</li>\n      <li class=\"mb-10\"><b>Istio 1.7+：</b> 允许用户部署多个Ingress和Egress网关</li>\n      <li class=\"mb-10\">\n        <b>Rancher 由Fleet提供的持续交付：</b> Fleet是一个Rancher内置的部署工具，用于在多个集群中从Git源码库交付应用程序和配置。\n        <ul>\n          <li>部署由manifests、kustomize或Helm定义的任何Kubernetes资源</li>\n          <li>使用staged checkout和pull-based的更新模式将部署扩展到任何数量的集群中</li>\n          <li>将集群组织成组，以便更容易管理</li>\n          <li>将Git源存储库映射到目标集群组上</li>\n        </ul>\n      </li>\n      <li class=\"mb-10\">\n        <b>EKS生命周期管理功能增强</b>\n        <ul>\n          <li>集群创建已得到加强，支持管理节点组、私人访问和控制平面记录</li>\n          <li>注册现有的EKS集群允许管理升级和配置</li>\n        </ul>\n      </li>\n      <li>\n        <b>Rancher Server备份：</b>\n        <ul>\n          <li>在不能访问etcd数据库的情况下备份Rancher服务器</li>\n          <li>将数据恢复到任何Kubernetes集群中</li>\n        </ul>\n      </li>\n    </ul>'\n  seeWhatsNew: 了解更多关于该版本的改进和新功能。\n  whatsNewLink: \"2.5的新内容\"\n  learnMore: 了解更多\n  migration:\n    title: 迁移帮助\n    body: 阅读集群管理器用户的迁移指南--你需要利用扩展的集群资源管理器的一切优势\n  community:\n    title: 社区支持\n    docs: Rancher文档\n    forums: 论坛\n  commercial:\n    title: 付费支持\n    body: 如需了解付费支持，请单击<a href=\"https://rancher.com/support-maintenance-terms/\" target='_blank' rel='noopener nofollow noreferrer'>这里</a>。\n  landingPrefs:\n    title: 你想在登录时看到什么？\n    options:\n      thisScreen: 当前页面\n      lastVisited: 上一次登录时最后访问的页面\n      custom: 自定义首页\n      defaultOverview: 默认集群（{cluster}）的概览页面\n      appsAndMarketplace: 应用市场页面\n      fleet: Fleet页面\n  welcomeToRancher: 欢迎使用Rancher！\n\nlogging:\n  clusterFlow:\n    noOutputsBanner: 在选定的命名空间中没有集群输出\n  flow:\n    clusterOutputs:\n      doesntExistTooltip: 该集群输出不存在\n      label: 集群输出\n    matches:\n      label: 匹配\n      addSelect: 添加包含规则\n      addExclude: 添加排除规则\n    filters:\n      label: 过滤\n    outputs:\n      doesntExistTooltip: 该集群输出不存在\n      label: 输出\n  install:\n    k3sContainerEngine: K3S 容器引擎\n    enableAdditionalLoggingSources: 启用增强的云日志收集服务\n    dockerRootDirectory: Docker根目录\n  elasticsearch:\n    host: 主机\n    scheme: 主题\n    port: 端口\n    indexName: 索引名称\n    user: 用户名\n    password: 密码\n    caFile:\n      label: CA 证书文件\n    clientCert:\n      label: 客户端证书\n      placeholder: 粘贴客户端证书\n    clientKey:\n      #label: Client Key\n      #placeholder: 粘贴 client key\n    #clientKeyPass: Client Key Pass\n  kafka:\n    #brokers: Brokers\n    defaultTopic: 默认 Topic\n    saslOverSsl: 通过SSL实现SASL\n    scramMechanism: Scram 机制\n    username: 用户名\n    password: 密码\n    sslCaCert:\n      label: SSL CA 证书\n      placeholder: 请输入 CA 证书\n    sslClientCert:\n      label: SSL 客户端证书\n      placeholder: 请把客户端证书粘贴在 CA 证书内\n    sslClientCertChain:\n      label: SSL 客户端证书链\n      placeholder: 请输入 SSL 客户端证书链\n    sslClientCertKey: SSL 客户端证书密钥\n  loki:\n    #url: URL\n    tenant: 租户\n    username: 用户名\n    password: 密码\n    configureKubernetesLabels: 以类似 Prometheus 的格式配置 Kubernetes 元数据\n    extractKubernetesLabels: 提取 Kubernetes 标签作为 Loki 标签\n    dropSingleKey: 如果一条记录只有 1 个键，那么只需将日志行设置为该值并丢弃该键\n    caCert: CA 证书\n    cert: 证书\n    key: 密钥\n  awsElasticsearch:\n    #url: URL\n    #keyId: Key Id\n    #secretKey: Secret Key\n  #azurestorage:\n    #storageAccount: Storage Account\n    #accessKey:  Access Key\n    #container: Container\n    #path: 路径\n    #storeAs: Store As\n  #cloudwatch:\n    #keyId: Key Id\n    #secretKey: Secret Key\n    #endpoint: Endpoint\n    #region: Region\n  datadog:\n    #apiKey: API Key\n    #useSSL: Use SSL\n    useCompression: 使用压缩\n    #host: Host\n  file:\n    path: 路径\n  gcs:\n    project: 项目\n    credentialsJson: 凭证\n    bucket: 桶名称\n    path: 路径\n    overwriteExistingPath: 覆盖现有的路径\n  #kinesisStream:\n    #streamName: Stream Name\n    #keyId: Key Id\n    #secretKey: Secret Key\n  #logdna:\n    #apiKey: API Key\n    #hostname: Hostname\n    #app: App\n  #logz:\n    #url: URL\n    #port: Port\n    token: API 令牌\n    enableCompression: 启用压缩\n  newrelic:\n    apiKey: API 密钥\n    #licenseKey: License Key\n    #baseURI: Base URI\n  sumologic:\n    endpoint: 端点\n    sourceName: 源名称\n  syslog:\n    host: syslog 主机地址\n    port: 端口\n    transport: 传输\n    insecure: 不安全的\n    trustedCaPath: 受信 CA 路径\n    format:\n      title: 格式\n      type: 类型\n      addNewLine: 添加新行\n      #messageKey: Message Key\n    buffer:\n      #title: Buffer\n      tags: 标签\n      chunkLimitSize: 存储块大小限制\n      chunkLimitRecords: 块限制 chunkLimitRecords\n      totalLimitSize: 总限制大小\n      flushInterval: 冲洗时间间隔\n      #timekey: Timekey\n      #timekeyWait: Timekey Wait\n      #timekeyUseUTC: Timekey 使用 UTC\n  s3:\n    #keyId: Key Id\n    #secretKey: Secret Key\n    #endpoint: Endpoint\n    #bucket: Bucket\n    path: 路径\n    overwriteExistingPath: 覆盖现有的路径\n  output:\n    selectOutputs: 选择输出\n    selectBanner: 选择以配置输出\n    sections:\n      target: 目标\n      access: 访问\n      certificate: SSL 证书\n      labels: 标签\n  #outputProviders:\n    #elasticsearch: Elasticsearch\n    #splunkHec: Splunk\n    #kafka: Kafka\n    #forward: Fluentd\n    #loki: Loki\n    #awsElasticsearch: Amazon Elasticsearch\n    #azurestorage: Azure Storage\n    #cloudwatch: Cloudwatch\n    #datadog: Datadog\n    #file: File\n    #gcs: GCS\n    #kinesisStream: Kinesis Stream\n    #logdna: LogDNA\n    #logz: LogZ\n    #newrelic: New Relic\n    #sumologic: SumoLogic\n    #syslog: Syslog\n    #s3: S3\n    unknown: 未知类型\n  overview:\n    #poweredBy: Banzai Cloud\n    clusterLevel: 集群级别\n    namespaceLevel: 命名空间级别\n  provider: 提供商\n  splunk:\n    host: splunk 主机\n    port: 端口\n    protocol: 协议\n    #index: Index\n    token: 令牌\n    insecureSsl: 不安全的SSL\n    #indexName: Index Name\n    #source: Source\n    caFile: CA 文件\n    caPath: CA 路径（目录）\n    clientCert: 客户端证书\n    clientKey: 客户端密钥\n  forward:\n    host: 主机\n    port: 端口\n    sharedKey: 共享密钥\n    username: 用户名\n    password: 密码\n    clientCertPath: 客户端证书路径\n    clientPrivateKeyPath: 客户端私钥路径\n    clientPrivateKeyPassphrase: 客户端私钥密码\n\nlonghorn:\n  overview:\n    title: 概述\n    subtitle: \"由<a href='https://github.com/longhorn' target='_blank' rel='noopener nofollow noreferrer' >Longhorn</a>提供支持\"\n    linkedList:\n      longhorn:\n        #label: 'Longhorn'\n        description: '通过 UI 管理存储系统'\n        na: 资源不可用\n\nlogin:\n  howdy: 您好！\n  welcome: 欢迎使用 {vendor}\n  loggedOut: 您已登出当前账号。\n  loginAgain: 请重新登录。\n  error: 登录时发生错误，请重试。\n  useLocal: 使用Local User账户登录\n  loginWithProvider: 使用 {provider} 登录\n  username: 用户名\n  password: 密码\n  loggingIn: 登录中...\n  loggedIn: 已登录\n  loginWithLocal: 使用Local User账户登录\n  useProvider: 使用 {provider} 登录\n\nmonitoring:\n  accessModes:\n    many: 多次读写\n    once: 一次读写\n    readOnlyMany: 多次只读\n  aggregateDefaultRoles:\n    label: 聚合为默认 Kubernetes 角色\n    tip: '将标签添加到监控图部署的ClusterRoles上，以<a target=\"_blank\" rel=\"noopener nofollow noreferrer\" href=\"https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles\">聚合到相应的默认k8s管理、编辑和查看ClusterRoles。</a>。'\n  alerting:\n    config:\n      label: 配置告警管理\n    enable:\n      label: 部署 Alertmanager\n    secrets:\n      additional:\n        info: \"密文应挂载到容器路径 <pre class='inline-block m-0'>/etc/alertmanager/secrets/</pre>。\"\n        label: 附加密文\n      existing: 选择现有的配置密文\n      info: |\n        <span class=\"text-bold\">创建默认配置</span>。在部署这个 chart 时，将在<pre class='inline-block m-0'>cattle-monitoring-system</pre> 命名空间中创建一个包含 Alertmanager 配置的密钥，名称为<pre class='inline-block m-0'>alertmanager-rancher-monitoring-alertmanager</pre>。默认情况下，在卸载或升级此图表时，此 Secret 将永远不会被修改。<br/>\n        一旦您部署了这个 chart，您应该通过用户界面编辑密钥，以便添加您的自定义通知配置，这些配置将被 Alertmanager 用于发送警报。<br /> <br />\n        <span class=\"text-bold\">选择一个现有的配置密钥</span>：您必须指定一个存在于<pre class='inline-block m-0'>cattle-monitoring-system</pre>命名空间中的密钥。如果命名空间不存在，您将无法选择一个现有的密钥。\n      label: Alertmanager 密文\n      new: 创建默认配置\n      radio:\n        label: 配置密文\n    templates:\n      keyLabel: 文件名称\n      label: 模板文件\n      valueLabel: YAML 模板\n    title: 配置 Alertmanager\n  clusterType:\n    label: 集群类型\n    placeholder: 选择集群类型\n  createDefaultRoles:\n    label: 创建默认 Monitoring 集群角色\n    tip: '创建 <code>monitoring-admin</code>，<code>monitoring-edit</code>，和 <code> monitor-view</code> ClusterRoles，可以被分配给用户，为部署监控 Chart 安装 CRDs 提供权限。'\n  etcdNodeDirectory:\n    label: ETCD 节点证书目录\n    tooltip: '对于使用 RancherOS 作为 etcd 节点的集群，这个选项应该设置为 <pre class=\"line-block m-0\" >/opt/rke/etc/kubernetes/ssl</pre>。不支持需要指定多个证书目录的混合环境(例如，由 RancherOS 和 Ubuntu 主机组成的 etcd 平面)。'\n  grafana:\n    storage:\n      annotations: PVC 注释\n      className: 存储类名称\n      existingClaim: 使用已有的 Claim\n      #finalizers: PVC Finalizers\n      label: Grafana 的持久存储\n      mode: 访问模式\n      selector: 选择器\n      size: 大小\n      subpath: 使用子路径\n      type: 持久存储类型\n      types:\n        existing: 使用已有的 PVC 启用 Grafana\n        statefulset: 使用 StatefulSet 模板启用 Grafana\n        template: 使用 PVC 模板启用 Grafana\n      volumeMode: 存储卷模式\n      volumeName: 存储卷名称\n    title: 配置 Grafana\n  overview:\n    alertsList:\n      ends:\n        label: 停止于\n      label: 已启用的告警\n      message:\n        label: 信息\n      severity:\n        label: 严重程度\n      start:\n        label: 开始于\n    linkedList:\n      alertManager:\n        description: 已启用的告警\n        #label: Alertmanager\n      grafana:\n        description: Metrics 仪表盘\n        #label: Grafana\n      na: 资源不可用\n      prometheusPromQl:\n        description: PromQL 图表\n        label: Prometheus 图表\n      prometheusRules:\n        description: 配置规则\n        label: Prometheus 规则\n      prometheusTargets:\n        description: 配置目标\n        #label: Prometheus Targets\n    subtitle: '由<a href=''https://github.com/coreos/prometheus-operator'' target=''_blank'' rel=''noopener nofollow'' >Prometheus</a>提供支持'\n    title: 仪表盘\n    v1Warning: '当前监控由 Rancher UI 部署，如果你想在仪表盘中启用新的监控，请先在 Rancher UI 中禁用原来的监控。'\n  prometheus:\n    config:\n      #adminApi: Admin API\n      evaluation: 评估时间间隔\n      ignoreNamespaceSelectors:\n        help: '忽略命名空间选择器允许集群管理员限制团队查看他们有权监视的命名空间之外的资源，但这会破坏应用程序的功能，这些应用程序依赖于设置跨多个命名空间捕获目标监控数据，比如 Istio。'\n        label: 命名空间选择器\n        radio:\n          enforced: '使用: 监控可以基于与命名空间选择器字段匹配的命名空间访问资源'\n          ignored: '忽略: 监控只能访问它们所在命名空间中的资源'\n      limits:\n        cpu: CPU 限制\n        memory: Memory 限制\n      requests:\n        cpu: CPU 预留\n        memory: Memory 预留\n      resourceLimits: 资源限制\n      retention: 预留\n      retentionSize: 预留大小\n      scrape: 刮擦间隔（prometheus 获取数据间隔）\n    storage:\n      className: 存储类名称\n      label: Prometheus 持久存储\n      mode: 访问模式\n      selector: 选择器\n      selectorWarning: '如果你正在使用一个动态配置器(例如 Longhorn)，不应该指定选择器，因为带有非空选择器的PVC不能动态配置PV。'\n      size: 大小\n      volumeMode: Volume 模式\n      volumeName: Volume 名称\n    title: 配置 Prometheus\n    warningInstalled: |\n      '警告：目前已经部署了Prometheus Operators。目前不支持在一个集群上部署多个Prometheus Operators。在尝试安装此chart之前，请从该集群中移除所有其他的普罗米修斯Operators部署。\n      如果您是从启用了监控功能的旧版Rancher迁移过来的，请在尝试安装此chart之前完全禁用此集群上的监控功能。\n  receiver:\n    fields:\n      name: 名称\n    tls:\n      #label: SSL\n      caFilePath:\n        label: CA 文件路径\n        placeholder: 例如：./ca-file.csr\n      certFilePath:\n        label: 证书文件路径\n        placeholder: 例如：./cert-file.crt\n      keyFilePath:\n        label: 密钥文件路径\n        placeholder: 例如：./key-file.pfx\n      secretsBanner: 当部署监控图表时，必须在<pre class=\"inline-block m-0 p-0 vertical-middle\">alertmanager.alertmanagerSpec.secrets</pre>中引用以下文件路径。\n\n  route:\n    fields:\n      groupBy: Group By\n      groupInterval: 组间隔\n      groupWait: 组等待\n      receiver: 接收者\n      repeatInterval: 重复间隔\n  tabs:\n    alerting: 告警\n    general: 总体\n    #grafana: Grafana\n    #prometheus: Prometheus\n  v1Warning: '当前监控由 Rancher UI 部署，如果你想在仪表盘中启用新的监控，请先在 Rancher UI 中禁用原来的监控。'\n  volume:\n    modes:\n      block: 块\n      file: 文件系统\n\nmonitoringReceiver:\n  addButton: 添加 {type}\n  custom:\n    label: 自定义\n    title: 自定义参数\n    info: 这里提供的YAML将直接附加到Alertmanager的接收器的配置密钥中。\n  email:\n    label: 电子邮箱\n    title: 电子邮箱参数\n  opsgenie:\n    #label: Opsgenie\n    title: Opsgenie参数\n  pagerduty:\n    #label: PagerDuty\n    title: PagerDuty参数\n    info: \"你可以找到更多关于为PagerDuty创建集成密钥的信息<a href='https://www.pagerduty.com/docs/guides/prometheus-integration-guide/' target='_blank' rel='noopener nofollow' class='flex-right'>这里</a>。\"\n  slack:\n    label: Slack\n    title: Slack参数\n    info: \"您可以在<a href='https://rancher.slack.com/apps/A0F7XDUAZ-incoming-webhooks' target='_blank' rel='noopener noreferrer nofollow'>这里</a>找到有关为Slack创建传入Webhooks的其他信息。\"\n  webhook:\n    #label: Webhook\n    title: Webhook参数\n    urlTooltip: 对于一些webhooks来说，这是一个指向DNS服务的url\n    modifyNamespace: 如果<pre class=\"inline-block m-0 p-0 vertical-middle\">rancher-alerting-drivers</pre>被安装在一个非默认的命名空间中，你需要更新下面网址中的命名空间。\n    banner: 要使用Microsoft Teams或阿里巴巴云短信，你需要先安装<pre class=\"inline-block m-0 p-0 vertical-middle\">rancher-alerting-drivers</pre>。\n    add:\n      generic: 通用\n      #msTeams: Microsoft Teams\n      alibabaCloudSms: 阿里巴巴云短信\n  auth:\n    label: 认证\n    authType: 认证类型\n    username: 用户名\n    password: 密码\n    none:\n      label: 无\n    bearerToken:\n      #label: Bearer Token\n      placeholder: 例如：secret-token\n    #basicAuth:\n      #label: Basic Auth\n    bearerTokenFile:\n      #label: Bearer Token File\n      placeholder: 例如：./user_token\n  shared:\n    proxyUrl:\n      label: 代理URL\n      placeholder: 例如：http://my-proxy/\n    sendResolved:\n      label: 启用发送已解决的警报\n\nmonitoringRoute:\n  groups:\n    label: 分组\n  info: 这是 Alertmanager 使用的默认通知，作为与任何其他路由不匹配的警报的默认目的地。此通知必须存在，不能删除。\n  interval:\n    label: 组间隔\n  matching:\n    info: 根路由必须匹配所有内容，因此无法配置匹配。\n    label: 匹配\n  receiver:\n    label: 接收者\n  regex:\n    label: 匹配正则表达式\n  repeatInterval:\n    label: 重复间隔\n  wait:\n    label: 组等待时长\n\nnameNsDescription:\n  name:\n    label: 名称\n    placeholder: '请输入名称'\n  namespace:\n    label: 命名空间\n    #placeholder:\n  workspace:\n    label: 工作空间\n    #placeholder:\n  description:\n    label: 描述\n    placeholder: 请输入一些能更好地描述该资源的文字\n\nnamespace:\n  containerResourceLimit: 容器资源限制\n  project:\n    label: 项目\n  resources: 资源\n  enableAutoInjection: 启用Istio自动注入\n  disableAutoInjection: 禁用Istio自动注入\n\nnamespaceFilter:\n  selected:\n    label: \"{total} 项目选择\"\n\nnamespaceList:\n  selectLabel: 命名空间\n  addLabel: 添加命名空间\n\nnode:\n  detail:\n    detailTop:\n      containerRuntime: 容器运行时\n      internalIP: 内部IP地址\n      externalIP: 外部IP地址\n      #os: OS\n      version: 版本\n    glance:\n      consumptionGauge:\n        used: 已使用\n        amount: \"已使用{total} {unit}中的{used}\"\n        cpu: CPU\n        memory: 内存\n        pods: PODS\n      diskPressure: 磁盘压力\n      kubelet: kubelet\n      memoryPressure: 内存压力\n      pidPressure: PID 压力\n    tab:\n      conditions: 状态\n      images: 镜像\n      info:\n        label: 信息\n        key:\n          architecture: 架构\n          bootID: Boot ID\n          containerRuntimeVersion: Container Runtime 版本\n          kernelVersion: Kernel 版本\n          kubeProxyVersion: Kube Proxy 版本\n          kubeletVersion: Kubelet 版本\n          machineID: 机器 ID\n          operatingSystem: 操作系统\n          osImage: 镜像\n          systemUUID: System UUID\n      pods: Pods\n      taints: 污点\n\npersistentVolume:\n  pluginConfiguration:\n    label: 插件配置信息\n  customize:\n    label: 自定义\n    affinity:\n      #label: Node Selectors\n      addLabel: 添加 Node Selector\n    assignToStorageClass:\n      label: 分配给存储类\n    mountOptions:\n      label: 挂载选项\n      addLabel: 添加选项\n    accessModes:\n      label: 访问模式\n      readWriteOnce: 单节点读写\n      readOnlyMany: 多节点只读\n      readWriteMany: 多节点读写\n  shared:\n    partition:\n      label: 分区\n      placeholder: 例如：1; 0\n    readOnly:\n      label: 只读\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    secretName:\n      label: 密钥名称\n      placeholder: 例如：secret\n    secretNamespace:\n      label: 密钥命名空间\n      placeholder: 例如：default\n    monitors:\n      add: 添加监控\n  vsphereVolume:\n    #label: VMWare vSphere 卷\n    volumePath:\n      label: 卷路径\n      placeholder: 例如：/\n    storagePolicyName:\n      label: 存储策略名称\n      placeholder: 例如：sp\n    storagePolicyId:\n      label: 存储策略ID\n      placeholder: 例如：sp1\n  csi:\n    label: CSI（不支持）\n    driver:\n      label: 驱动\n      placeholder: 例如：driver.longhorn.io\n    volumeHandle:\n      #label: Volume Handle\n      placeholder: 例如：pvc-xxxx\n    volumeAttributes:\n      add: 添加卷参数\n    nodePublishSecretName:\n      #label: Node Publish Secret Name\n      placeholder: 例如：secret\n    nodePublishSecretNamespace:\n      #label: Node Publish Secret Namespace\n      placeholder: 例如：default\n    nodeStageSecretName:\n      #label: Node Stage Secret Name\n      placeholder: 例如：secret\n    nodeStageSecretNamespace:\n      #label: Node Stage Secret Namespace\n      placeholder: 例如：default\n    controllerExpandSecretName:\n      #label: Controller Expand Secret Name\n      placeholder: 例如：secret\n    controllerExpandSecretNamespace:\n      #label: Controller Expand Secret Namespace\n      placeholder: 例如：default\n    controllerPublishSecretName:\n      #label: Controller Publish Secret Name\n      placeholder: 例如：secret\n    controllerPublishSecretNamespace:\n      #label: Controller Publish Secret Namespace\n      placeholder: 例如：default\n  cephfs:\n    label: Ceph Filesystem（不支持）\n    path:\n      label: 路径\n      placeholder: 例如：/var\n    user:\n      label: 用户\n      placeholder: 例如：root\n    secretFile:\n      label: 密钥文件\n      placeholder: 例如：secret\n  rbd:\n    label: Ceph RBD（不支持）\n    user:\n      label: 用户\n      placeholder: 例如：root\n    keyRing:\n      #label: Key Ring\n      placeholder: 例如：/etc/ceph/keyring\n    pool:\n      #label: Pool\n      placeholder: 例如：rbd\n    image:\n      label: 镜像\n      placeholder: 例如：image\n  fc:\n    label: Fibre Channel（不支持）\n    targetWWNS:\n      add: 添加模板WWN\n    wwids:\n      add: 添加 WWID\n    lun:\n      #label: Lun\n      placeholder: 例如：2\n  flexVolume:\n    label: Flex Volume（不支持）\n    driver:\n      label: 驱动\n      placeholder: 例如：driver\n    options:\n      add: 添加选项\n  flocker:\n    label: Flocker（不支持）\n    datasetName:\n      label: 数据集名称\n      placeholder: 例如：dataset\n    datasetUUID:\n      label: 数据集 UUID\n      placeholder: 例如：uuid\n  glusterfs:\n    label: Gluster Volume（不支持）\n    endpoints:\n      label: Endpoints\n      placeholder: 例如：glusterfs-cluster\n    path:\n      label: 路径\n      placeholder: 例如：kube-vol\n  iscsi:\n    label: iSCSI Target（不支持）\n    initiatorName:\n      #label: Initiator Name\n      #placeholder: iqn.1994-05.com.redhat:1df7a24fcb92\n    iscsiInterface:\n      #label: iSCSI Interface\n      placeholder: 例如：interface\n    chapAuthDiscovery:\n      #label: Chap Auth Discovery\n    chapAuthSession:\n      #label: Chap Auth Session\n    iqn:\n      #label: IQN\n      #placeholder: iqn.2001-04.com.example:storage.kube.sys1.xyz\n    lun:\n      #label: Lun\n      placeholder: 例如：2\n    targetPortal:\n      label: 模板Portal\n      placeholder: 例如：portal\n    portals:\n      add: 添加Portal\n  cinder:\n    label: Openstack Cinder Volume（不支持）\n    volumeId:\n      #label: Volume ID\n      placeholder: 例如：vol\n  quobyte:\n    label: Quobyte Volume（不支持）\n    volume:\n      label: Volume\n      placeholder: 例如：vol\n    user:\n      label: 用户名\n      placeholder: 例如：root\n    group:\n      label: 用户组\n      placeholder: 例如：abc\n    registry:\n      label: 仓库\n      placeholder: 例如：abc\n  photonPersistentDisk:\n    label: Photon Volume（不支持）\n    pdId:\n      #label: PD ID\n      placeholder: 例如：abc\n  portworxVolume:\n    label: Portworx Volume（不支持）\n    volumeId:\n      #label: Volume ID\n      placeholder: 例如：abc\n  scaleIO:\n    label: ScaleIO Volume（不支持）\n    volumeName:\n      #label: Volume Name\n      placeholder: 例如：vol-0\n    gateway:\n      #label: Gateway\n      placeholder: 例如：https://localhost:443/api\n    protectionDomain:\n      #label: Protection Domain\n      placeholder: 例如：pd01\n    storageMode:\n      #label: Storage Mode\n      placeholder: 例如：ThinProvisioned\n    storagePool:\n      label: 存储池\n      placeholder: 例如：sp01\n    system:\n      label: 系统\n      placeholder: 例如：scaleio\n    sslEnabled:\n      label: 启用SSL\n  storageos:\n    label: StorageOS（不支持）\n    volumeName:\n      label: 卷名称\n      placeholder: 例如：vol\n    volumeNamespace:\n      label: 卷命名空间\n      placeholder: 例如：default\n  nfs:\n    #label: NFS Share\n    path:\n      label: 路径\n      placeholder: 例如：/var\n    server:\n      label: Server IP 地址\n      placeholder: 例如：10.244.1.4\n  longhorn:\n    #label: Longhorn\n    volumeHandle:\n      #label: Volume Handle\n      placeholder: 例如：pvc-xxxx\n    options:\n      label: 选项\n      addLabel: 添加\n  local:\n    label: 本地\n    path:\n      label: 路径\n      placeholder: 例如：/mnt/disks/ssd1\n  hostPath:\n    label: 主机路径\n    pathOnTheNode:\n      label: 节点上的路径\n      placeholder: 例如：/mnt/disks/ssd1\n    mustBe:\n      label: 节点上的路径必须是：\n      anything: '任意路径：不需要检查目标路径'\n      directory: 一个文件夹，如果该文件夹不存在，则自动创建一个文件夹\n      file: 一个文件，如果该文件不存在，则自动创建一个文件\n      existingDirectory: 一个已有的文件夹\n      existingFile: 一个已有的文件\n      existingSocket: 一个已有的socket\n      existingCharacter: 一个已有的character device\n      existingBlock: 一个已有的block device\n  gcePersistentDisk:\n    #label: Google Persistent Disk\n    persistentDiskName:\n      label: Disk 名称\n      placeholder: 例如：abc\n  awsElasticBlockStore:\n    #label: Amazon EBS Disk\n    volumeId:\n      label: 卷ID\n      placeholder: 例如：volume1\n  azureFile:\n    #label: Azure Filesystem\n    shareName:\n      label: Share名称\n      placeholder: 例如：abc\n  azureDisk:\n    #label: Azure Disk\n    diskName:\n      label: Disk名称\n      placeholder: 例如：kubernetes-pvc\n    diskURI:\n      #label: Disk URI\n      placeholder: 例如：https://example.com/disk\n    kind:\n      label: 类型\n      dedicated: 专用\n      managed: 管理\n      shared: 共享\n    cachingMode:\n      label: 缓存模式\n      none: 无\n      readOnly: 只读\n      readWrite: 读写\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    readOnly:\n      label: 只读\n\npersistentVolumeClaim:\n  accessModes: 访问模式\n  capacity: 容量\n  storageClass: 存储类\n  useDefault: 使用默认存储类\n  volumes: 持久卷\n  volumeName: 持久卷名称\n  source:\n    label: 资源\n    options:\n      new: 使用存储类创建新的持久卷(PV)\n      existing: 使用已有的持久卷(PV)\n  volumeClaim:\n    label: 卷声明\n    storageClass: 存储类\n    requestStorage: 需要的存储大小\n    persistentVolume: 持久卷\n  customize:\n    label: 自定义\n    accessModes:\n      readWriteOnce: 单节点读写\n      readOnlyMany: 多节点只读\n      readWriteMany: 多节点读写\n  status:\n    label: 状态\nprefs:\n  title: 用户偏好设置\n  theme:\n    label: 主题\n    light: 浅色\n    auto: 自动\n    dark: 深色\n    autoDetail: 选择自动设置，将会在晚 6 点到次日早 6 点间自动切换到黑色主题。\n  landing:\n    label: 默认登录页面\n    vue: 仪表盘\n    ember: Rancher UI\n  formatting: 格式\n  dateFormat:\n    label: 日期格式\n  timeFormat:\n    label: 时间格式\n  perPage:\n    label: 每页行数\n    value: |-\n      {count, number}\n  keymap:\n    label: YAML 编辑器选择\n    sublime: '默认'\n    emacs: 'Emacs'\n    vim: 'Vim'\n  advanced: 高级选项\n  dev:\n    label: 启用开发工具\n  hideDesc:\n    label: 隐藏所有类型说明框\n  helm:\n    'true': 包括预发布的版本\n    'false': 只显示正式发布的版本\n    #label: Helm Charts\n\nprincipal:\n  loading: 加载中&hellip;\n  error: 无法获取信息\n  name: 名称\n  loginName: 用户名\n  type: 类型\n\nprobe:\n  checkInterval:\n    label: 检查间隔\n    placeholder: '默认值是10秒'\n  command:\n    label: 运行命令\n    placeholder: 例如：cat /tmp/health\n  failureThreshold:\n    label: 失败阈值\n    placeholder: '默认值是3次'\n  httpGet:\n    headers:\n      label: 请求头\n    path:\n      label: 请求路径(Path)\n      placeholder: 例如：/healthz\n    port:\n      label: 检查端口\n      placeholder: 例如：80\n      placeholderDuex: 例如：25\n  initialDelay:\n    label: 初始延迟\n    placeholder: '默认值是'\n  successThreshold:\n    label: 成功阈值\n    placeholder: '默认值是1'\n  timeout:\n    label: 超时\n    placeholder: '默认值是3'\n  type:\n    label: 检测类型\n    placeholder: 选择检查类型\n\nprometheusRule:\n  alertingRules:\n    addLabel: 添加告警\n    annotations:\n      description:\n        input: 描述注释值\n        label: 描述\n      label: 注释\n      message:\n        input: 消息注释值\n        label: 消息\n      runbook:\n        #input: Runbook URL Annotation Value\n        #label: Runbook URL\n      #summary:\n        #input: Summary Annotation Value\n        #label: Summary\n    bannerText: '在触发告警时，注释和标签将被传递给配置的 alertmanager，以允许它们构造通知信息并发送给配置的接收者。'\n    for:\n      label: 告警触发等待时间\n      #placeholder: '60'\n    label: 高级规则\n    labels:\n      label: 标签\n      severity:\n        choices:\n          critical: 重要\n          label: 严重性标签值\n          none: none\n          warning: 警告\n        label: 严重程度\n    name: 告警名称\n    removeAlert: 删除告警\n  groups:\n    add: 添加规则组\n    groupRowLabel: 规则组 {index}\n    groupInterval:\n      label: 覆盖组间隔\n      placeholder: '60'\n    label: 规则组\n    name: 组名称\n    none: 请添加至少一个规则组，其中至少包含一个警告或一个记录规则。\n    removeGroup: 删除组\n    responseStrategy:\n      label: 部分响应策略\n  promQL:\n    label: PromQL 表达式\n  recordingRules:\n    addLabel: 添加记录\n    label: 记录规则\n    labels: 标签\n    name: 时间序列的名称\n    removeRecord: 删除记录\n\npromptRemove:\n  andOthers: |-\n    {count, plural,\n    =0 {.}\n    =1 {，还有另一个}\n    other {, 还有其他{count}个}\n    }\n  attemptingToRemove: \"您在尝试删除 {type}\"\n  protip: \"提示：按住 {alternateLabel} 键同时单击 delete 以绕过此确认\"\n  confirmName: \"Enter <b>{nameToMatch}</b> below to confirm:\"\n\nrancherAlertingDrivers:\n  msTeams: 启用Microsoft Teams通知\n  sms: 启用短信通知\n  selectOne: 你必须选择以下至少一个选项。\n\nrbac:\n  roleBinding:\n    noData: 没有与此资源相关联的成员。\n    user:\n      label: 用户\n    role:\n      label: 角色\n    add: 添加成员\n  displayRole:\n    fleetworkspace-admin: 管理员\n    fleetworkspace-member: 成员\n    fleetworkspace-readonly: 只读用户\n  roletemplate:\n    label: 角色\n    newUserDefault:\n      no: 否\n      tooltip: 这并不影响任何已经存在的角色的绑定。\n    locked:\n      label: 锁定\n      yes: '是：新的绑定不允许使用这个角色'\n      no: 否\n    tabs:\n      grantResources:\n        label: 授予资源\n        tableHeaders:\n          verbs: 操作\n          resources: 资源\n          nonResourceUrls: 非资源URL\n          apiGroups: API组\n    subtypes:\n      GLOBAL:\n        createButton: 创建全局角色\n        label: 全局\n        yes: \"是：新用户的默认角色\"\n        defaultLabel: 新用户的默认角色\n      CLUSTER:\n        createButton: 创建集群角色\n        label: 集群\n        yes: \"是：创建新集群的默认角色\"\n        defaultLabel: 集群创建者\n      NAMESPACE:\n        createButton: 创建项目或命名空间角色\n        label: 项目或命名空间\n        yes: \"是：创建项目或命名空间的默认角色\"\n        defaultLabel: 项目创建者\n      RBAC_ROLE:\n        label: 角色\n      RBAC_CLUSTER_ROLE:\n        label: 集群角色\n      noContext:\n        label: 没有内容\n  globalRoles:\n    types:\n      global:\n        label: 全局权限\n        description: |-\n          控制{isUser, select,\n          true {user}\n          false {group}}有什么权限来管理整个{appName}的安装。\n      custom:\n        label: 自定义\n        description: 不是Rancher创建的角色\n      builtin:\n        label: 内置角色\n        description: 额外的角色来定义更搞细粒度的权限模型。\n    unknownRole:\n        description: 无描述\n    assignOnlyRole: 已分配该角色\n    role:\n      admin:\n        label: 管理员\n        description: 管理员可以完全控制整个安装和所有集群中的所有资源。\n      restricted-admin:\n        label: 受限管理员\n        description: 受限管理员可以完全控制所有下游集群的所有资源，但不能访问本地集群。\n  user:\n    label: 普通用户\n    description: 普通用户可以创建新的集群并管理他们被授予访问权的集群和项目。\n  user-base:\n    label: User-Base 用户\n    description: User-Base 用户只拥有登录权限。\n  clusters-create:\n    label: 创建集群\n    description: 允许用户创建集群，并成为该集群的所有者（owner）。\n  clustertemplates-create:\n    label: 创建RKE集群模板\n    description: 允许用户创建RKE集群模板，并成为该模板的所有者（owner）。\n  authn-manage:\n    label: 配置认证方式\n    description: 运行用户启用、编辑或禁用所有的认证方式。\n  catalogs-manage:\n    label: 配置应用\n    description: 允许用户添加、编辑和删除应用。\n  clusters-manage:\n    label: 管理所有集群\n    description: 允许用户管理所有集群，包括他们不是成员的集群。\n  clusterscans-manage:\n    label: 管理CIS集群扫描\n    description: 允许用户运行新建的CIS集群扫描和管理现有的CIS集群扫描。\n  kontainerdrivers-manage:\n    label: 创建集群驱动\n    description: 允许用户新建集群驱动，并成为该集群驱动的所有者（owner）。\n  features-manage:\n    label: 配置功能标记\n    description: 允许用户通过功能标志设置来启用和禁用自定义功能。\n  nodedrivers-manage:\n    label: 配置集群驱动\n    description: 允许用户启用、配置和删除所有节点驱动设置。\n  nodetemplates-manage:\n    label: 管理节点模板\n    description: 允许用户定义、编辑和删除节点模板。\n  podsecuritypolicytemplates-manage:\n    label: 管理Pod安全策略（PSP）\n    description: 允许用户定义、编辑和删除Pod安全策略。\n  roles-manage:\n    label: 管理用户角色\n    description: 允许用户定义、编辑和删除用户角色。\n  settings-manage:\n    label: 管理Rancher配置\n    description: 允许用户管理Rancher配置。\n  users-manage:\n    label: 管理用户\n    description: 允许用户为所有用户创建、删除和设置密码。\n  catalogs-use:\n    label: 使用应用\n    description: 允许用户查看和部署应用中的模板。 普通用户默认拥有此权限。\n  nodetemplates-use:\n    label: 使用节点模板\n    description: 允许用户使用任何现有的节点模板来部署新的节点。\n  view-rancher-metrics:\n    label: 查看Rancher指标\n    description: 允许用户通过API查看Metrics。\n  base:\n    label: 登录权限\n\nresourceDetail:\n  detailTop:\n    annotations: 注释\n    created: 已创建\n    deleted: 已删除\n    description: 描述\n    labels: 标签\n    ownerReferences: |-\n      {count, plural,\n      =1 {Owner}\n      other {Owners}}\n    hideAnnotations: |-\n      {annotations, plural,\n      =1 {Hide 1 annotation}\n      other {Hide {annotations} annotations}}\n    showAnnotations: |-\n      {annotations, plural,\n      =1 {Show 1 annotation}\n      other {Show {annotations} annotations}}\n    name: 名称\n  header:\n    clone: \"从 {subtype} {name} 克隆\"\n    create: 创建 {subtype}\n    edit: \"{subtype} {name}\"\n    stage: \"Stage from {subtype} {name}\"\n    view: \"{subtype} {name}\"\n  masthead:\n    #age: Age\n    defaultBannerMessage:\n      error: 此资源当前处于错误状态，但没有可用的详细消息。\n      transitioning: 此资源当前处于转换状态，但没有可用的详细消息。\n    sensitive:\n      hide: 隐藏敏感信息\n      show: 显示敏感信息\n    namespace: 命名空间\n    workspace: 工作空间\n    project: 项目\n    detail: 详情\n    config: 配置\n    #yaml: YAML\n    managedWarning: |-\n      This {type} is managed by {hasName, select,\n        no {a {managedBy} app}\n        yes {the {managedBy} app {appName}}}; 在此所做的更改可能会在应用程序下次更改时被覆盖。\nresourceList:\n  head:\n    create: 创建\n    createFromYaml: 使用 YAML 文件创建\n    createResource: \"创建 {resourceName}\"\n\nresourceTable:\n  groupBy:\n    none: 平面列表\n    namespace: 以命名空间分组\n    project: 以项目分组\n  groupLabel:\n   cluster: \"<span>集群：</span> {name}\"\n  namespace: \"<span>命名空间：</span> {name}\"\n  machinePool: \"<span>节点池</span> {name}\"\n  notInANamespace: 不在命名空间内\n  notInAProject: 不在项目内\n  project: \"<span>项目：</span> {name}\"\n  notInAWorkspace: 不在工作空间内\n  workspace: \"<span>工作空间:</span> {name}\"\n\nresourceTabs:\n  conditions:\n    tab: 条件\n  events:\n    tab: 最近事件\n  related:\n    tab: 相关资源\n    #from: Referred To By\n    #to: Refers To\n\n\nresourceYaml:\n  errors:\n    namespaceRequired: 这个资源是有命名空间的，所以必须提供一个命名空间。\n  buttons:\n    continue: 继续编辑\n    diff: 显示差异\n\nrioConfig:\n  configure:\n    description: 描述\n    helpText:\n      listItem1: Kubernetes 的应用部署引擎\n      listItem2: \"Rio 使 DevOps 更快、更容易地构建、测试、部署、扩展和版本无状态应用。\"\n    requirements:\n      header: 主机要求\n      helpText:\n        listItem1: 至少 1 核心 CPU\n        listItem2: 至少 2 GB 内存\n  #header: Rio\n  yaml:\n    buttonText: 自定义\n\nsecret:\n  authentication: 身份验证\n  certificate:\n    certificate: 证书\n    cn: 域名\n    expires: 到期\n    issuer: Issuer\n    plusMore: \"+ {n} 更多\"\n    privateKey: 私钥\n  data: 数据\n  registry:\n    address: 仓库类型\n    domainName: 仓库地址\n    password: 密码\n    username: 用户名\n  basic:\n    password: 密码\n    username: 用户名\n  ssh:\n    keys: Keys\n    public: 公钥\n    private: 私钥\n  serviceAcct:\n    ca: CA 证书\n    token: Token\n  type: 类型\n  types:\n    #'opaque': 'Opaque'\n    #'kubernetes.io/service-account-token': 'Svc Acct Token'\n    'kubernetes.io/dockercfg': '仓库'\n    'kubernetes.io/dockerconfigjson': '仓库'\n    'kubernetes.io/basic-auth': 'HTTP Basic Auth'\n    'kubernetes.io/ssh-auth': 'SSH 密钥'\n    'kubernetes.io/tls': 'TLS 证书'\n    'bootstrap.kubernetes.io/token': 'Bootstrap Token'\n    'istio.io/key-and-cert': 'Istio 证书'\n    'helm.sh/release.v1': 'Helm 版本'\n    'fleet.cattle.io/cluster-registration-values': 'Fleet 集群'\n    'provisioning.cattle.io/cloud-credential': '云凭证'\n  initials:\n    'opaque': 'O'\n    'kubernetes.io/service-account-token': 'SAT'\n    'kubernetes.io/dockercfg': 'R'\n    'kubernetes.io/dockerconfigjson': 'R'\n    'kubernetes.io/basic-auth': 'HTTP'\n    'kubernetes.io/ssh-auth': 'SSH'\n    'kubernetes.io/tls': 'TLS'\n    'bootstrap.kubernetes.io/token': 'Boot'\n    'istio.io/key-and-cert': 'Ist'\n    'helm.sh/release.v1': 'Helm'\n    'fleet.cattle.io/cluster-registration-values': 'F'\n    'provisioning.cattle.io/cloud-credential': 'CC'\n  relatedWorkloads: 相关的工作负载\n\nselectOrCreateAuthSecret:\n  label: 认证\n  options:\n    none: 无\n    basic: HTTP Basic Auth\n    ssh: SSH 密钥\n    custom: 密钥名称\n  ssh:\n    publicKey: 公钥\n    privateKey: 私钥\n  basic:\n    username: 用户名\n    password: 密码\n  namespaceGroup: \"命名空间：{name}\"\n  chooseExisting: \"选择一个已有的密钥\"\n  createSsh: 创建一个新的SSH密钥对\n  createBasic: 创建一个新的HTTP Basic Auth 密钥\n\nservicePorts:\n  header:\n    label: 端口规则\n  rules:\n    listening:\n      label: 监听端口\n      placeholder: 例如：8080\n    name:\n      label: 端口名称\n      placeholder: 例如：myport\n    node:\n      label: 节点端口\n      placeholder: 例如：80\n    protocol:\n      label: 协议\n    target:\n      label: 目标端口\n      placeholder: 例如：80 或 http\n\nserviceTypes:\n  clusterip: 集群 IP 地址\n  externalname: 外部 DNS 名称\n  headless: Headless\n  loadbalancer: 负载均衡\n  nodeport: 节点端口\n\nservicesPage:\n  anyNode: 任何节点\n  labelsAnnotations:\n    label: 标签和注释信息\n  affinity:\n    actionLabels:\n      clientIp: 客户端 IP\n      none: 未配置会话保持\n    helpText: 根据其源 IP 将连接映射到一个一致的目标\n    label: 会话保持\n    timeout:\n      label: 会话粘滞时间\n      placeholder: 以秒为单位，例如 10800 表示 10800 秒，即 48 分钟\n  externalName:\n    define: DNS 名称\n    helpText: \"外部名称的目的是指定一个 DNS 名称。如果要硬编码一个 IP 地址，请使用 headless 服务。\"\n    label: 外部 DNS 服务名称\n    placeholder: 例如：my.database.example.com\n    input:\n      label: DNS名称\n  ips:\n    define: 定义服务端口\n    clusterIpHelpText: Cluster IP 地址必须在为 API 服务器配置的 CIDR 范围内。\n    external:\n      label: 外部 IP\n      placeholder: 例如：1.1.1.1\n      protip: 集群中哪些节点也将接受该服务的流量的 IP 地址列表\n    input:\n      label: 集群 IP\n      placeholder: 例如：10.43.XXX.XXX\n    label: 监听 IP\n  pods:\n    #label: Pods\n  ports:\n    label: 端口\n  selectors:\n    helpText: \"如果没有创建选择器，则必须手动输入端点。\"\n    label: 选择器\n    matchingPods:\n      matchesSome: |-\n        {matched, plural,\n          =0 {与{total, number}个pods中的0个匹配。如果没有创建选择器，必须进行手动端点。}\n          =1 {与{total, number}个pods中的1个匹配： \"{sample}\"}\n          other {与{total, number}个pods中的{matched, number}个匹配，包括 \"{sample}\"。}\n        }\n  serviceTypes:\n    clusterIp:\n      abbrv: IP\n      description: 在集群内部 IP 上公开服务。选择此值使服务只能从集群内部访问。这是默认类型。\n      label: 集群 IP\n    externalName:\n      abbrv: EN\n      description: \"将服务与`externalName`字段的内容(如 foo.bar.example.com)进行映射，返回一个带有其值的 CNAME 记录。没有设置任何形式的代理。\"\n      label: 外部 DNS 服务名称\n    headless:\n      abbrv: H\n      description: 既没有定义集群 IP，也没有定义负载均衡器。这些是用来与 Kubernetes 实现之外的其他服务发现机制对接的。没有分配集群 IP，kube-proxy 也不处理这些服务。\n      label: Headless\n    loadBalancer:\n      abbrv: LB\n      description: 使用云提供商的负载平衡器向外部暴露服务。\n      label: 负载均衡器\n    nodePort:\n      abbrv: NP\n      description: \"在每个节点的 IP 上以静态端口（`NodePort`）公开服务。您将能够通过请求`<NodeIP>:<NodePort>`从集群外部联系这种类型的服务。\"\n      label: 节点端口\n  typeOpts:\n    label: 服务类型\n\nsortableTable:\n  actionAvailability:\n    selected: \"已选择 {actionable} 项\"\n    some: \"一共有 {total} 项，符合条件的有 {actionable} 项\"\n  noData: 没有匹配项\n  noRows: 没有内容显示\n  noActions: 没有可用的操作\n  paging:\n    generic: |-\n      {pages, plural,\n      =0 {无项目}\n      =1 {{count}项}\n      other {{count}项中的第{from} - {to}项}}\n    resource: |-\n      {pages, plural,\n      =0 {No {pluralLabel}}\n      =1 {{count} {count, plural, =1 {{singularLabel}} other {{pluralLabel}}}}\n      other {{from} - {to} of {count} {pluralLabel}}}\n  search: Filter\n\nstorageClass:\n  actions:\n    setAsDefault: 设置为默认配置\n    resetDefault: 重设默认配置\n  parameters:\n    label: 参数\n  customize:\n    label: 自定义\n    reclaimPolicy:\n      label: 回收策略\n      delete: 删除存储卷时，同时删除卷和底层设备。\n      retain: 保留存储卷，以通过手动清理。\n    allowVolumeExpansion:\n      label: 允许扩展存储卷\n      enabled: 允许\n      disabled: 不允许\n    volumeBindingMode:\n      label: 存储卷卷绑定模式\n      now: 在创建PersistentVolumeClaim时，立即绑定并配置一个持久卷\n      later: 创建了使用PersistentVolumeClaim的Pod之后，再绑定并配置一个持久卷。\n    mountOptions:\n      label: 挂载存储卷选项\n      addlabel: 添加选项\n  aws-ebs:\n    title: Amazon EBS磁盘\n    volumeType:\n      label: 存储卷类型\n      #gp2: GP2 - General Purpose SSD\n      #io1: IO1 - Provisioned IOPS SSD\n      #st1: ST1 - Throughput-Optimized HDD\n      #sc1: SC1 - Cold-Storage HDD\n      provisionedIops:\n        #label: Provisioned IOPS\n        suffix: 每秒，每GB\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    availabilityZone:\n      label: 可用区\n      automatic: '自动选择：选择节点所在区域作为可用区'\n      manual: '手动选择：自行指定一个可用区'\n      placeholder: 例如：us-east-1d, us-east-1c\n    encryption:\n      label: 加密\n      enabled: 启用\n      disabled: 不启用\n    keyId:\n      label: 用于加密的KMS密钥ID\n      automatic: '自动：生成一个密钥'\n      manual: '手动：使用一个指定的密钥（full ARN）'\n  azure-disk:\n    title: Microsoft Azure磁盘\n    storageAccountType:\n      label: Storage Account类型\n      placeholder: 例如：Standard_LRS\n    kind:\n      label: 类型\n      shared: 共享 (unmanaged disk)\n      dedicated: 独享 (unmanaged disk)\n      managed: 管理\n  azure-file:\n    title: Azure文件\n    skuName:\n      label: Sku名称\n      placeholder: 例如：Standard_LRS\n    location:\n      label: 位置\n      placeholder: 例如：eastus\n    storageAccount:\n      #label: Storage Account\n      placeholder: 例如：azure_storage_account_name\n  gce-pd:\n    title: Google Persistent磁盘\n    volumeType:\n      label: 存储卷类型\n      standard: 标准\n      ssd: SSD\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    availabilityZone:\n      label: 可用区\n      automatic: '自动选择：选择节点所在区域作为可用区'\n      manual: '手动选择：自行指定一个可用区'\n      placeholder: 例如：us-east-1d和us-east-1c\n    replicationType:\n      label: 副本类型\n      zonal: 可用区\n      regional: 区域\n  longhorn:\n    #title: Longhorn\n    addLabel: 添加参数\n  vsphere-volume:\n    title: VMWare vSphere卷\n    diskFormat:\n      label: 磁盘格式\n      #thin: Thin\n      #zeroedthick: Zeroed Thick\n      #eagerzeroedthick: Eager Zeroed Thick\n    storagePolicyName:\n      label: 存储策略名称\n      placeholder: 例如：gold\n    datastore:\n      #label: Datastore\n      placeholder: 例如：VSANDatastore\n    hostFailuresToTolerate:\n      label: 容忍主机失败的次数\n      placeholder: 例如：2\n    cacheReservation:\n      label: 预留缓存的大小\n      placeholder: 例如：20\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext3\n  custom:\n    addLabel: 添加参数\n  glusterfs:\n    title: Gluster Volume（不支持）\n    restUrl:\n      label: REST URL\n      placeholder: 例如：http://127.0.0.1:8081\n    restUser:\n      label: REST 用户\n      placeholder: 例如：admin\n    restUserKey:\n      label: REST 用户密钥\n      placeholder: 例如：password\n    secretNamespace:\n      label: 密钥所在的命名空间\n      placeholder: 例如：default\n    secretName:\n      label: 密钥名称\n      placeholder: 例如：heketi-secret\n    clusterId:\n      label: 集群ID\n      placeholder: 例如：630372ccdc720a92c681fb928f27b53f\n    gidMin:\n      label: GID MIN\n      placeholder: 例如：40000\n    gidMax:\n      label: GID MAX\n      placeholder: 例如：50000\n    volumeType:\n      label: 卷类型\n      placeholder: 例如：eplicate:3\n  cinder:\n    title: Openstack Cinder Volume（不支持）\n    volumeType:\n      label: 卷类型\n      placeholder: 例如：fast\n    availabilityZone:\n      label: 可用区\n      automatic: \"自动选择：选择节点所在区域作为可用区\"\n      manual:\n        label: \"手动选择：自行指定一个可用区\"\n        placeholder: 例如：nova\n  rbd:\n    title: Ceph RBD（不支持）\n    monitors:\n      label: 监控\n      placeholder: 例如：10.16.153.105:6789\n    adminId:\n      #label: Admin ID\n      placeholder: 例如：kube\n    adminSecretNamespace:\n      label: Admin密钥所在的命名空间\n      placeholder: 例如：kube-system\n    adminSecret:\n      label: Admin密钥\n      placeholder: 例如：Secret\n    pool:\n      label: 池\n      placeholder: 例如：kube\n    userId:\n      label: 用户ID\n      placeholder: 例如：kube\n    userSecretNamespace:\n      label: 用户密钥所在的命名空间\n      placeholder: 例如：default\n    userSecretName:\n      label: 用户密钥名称\n      placeholder: 例如：ceph-secret-user\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    imageFormat:\n      label: 镜像格式\n      placeholder: 例如：2\n    imageFeatures:\n      label: 镜像功能\n      placeholder: 例如：layering\n  quobyte:\n    title: Quobyte Volume （不支持）\n    quobyteApiServer:\n      label: Quobyte API Server\n      placeholder: 例如：http://138.68.74.142:7860\n    registry:\n      label: 仓库IP地址\n      placeholder: 例如：138.68.74.142:7861\n    adminSecretNamespace:\n      label: Admin密钥所在的命名空间\n      placeholder: 例如：kube-system\n    adminSecretName:\n      label: Admin密钥名称\n      placeholder: 例如：quobyte-admin-secret\n    user:\n      label: 用户\n      placeholder: 例如：root\n    group:\n      label: 用户组\n      placeholder: 例如：root\n    quobyteConfig:\n      label: Quobyte配置\n      placeholder: 例如：BASE\n    quobyteTenant:\n      label: Quobyte租户角色\n      placeholder: 例如：DEFAULT\n  portworx-volume:\n    title: Portworx Volume（不支持）\n    filesystem:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    blockSize:\n      label: 区块大小\n      placeholder: 例如：32\n    repl:\n      label: Repl\n      placeholder: 例如：1; 0 for entire device\n    ioPriority:\n      label: I/O优先级\n      placeholder: 例如：low\n    snapshotsInterval:\n      label: 快照间隔\n      placeholder: 例如：70\n    aggregationLevel:\n      label: 聚合水平\n      placeholder: 例如：0\n    ephemeral:\n      #label: Ephemeral\n      placeholder: 例如：true\n  scaleio:\n    title: ScaleIO Volume（不支持）\n    gateway:\n      label: 网关\n      placeholder: 例如：https://192.168.99.200:443/api\n    system:\n      label: 系统\n      placeholder: 例如：scaleio\n    protectionDomain:\n      #label: Protection Domain\n      placeholder: 例如：pd0\n    storagePool:\n      label: 存储池\n      placeholder: 例如：sp1\n    storageMode:\n      label: 存储模式\n      #thin: Thin Provisioned\n      #thick: Thick Provisioned\n    secretRef:\n      #label: Secret Ref\n      placeholder: 例如：sio-secret\n    readOnly:\n      label: 只读\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：xfs\n  storageos:\n    title: StorageOS（不支持）\n    pool:\n      label: 池\n      placeholder: 例如：default\n    description:\n      label: 描述\n      placeholder: 例如：Kubernetes volume\n    filesystemType:\n      label: 文件系统类型\n      placeholder: 例如：ext4\n    adminSecretNamespace:\n      label: Admin密钥所在的命名空间\n      placeholder: 例如：default\n    adminSecretName:\n      label: Admin密钥名称\n      placeholder: 例如：storageos-secret\n  no-provisioner:\n    title: 本地存储（不支持）\n\ntableHeaders:\n  #accessKey: Access Key\n  address: 地址\n  age: 存活时间\n  apiGroup: API 分组\n  authRoles:\n    globalDefault: 新用户默认\n    clusterDefault: 集群创建者默认\n    projectDefault: 项目创建者默认\n  branch: 分支\n  builtIn: 内置\n  #bundlesReady: Bundles\n  bundleDeploymentsReady: 部署\n  builtin: 内建\n  #chart: Chart\n  clusterCreatorDefault: 默认集群创建者\n  #clusterFlow: Cluster Flow\n  #clusterOutput: Cluster Output\n  clusters: 集群\n  clustersReady: 就绪的集群\n  clusterGroups: 集群组\n  #commit: Commit\n  condition: 状态\n  #customVerbs: Custom Verbs\n  description: 描述\n  expires: 过期时间\n  providers: 配置提供商\n  #cpu: CPU\n  date: 日期\n  default: 默认\n  destination: 目标\n  download: 下载\n  effect: 影响\n  endpoints: 端点\n  #flow: Flow\n  gitRepos: Git 代码仓库\n  host: |-\n    {count, plural,\n      one { 主机 }\n      other { 主机 }\n    }\n  image: 镜像\n  imageSize: 大小\n  ingressDefaultBackend: 默认\n  ingressTarget: 目标\n  internalExternalIp: 外网 IP 地址或内网 IP 地址\n  jobs: Jobs\n  key: 密钥\n  keys: 数据\n  lastUpdated: 最后更新时间\n  lastSeen: 最后出现\n  loggingOutputProviders: Providers\n  machines: 机器\n  matches: 匹配\n  maxKubernetesVersion: 最大 Kubernetes 版本\n  message: 信息\n  minKubernetesVersion: 最小 Kubernetes 版本\n  name: 名称\n  nameDisplay: 显示名称\n  nameUnlinked: 名称\n  namespace: 命名空间\n  namespaceName: 命名空间名称\n  namespaceNameUnlinked: 名称\n  node: 节点\n  nodeName: 节点名称\n  nodesReady: 就绪节点\n  #nodePort: Node Port\n  object: 对象\n  output: 输出\n  p95: 95 百分位数\n  persistentVolumeSource: 持久卷源\n  podImages: 镜像\n  #pods: Pods\n  port: 端口\n  protocol: 协议\n  provider: 提供商\n  publicPorts: 公有端口\n  ram: 内存(RAM)\n  rbac:\n    create: 创建\n    delete: 删除\n    get: 查询\n    list: 列表\n    patch: 修改\n    update: 更新\n    watch: 监控\n  ready: 就绪\n  reason: 原因\n  repo: Repo\n  reposReady: 就绪的 Repo\n  replicas: 副本数量\n  reqRate: 请求频率\n  resource: 资源\n  resources: 资源\n  restarts: 重启\n  rioImage: Rio 镜像\n  role: 角色\n  roles: 角色\n  scale: 比例\n  scope: 规模\n  selector: 选择器\n  simpleName: 名称\n  simpleScale: 比例\n  simpleType: 类型\n  started: 已开始\n  state: 状态\n  status: 状态\n  storage_class_provisioner: 提供者\n  subject: 主题\n  subType: 类型\n  success: 成功\n  summary: 概述\n  target: 目标\n  targetKind: 目标类型\n  targetPort: 目标端口\n  type: 类型\n  updated: 更新\n  upgrade: 升级\n  url: URL 地址\n  userDisplayName: 显示名称\n  userId: 用户 ID\n  userStatus: 用户状态\n  username: 本地用户名\n  value: 值\n  version: 版本号\n  weight: 权重\n\ntarget:\n  router:\n    label: 路由\n    placeholder: 选择路由\n  service:\n    label: 服务（svc）\n    placeholder: 选择服务\n  title: 目标\n  version:\n    label: 版本\n    placeholder: 选择版本\n\nuser:\n  detail:\n    username: 用户名\n    globalPermissions:\n      label: 全局权限\n      description: 管理影响整个安装的资源的权限\n      adminMessage: 该用户是一个管理员，拥有所有权限\n      tableHeaders:\n        permission: 权限\n    clusterRoles:\n      label: 集群角色\n      description: 授予一个用户在某个集群的角色\n      tableHeaders:\n        cluster: 集群\n    projectRoles:\n      label: 项目角色\n      description: 授予一个用户在某个项目的角色\n      tableHeaders:\n        project: 项目\n    generic:\n      tableHeaders:\n        role: 角色\n        #granted: Granted\n  edit:\n    credentials:\n      label: 凭证\n      username:\n        label: 用户名\n        placeholder: 例如：jsmith\n        exists: '用户名已被使用。请选择一个新的用户名'\n      displayName:\n        label: 显示名称\n        placeholder: 例如：John Smith\n      userDescription:\n        label: 描述\n        placeholder: 例如：This account is for John Smith\n  list:\n    errorRefreshingGroupMemberships: 刷新小组成员名单时出错，请重试\nvalidation:\n  arrayLength:\n    between: '\"{key}\" 应该包含 {min} 至 {max} {max, plural, =1 {项} other {项}}'\n    exactly: '\"{key}\" 应该包含 {count, plural, =1 {#项} other {#项}}'\n    max: '\"{key}\" 应该包含最多 {count} {count, plural, =1 {项} other {项}}'\n    min: '\"{key}\" 应该包含最少 {count} {count, plural, =1 {项} other {项}}'\n  boolean: '\"{key}\" 必须是一个布尔值'\n  chars: '\"{key}\" 包含 {count, plural, =1 {一个无效字符} other {#多个无效字符}}: {chars}'\n  custom:\n    missing: \"{validatorName}不存在校验! 该校验是否存在于自定义校验中？名字的拼写是否正确？\"\n  dns:\n    doubleHyphen: '\"{key}\" 不能包含两个或多个连续的连字符“-”'\n    hostname:\n      empty: '\"{key}\" 必须至少包含一个字符'\n      emptyLabel: '\"{key}\" 不能包含两个连续的点“.”'\n      endDot: '\"{key}\" 不能以点“.”结束'\n      endHyphen: '\"{key}\" 不能以连字符“-”结束'\n      startDot: '\"{key}\" 不能以点“.”开始'\n      startHyphen: '\"{key}\" 不能以连字符“-”开始'\n      startNumber: '\"{key}\" 不能以数字开始'\n      tooLong: '\"{key}\" 的长度不能超过 {max} 个字符数量'\n      tooLongLabel: '\"{key}\" 不能包含超过 {max} 字符的部分'\n    label:\n      emptyLabel: '\"{key}\" 不能为空'\n      endHyphen: '\"{key}\" 不能以连字符“-”结束'\n      startHyphen: '\"{key}\" 不能以连字符“-”开始'\n      startNumber: '\"{key}\" 不能以数字开始'\n      tooLongLabel: '\"{key}\" 的长度不能超过 {max} 个字符数量'\n  flowOutput:\n    global: 需要选择 \"集群输出\"。\n    both: 需要选择 \"输出\" 或 \"集群输出\"。\n  output:\n    logdna:\n      apiKey: 需要设置一个 \"Api 密钥\"。\n  invalidCron: 无效 cron 调度\n  k8s:\n    identifier:\n      emptyLabel: '\"{key}\" 不能为空'\n      emptyPrefix: '\"{key}\" 不能为空'\n      endLetter: '\"{key}\" 末位必须是字母或数字'\n      startLetter: '\"{key}\" 首位必须是字母或数字'\n      tooLongKey: '\"{key}\" 的长度不能超过 {max} 个字符数量'\n      tooLongPrefix: '\"{key}\" 前缀不能超过 {max} 个字符数量'\n  noSchema: 没有找到可以验证的模式\n  noType: 无类型可验证\n  number:\n    between: '\"{key}\" 的长度必须在 {min} 和 {max} 之间'\n    exactly: '\"{key}\" 的长度必须是 {val}'\n    max: '\"{key}\" 的长度必须小于或等于 {val}'\n    min: '\"{key}\" 的长度必须大于或等于 {val}'\n  podAffinity:\n    affinityTitle: Pod 亲和性\n    antiAffinityTitle: Pod 反亲和性\n    requiredDuringSchedulingIgnoredDuringExecution: 需要规则\n    preferredDuringSchedulingIgnoredDuringExecution: 优先规则\n    topologyKey: Rule [{index}] of {group} {rules} - 拓扑键是必需的。\n    matchExpressions:\n      operator: Rule [{index}] of {group} {rules} - operator must be one of 'In', 'NotIn', 'Exists', 'DoesNotExist'\n      valueMustBeEmpty: Rule [{index}] of {group} {rules} - value must be empty if operator is 'Exists' or 'DoesNotExist'\n      valuesMustBeDefined: Rule [{index}] of {group} {rules} - value must be defined if operator is 'In' or 'NotIn'\n  port: 端口号的取值范围是1到65535之间的任何数字。\n  prometheusRule:\n    groups:\n      required: 至少需要一个规则组。\n      singleAlert: 规则可以包含警告规则或记录规则，但不能同时包含两者。\n      valid:\n        name: '规则组需要名称 {index}.'\n        rule:\n          alertName: '规则组{groupIndex}规则{ruleIndex}需要一个警报名称。 '\n          expr: '规则组{groupIndex}规则{ruleIndex}需要一个PromQL表达式'\n          labels: '规则组{groupIndex}规则{ruleIndex}至少需要一个标签。建议使用严重程度作为标签。'\n          recordName: '规则组{groupIndex}规则{ruleIndex}需要一个时间序列名称。'\n        singleEntry: '在规则组{index}中至少需要一个警报规则或一个记录规则。'\n  required: '\"{key}\"是必填项'\n  requiredOrOverride: '\"{key}\" 是必须的或必须允许覆盖的'\n  roleTemplate:\n    roleTemplateRules:\n      missingVerb: 你必须为每个资源授予指定至少一个动作\n      missingResource: 你必须为每个资源授予至少指定一个资源、非资源URL或API组\n      missingApiGroup: 你必须为每个资源授予指定一个API组\n      missingOneResource: 你必须为每个资源授予至少指定一个资源、非资源URL或API组\n  service:\n    externalName:\n      none: '使用外部 DNS 服务时，External Name 是必填项'\n    ports:\n      name:\n        required: \"端口规则 [{position}] - 端口名称是必填项\"\n      nodePort:\n        requriedInt: \"端口规则 [{position}] - 如果包含节点端口，则节点端口必须是整数值，例如：80\"\n      port:\n        required: \"端口规则 [{position}] - 端口是必填项\"\n        requriedInt: \"端口规则 [{position}] - 如果包含端口，则端口必须是整数值，例如：80\"\n      targetPort:\n        between: \"端口规则 [{position}] - 目标端口的取值范围是： 1~65535\"\n        iana: \"端口规则 [{position}] - 目标端口必须是 IANA 服务名称或整数值\"\n        ianaAt: \"端口规则 [{position}] - 目标端口 \"\n        required: \"端口规则 [{position}] - 目标端口是必填项\"\n  stringLength:\n    between: '\"{key}\" 的长度必须在 {min} 和 {max} 之间 {max, plural, =1 {字符} other {字符}}'\n    exactly: '\"{key}\" 的长度必须是 {count, plural, =1 {#字符} other {#字符}}'\n    max: '\"{key}\" 的长度必须小于或等于 {count} {count, plural, =1 {字符} other {字符}}'\n    min: '\"{key}\" 的长度必须大于或等于 {count} {count, plural, =1 {字符} other {字符}}'\n  targets:\n    missingProjectId: 一个目标必须选定一个项目。\n  monitoring:\n    route:\n      match: 必须选择至少一个匹配或匹配正则表达式\n      interval: '\"{key}\" 必须是以数字后跟单位(如 1h, 2m, 30s)的格式。'\n\nwizard:\n  back: 返回\n  finish: 完成\n  next: 下一步\n  step: \"步骤 {number}:\"\n\nwm:\n  connection:\n    connected: 已连接\n    connecting: 正在连接&hellip;\n    disconnected: 已断开连接\n    error: 错误\n  containerLogs:\n    clear: 清除\n    containerName: \"容器： {label}\"\n    download: 下载\n    follow: 回到底部\n    noData: 在当前范围内没有日志条目显示\n    noMatch: 没有符合当前过滤条件的数据\n    previous: 使用前一个容器\n    range:\n      all: 全部\n      hours: |-\n        {value, number}\n        {value, plural,\n        =1 {小时}\n        other {小时}\n        }\n      label: 显示最后一个\n      lines: \"{value, number}行\"\n      minutes: |-\n        {value, number} {value, plural,\n        =1 {分}\n        other {分}\n        }\n    search: 过滤条件\n    timestamps: 显示时间戳\n    wrap: 自动换行\n  containerShell:\n    clear: 清除\n    containerName: \"容器：{label}\"\n  kubectlShell:\n    #title: \"Kubectl: {name}\"\n\nworkload:\n  container:\n    command:\n      addEnvVar: 添加\n      args: 命令 (CMD)\n      as: 作为\n      command: 入口 (Entrypoint)\n      env: 环境变量\n      fromResource:\n        key:\n          label: 键\n          placeholder: \"例如：metadata.labels['<KEY>']\"\n        name:\n          label: 变量名\n          placeholder: \"例如：FOO\"\n        prefix: 前缀\n        source:\n          label: 源\n          placeholder: 例如：my-container\n        secret: 密文\n        configMap: 配置映射\n        containerName: 容器名称\n        type: 类型\n        value:\n          label: 值\n          placeholder: 例如：bar\n      #tty: TTY\n      workingDir: 工作目录\n      stdin: 标准输入\n    containerName: 容器名称\n    healthCheck:\n      checkInterval: 检查间隔\n      command:\n        command: 运行命令\n      failureThreshold: 故障阈值\n      httpGet:\n        headers: 请求头\n        path: 请求路径\n        port: 检查端口\n      initialDelay: 初始延迟\n      livenessProbe: 存活检查\n      livenessTip: 当该检查失败时，将重新启动容器，不建议用于大多数用途。\n      noHealthCheck: \"没有给容器配置存活、就绪或启动探测器\"\n      readinessProbe: 就绪检查\n      readinessTip: 当该检查失败时，会将容器从服务端点中移除，建议配置该检查。\n      startupProbe: 启动检查\n      startupTip: 容器在尝试其他健康检查之前，将等待此检查成功。\n      successThreshold: 成功阈值\n      timeout: 超时时间\n      kind:\n        none:  无\n        HTTP:  HTTP 请求返回成功的状态 (200-399)\n        HTTPS: HTTPS 请求返回成功的状态\n        tcp:   成功启动 TCP 连接\n        exec:  容器内运行的命令以 0 状态退出\n    image: 容器镜像\n    imagePullPolicy: 拉取镜像策略\n    imagePullSecrets: 拉取密钥\n    init: 初始化容器\n    name: 容器名称\n    noResourceLimits: 没有配置资源需求。\n    noPorts: 当前没有配置端口。\n    ports:\n      createService: 服务类型\n      noCreateService: 不创建服务\n      containerPort: 容器端口\n      hostIP: 主机 IP\n      hostPort: 公共主机端口\n      name: 名称\n      protocol: 协议\n      listeningPort: 监听端口\n    removeContainer: 移除容器\n    security:\n      addCapabilities: 添加功能\n      addGroupIDs: 添加组 ID\n      allowPrivilegeEscalation:\n        label: 允许权限提升\n        'false': 否\n        'true': \"是，容器可以获得比其父进程更多的权限。\"\n      dropCapabilities: 弃用 Capabilities\n      fsGroup: Filesystem 组\n      hostIPC: 使用主机 IPC 命名空间\n      hostPID: 只用主机 PID 命名空间\n      privileged:\n        label: 特权模式\n        'false': 否\n        'true': \"是，容器拥有访问主机全部权限\"\n      readOnlyRootFilesystem:\n        label: 只读 Root Filesystem\n        'false': 否\n        'true': \"是，容器有一个只读的文件系统\"\n      runAsGroup: 以群组 ID 运行\n      runAsNonRoot:\n        label: 以非 Root 方式运行\n        false: 否\n        true: \"是，容器必须以非 root 用户的身份\"\n      runAsNonRootOptions:\n        noOption: \"否\"\n        yesOption: \"是：容器必须以非 root 用户的身份运行。\"\n      runAsUser: 以用户 ID 运行\n      shareProcessNamespace: 共享单一进程命名空间\n      supplementalGroups: 其他组别 ID\n      sysctls: Sysctls\n      sysctlsKey: 名称\n    standard: 标准容器\n    titles:\n      container: 容器配置\n      command: 命令\n      containers: 容器\n      env: 环境变量\n      events: 事件\n      healthCheck: 健康检查\n      image: 镜像\n      networking: 网络\n      networkSettings: 网络设置\n      podAnnotations: Pod 注释\n      podLabels: Pod 标签\n      podScheduling: Pod 调度\n      nodeScheduling: 节点调度\n      ports: 端口映射\n      resources: 资源限制和预留\n      securityContext: 安全性上下文\n      status: 状态\n      volumeClaimTemplates: PVC 模板\n      upgrading: 扩缩容/升级策略\n  cronSchedule: 定时调度\n  detail:\n    #pods:\n      #title: Pods\n  detailTop:\n    node: 节点\n    #podIP: Pod IP\n    podRestarts: Pod 重启\n    workload: 工作负载\n    #pods: Pods by State\n    #runs: Runs\n  gaugeStates:\n    #active: Active\n    #transitioning: Transitioning\n    warning: 警告\n    error: 错误\n    succeeded: 成功\n    running: 运行中\n    failed: 失败\n  hideTabs: '隐藏高级选项'\n  job:\n    activeDeadlineSeconds:\n      label: 活动终止时间\n      tip: Job 在系统试图终止它之前可能处于活动状态的持续时间。\n    backoffLimit:\n      label: 重试次数\n      tip: 标记此 Job 失败之前的重试次数。\n    completions:\n      label: 完成 Job 历史数\n      tip: Job 应该运行的成功完成的 Pod 数。\n    failedJobsHistoryLimit:\n      label: 失败 Job 历史数\n      tip: 要保留的失败的已完成 Job 的数量。\n    parallelism:\n      label: 并发数\n      tip: Job 在给定时间应同时运行的 Pod 的最大数量。\n    startingDeadlineSeconds:\n      label: 运行 Job 的截止时间\n      tip: 如果 Job 错过了调度时间，再次尝试运行 Job 的截止时间，单位是秒\n    successfulJobsHistoryLimit:\n      label: 历史 Successful Job 累计数量\n      tip: 保留 Successful Job 的数量\n    suspend: 停止\n  networking:\n    dnsPolicy:\n      label: DNS 策略\n      options:\n        clusterFirst: 与配置的集群域后缀不匹配的任何 DNS 查询（例如 “www.kubernetes.io”） 都将转发到从节点继承的上游名称服务器。集群管理员可能配置了额外的存根域和上游 DNS 服务器。\n        clusterFirstWithHostNet: 对于以 hostNetwork 方式运行的 Pod，应显式设置其 DNS 策略 \"ClusterFirstWithHostNet\"。\n        default: 此设置允许 Pod 忽略 Kubernetes 环境中的 DNS 设置。Pod 会使用其 dnsConfig 字段所提供的 DNS 设置。\n        none: None\n      placeholder: 请选择一个 DNS 策略\n    hostAliases:\n      add: 添加\n      keyLabel: IP 地址\n      keyPlaceholder: 例如：1.1.1.1\n      label: 主机别名\n      tip: 使用主机别名向 Pod /etc/hosts 文件添加条目\n      valueLabel: 主机名\n      valuePlaceholder: \"例如：foo.com, bar.com\"\n    hostname:\n      label: 主机名\n      placeholder: 例如：web\n    nameservers:\n      add: 添加\n      label: DNS 服务器地址\n      placeholder: 例如：1.1.1.1\n    networkMode:\n      label: 网络模式\n      options:\n        hostNetwork: 主机网络\n        normal: 集群网络\n      placeholder: 请选择网络模式\n    dns: DNS 服务器地址和搜索域\n    resolver:\n      label: DNS 解析选项\n      add: 添加\n    searches:\n      add: 添加\n      label: 搜索域\n      placeholder: 例如：mycompany.com\n    subdomain:\n      label: 子域名\n      placeholder: 例如：web\n  validation:\n    containers: 容器\n    containerImage: 容器{{name} - “容器镜像 \"是必需的。\n  replicas: 副本\n  showTabs: '显示高级选项'\n  scheduling:\n    activeDeadlineSeconds: 判定 Pod 是否活跃的截止时间\n    activeDeadlineSecondsTip: 系统将 Pod 判定为 failed 并杀死其关联的容器前的等待时长\n    affinity:\n      addNodeSelector: 添加节点选择器\n      anyNode: 自动匹配节点运行 Pods\n      affinityTitle: 在这些选择器匹配的节点上运行 Pod\n      antiAffinityTitle: 在不与这些选择器匹配的节点上运行 Pod\n      affinityOption: 亲和性\n      antiAffinityOption: 反亲和性\n      matchExpressions:\n        addRule: 添加规则\n        doesNotExist: 不存在\n        exists: 存在\n        #greaterThan: \">\"\n        in: '='\n        inNamespaces: \"在这些命名空间中的 Pod：\"\n        key: 键\n        #lessThan: <\n        namespaces: 命名空间\n        notIn: ≠\n        operator: 运算符\n        value: 值\n        weight: 权重\n      noPodRules: 没有配置 Pod 调度策略\n      nodeName: 节点名称\n      priority: 优先级\n      preferAny: \"倾向于任何一种：\"\n      preferred: 首选\n      required: 最好\n      requireAny: \"需要以下任何一种：\"\n      schedulingRules: 通过调度规则匹配节点运行 Pods\n      specificNode: 指定节点运行 Pods\n      thisPodNamespace: 此 Pod 的命名空间\n      topologyKey:\n        label: 拓扑键\n        placeholder: 例如：failure-domain.beta.kubernetes.io/zone\n      type: 类型\n    priority:\n      className: 优先级名称\n      priority: 优先级\n    terminationGracePeriodSeconds: 终止宽限期\n    terminationGracePeriodSecondsTip: 终止 Pod 运行前的宽限期\n    titles:\n      advanced: 高级选项\n      nodeScheduling: 节点调度\n      nodeSelector: 具有以下标签的节点\n      podScheduling: Pod 调度\n      priority: 优先级\n      tab: 调度\n      tolerations: 容忍\n      limits: 限制和预留\n    tolerations:\n      addToleration: 添加\n      effect: 影响\n      effectOptions:\n        all: 全部\n        noExecute: 不执行\n        noSchedule: \"不调度\"\n        preferNoSchedule: 倾向于不调度\n      labelKey: 标签键\n      operator: 运算符\n      operatorOptions:\n        equal: =\n        exists: 存在\n      tolerationSeconds: 时间\n      value: 值\n  serviceName: 服务名称\n  storage:\n    subtypes:\n      secret: 密文\n      configMap: 配置映射\n      hostPath: Bind-Mount\n      persistentVolumeClaim: PVC\n      createPVC: 创建 PVC\n      #csi: CSI\n      #nfs: NFS\n      #awsElasticBlockStore: Amazon EBS Disk\n      #azureDisk: Azure Disk\n      #azureFile: Azure File\n      #gcePersistentDisk: Google Persistent Disk\n      #driver.longhorn.io: Longhorn\n      #vsphereVolume: VMWare vSphere Volume\n    addClaim: 添加 pvc\n    addMount:  添加\n    addVolume: 添加卷\n    certificate: 证书\n    csi:\n      diskName: 磁盘名称\n      diskURI: 磁盘 URI\n      cachingMode:\n        label: 缓存模式\n        options:\n          none: 无\n          readOnly: 只读\n          readWrite: 读写\n      kind:\n        label: 种类\n        options:\n          dedicated: 专用\n          managed: 管理\n          shared: 共用\n      drivers:\n        #driver.longhorn.io: Longhorn\n      fsType: 文件系统类型\n      shareName: 共享名\n      secretName: 密文名称\n      volumeID: 卷 ID\n      partition: 分区\n      pdName: 持久磁盘名称\n      storagePolicyID: 存储策略 ID\n      storagePolicyName: 存储策略名称\n      volumePath: 存储卷路径\n    defaultMode: 默认模式\n    driver: 驱动\n    hostPath:\n      label: 节点上的路径必须是\n      options:\n        default: '任何东西：不检查目标路径'\n        directoryOrCreate: 一个目录，如果不存在，则创建一个目录\n        directory: 现有目录\n        fileOrCreate: 一个文件，如果它不存在，则创建一个文件\n        file: 现有文件\n        socket: 现有 socket\n        charDevice: 现有的字符设备\n        blockDevice: 现有块设备\n    mountPoint: 容器挂载路径\n    nodePath: 路径或节点\n    optional:\n      label: 选填项\n      'no': '否'\n      'yes': '是'\n    path: 路径\n    readOnly: 只读\n    server: Server\n\n    subPath: 卷内子路径\n    title: '存储'\n    volumeName: 卷名称\n    volumePath: 卷路径\n  typeDescriptions:\n    apps.daemonset: DaemonSets 在每个符合条件的节点上正好运行一个 pod。当新节点被添加到集群中时，DaemonSets 会自动部署到它们身上。推荐用于全系统或可垂直扩展的工作负载，每个节点永远不需要超过一个 pod。\n    apps.deployment: 部署运行分布在符合条件的节点中的可扩展数量的 pod 副本。变更会逐步推出，并可在需要时回滚到之前的版本。推荐用于无状态和水平可扩展的工作负载。\n    apps.statefulset: StatefulSets 管理有状态的应用程序，并提供关于创建的 pod 的顺序和唯一性的保证。推荐用于具有持久性存储或严格身份、法定人数或升级顺序要求的工作负载。\n    batch.cronjob: CronJobs 创建 Job，然后按照重复的时间表运行 Pod。该计划以标准的 Unix cron 格式表示，并使用 Kubernetes 控制平面的时区（通常是 UTC）。\n    batch.job: 作业创建一个或多个 pod，通过运行一个 pod 直到成功退出，可靠地执行一次性任务。失败的 pod 会自动替换，直到达到指定的完成运行次数。作业还可以并行运行多个 pod，或作为批处理工作队列。\n  upgrading:\n    activeDeadlineSeconds:\n      label: 判定 Pod 是否活跃的截止时间\n      tip: 系统将 Pod 判定为 failed 并杀死其关联的容器前的等待时长\n    concurrencyPolicy:\n      label: 并发策略\n      options:\n        allow: 允许多个 CronJobs 同时运行\n        forbid: 如果当前运行还没有结束，则跳过下一个运行\n        replace: 如果当前运行还没有结束，则替换运行\n    maxSurge:\n      label: 最大 Pod 数量\n      tip: 在任何给定时间内允许超出所需规模的最大 Pod 数量。\n    maxUnavailable:\n      label: 最大不可用数量\n      tip: 在任何给定时间内无法使用的最大 Pod 数量。\n    minReadySeconds:\n      label: Minimum Ready\n      tip: 在容器没有崩溃的情况下，Pod 被视为可用的最短期限。\n    podManagementPolicy:\n      label: Pod 管理策略\n    progressDeadlineSeconds:\n      label: 进程截止时间\n      tip: 在标志部署失败之前，等待部署取得进展的最短期限。\n    revisionHistoryLimit:\n      label: 修订历史记录限制\n      tip: 保留用于回滚的旧 ReplicaSets 的最大数量\n    strategies:\n      labels:\n        delete: \"删除：只有在手动删除旧 pod 时才会创建新 pod\"\n        recreate: \"重新创建：杀死所有的 pod，然后启动新的 pod。\"\n        rollingUpdate: \"滚动升级：创建新的 pod，直到达到 max surge，然后再删除旧 pod。停用的 pod 数量不能超过设定的最大不可用数量。\"\n    terminationGracePeriodSeconds:\n      label: 終止宽限期\n      tip: 杀死 Pod 前所需的等待时间\n    title: 升级中\n\n\n##############################\n# Model Properties\n##############################\nmodel:\n  account:\n    kind:\n      admin: 管理员\n      agent: Agent\n      project: 环境\n      registeredAgent: Registered Agent\n      service: 服务\n      user: 用户名\n  \"catalog.cattle.io.app\":\n    firstDeployed: 首次部署\n    lastDeployed: 最后部署\n  #authConfig:\n    #description:\n      #ldap: LDAP\n      #saml: SAML\n      #oauth: OAuth\n    #provider:\n      #system: System\n      #local: Local\n      #multiple: Multiple\n      #activedirectory: ActiveDirectory\n      #azuread: AzureAD\n      #github: GitHub\n      #keycloak: Keycloak\n      #ldap: LDAP\n      #openldap: OpenLDAP\n      #shibboleth: Shibboleth\n      #ping: Ping Identity\n      #adfs: ADFS\n      #okta: Okta\n      #freeipa: FreeIPA\n      #googleoauth: Google\n\n  cluster:\n    name: 集群名称\n  ingress:\n    displayKind: 7 层负载均衡\n  machine:\n    role:\n      #controlPlane: Control Plane\n      #etcd: etcd\n      #worker: Worker\n  openldapconfig:\n    domain:\n      help: 只有此目录下的用户才能正常登录。\n      label: 用户搜索起点\n      placeholder: \"例如：ou=Users,dc=mycompany,dc=com\"\n    server:\n      label: 主机名称或 IP 地址\n    serviceAccountPassword:\n      label: Service Account 密码\n    serviceAccountUsername:\n      label: Service Account 用户名\n  projectMember:\n    role:\n      member: 成员\n      owner: 所有者\n      readonly: 只读\n      restricted: 受限\n  service:\n    displayKind:\n      generic: 服务\n      loadBalancer: 4 层负载均衡\n\ntypeDescription:\n  #Map of\n  #type: Description to be shown on the top of list view describing the type.\n  #      Should fit on one line.\n  #      If you link to anything external, it must have\n  #      target=\"_blank\" rel=\"noopener noreferrer nofollow\"\n  cis.cattle.io.clusterscanbenchmark: 基准版本是指使用 kube-bench 运行的基准名称，以及该基准的有效配置参数。\n  cis.cattle.io.clusterscanprofile: 配置文件是 CIS 扫描的配置，也就是要使用的基准版本和该基准中要跳过的任何特定测试。\n  cis.cattle.io.clusterscan: 创建扫描以根据定义的配置文件在集群上触发 CIS 扫描。扫描完成后会创建一份报告。\n  cis.cattle.io.clusterscanreport: 报告是对集群进行 CIS 扫描的结果。\n  resources.cattle.io.backup: 创建备份是为了基于资源集执行一次性备份或安排重复性备份。\n  resources.cattle.io.restore: 创建还原是为了根据备份文件触发对集群的还原。\n  resources.cattle.io.resourceset: 资源集定义了要在备份中存储哪些 CRD 和资源。\n  monitoring.coreos.com.servicemonitor: 服务监视器（service monitor ）定义了 Prometheus 将获取的服务组和端点，这是定义指标集合的最常见方法。\n  monitoring.coreos.com.podmonitor: A pod monitor defines the group of pods that Prometheus will scrape for metrics. The common way is to use service monitors, but pod monitors allow you to handle any situation where a service monitor wouldn't work.\n  monitoring.coreos.com.prometheusrule: Prometheus 规则定义了记录和/或警报规则。记录规则可以预先计算值并保存结果，警报规则允许您定义何时向 AlertManager 发送通知的条件。\n  monitoring.coreos.com.prometheus: Prometheus 服务器是 deployment 运行的服务，它的刮擦配置和规则是由选定的 ServiceMonitors、PodMonitors 和 PrometheusRules 决定的，它的告警信息将发送给所有选择的具有定制资源配置的 AlertManager。\n  monitoring.coreos.com.alertmanager: 告警管理器是 deployment 类型运行的服务，其配置将由同一命名空间中的 密文 指定，该 密文 决定了哪些警报应发送给哪个接收者。\n  catalog.cattle.io.clusterrepo: Chart 仓库是一个 Helm 仓库或 Rancher 的基于 git 的应用商店，它提供了集群中可用的 Chart 列表。\n  catalog.cattle.io.operation: 操作是指最近应用于集群的 Helm 操作列表。\n  catalog.cattle.io.app: 已安装的应用程序 Apps 是通过 Rancher catalog 或通过 Helm CLI 安装的 Helm 3 charts。\n  logging.banzaicloud.io.clusterflow: 集群流定义了要从整个集群收集和过滤哪些日志，以及发送输出哪些日志。集群流需要部署在 logging operator 所在的命名空间中。\n  logging.banzaicloud.io.flow: 流定义要收集和过滤哪些日志，以及要发送输出哪些日志。该流是一个命名空间资源，这意味着只从部署该流的命名空间收集日志。\n  logging.banzaicloud.io.clusteroutput: 集群输出定义可以将日志发送到哪些日志提供程序，并且只有部署在 logging operator 所在的命名空间中时才有效。\n  logging.banzaicloud.io.output: 输出定义可以将日志发送到哪些日志提供程序。输出需要在与使用它的流相同的命名空间中。\n  logging: 要收集和发送日志，您需要定义流和输出。流定义要收集、筛选哪些日志，以及要发送输出的日志。如果希望收集集群中的所有日志，可以创建一个 ClusterFlow。输出可以在命名空间级别定义，也可以在集群级别定义，并供这两种流类型使用。\n\ntypeLabel:\n  management.cattle.io.token: |-\n    {count, plural,\n      one { API Key }\n      other { API Keys }\n    }\n  cis.cattle.io.clusterscan: |-\n    {count, plural,\n      one { 扫描 }\n      other { 扫描 }\n    }\n  cis.cattle.io.clusterscanprofile: |-\n    {count, plural,\n      one { 配置文件 }\n      other { 配置文件 }\n    }\n  cis.cattle.io.clusterscanbenchmark: |-\n    {count, plural,\n      one { Benchmark 版本 }\n      other { Benchmark 版本 }\n    }\n  catalog.cattle.io.operation: |-\n    {count, plural,\n      one { 最近的操作 }\n      other { 最近的操作 }\n    }\n  catalog.cattle.io.app: |-\n    {count, plural,\n      one { 已安装的 App }\n      other { 已安装的 Apps }\n    }\n  catalog.cattle.io.clusterrepo: |-\n    {count, plural,\n      one { Chart 仓库 }\n      other { Chart 仓库 }\n    }\n  catalog.cattle.io.repo: |-\n    {count, plural,\n      one { Namespaced Repo }\n      other { Namespaced Repos }\n    }\n  chartInstallAction: |-\n    {count, plural,\n      one { App }\n      other { Apps }\n    }\n  chartUpgradeAction: |-\n    {count, plural,\n      one { App }\n      other { Apps }\n    }\n  endpoints: |-\n    {count, plural,\n      one { Endpoint }\n      other { Endpoints }\n    }\n  fleet.cattle.io.cluster: |-\n    {count, plural,\n      =1 { 集群 }\n      other { 集群 }\n    }\n  fleet.cattle.io.clustergroup: |-\n    {count, plural,\n      one { 集群组 }\n      other { 集群组 }\n    }\n  fleet.cattle.io.gitrepo: |-\n    {count, plural,\n      one { Git 仓库 }\n      other {Git 仓库 }\n    }\n  management.cattle.io.authconfig: |-\n    {count, plural,\n      one { Auth Provider }\n      other { Auth Providers }\n    }\n  management.cattle.io.feature: |-\n    {count, plural,\n      one { Feature Flag }\n      other { Feature Flags }\n    }\n  management.cattle.io.setting: |-\n    {count, plural,\n      one { 高级设置 }\n      other { 高级设置 }\n    }\n  management.cattle.io.fleetworkspace: |-\n    {count, plural,\n      one { 工作空间 }\n      other { 工作空间 }\n    }\n  #pruh-mee-thee-eyes https://www.prometheus.io/docs/introduction/faq/#what-is-the-plural-of-prometheus\n  monitoring.coreos.com.prometheus: |-\n    {count, plural,\n      one { Prometheus }\n      other { Prometheis }\n    }\n  monitoring.coreos.com.servicemonitor: |-\n    {count, plural,\n      one { 服务监控 }\n      other { 服务监控 }\n    }\n  monitoring.coreos.com.alertmanager: |-\n    {count, plural,\n      one { 告警管理 }\n      other { 告警管理 }\n    }\n  monitoring.coreos.com.podmonitor: |-\n    {count, plural,\n      one { Pod 监控 }\n      other { Pod 监控 }\n    }\n  monitoring.coreos.com.prometheusrule: |-\n    {count, plural,\n      one { Prometheus 规则 }\n      other { Prometheus 规则 }\n    }\n  monitoring.coreos.com.thanosruler: |-\n    {count, plural,\n      one { Thanos 规则 }\n      other { Thanos 规则 }\n    }\n  monitoring.coreos.com.receiver: |-\n    {count, plural,\n      one { 接收者 }\n      other { 接收者 }\n    }\n  monitoring.coreos.com.route: |-\n    {count, plural,\n      one { 通知 }\n      other { 通知 }\n    }\n  'management.cattle.io.cluster': |-\n    {count, plural,\n      one { 所有集群 }\n      other { 所有集群 }\n    }\n  'cluster.x-k8s.io.cluster': |-\n    {count, plural,\n      one { CAPI集群 }\n      other { CAPI集群 }\n    }\n  'rancher.cattle.io.cluster': |-\n    {count, plural,\n      one { 集群 }\n      other { 集群 }\n    }\n  'management.cattle.io.user': |-\n    {count, plural,\n      one { 用户 }\n      other { 用户 }\n    }\n  group.principal: |-\n    {count, plural,\n      one { 组 }\n      other { 组 }\n    }\n  token: |-\n    {count, plural,\n      one { API密钥 }\n      other { API密钥 }\n    }\n\naction:\n  clone: 克隆\n  disable: 禁用\n  download: 下载 YAML\n  edit: 编辑配置\n  editYaml: 编辑 YAML\n  enable: 启用\n  openLogs: 查看日志\n  refresh: 刷新\n  remove: 删除\n  view: 查看配置\n  viewInApi: API 查看\n  activate: 激活\n  deactivate: 停用\n  show: 显示\n  hide: 隐藏\n  copy: 复制\n  unassign: 取消分配\n\n\nunit:\n  sec: secs\n  min: mins\n  hour: |-\n    {count, plural,\n      one { 小时 }\n      other { 小时 }\n    }\n  day: |-\n    {count, plural,\n      one { 天 }\n      other { 天 }\n    }\nworkloadPorts:\n  addPort: 添加\n  remove: 移除\n  addHost: 添加主机\n\npodAffinity:\n  addLabel: 添加 Pod 选择器\n\nkeyValue:\n  keyPlaceholder: '例如: foo'\n  valuePlaceholder: '例如: bar'\n\n###############################\n### 高级设置\n###############################\n\nadvancedSettings:\n  label: 高级设置\n  subtext: 一般用户不需要改变这些。请谨慎操作，一旦输入了不正确的值，会破坏你的{appName}安装。从默认设置中定制的设置被标记为 \"已修改\"。\n  show: 显示\n  hide: 隐藏\n  none: 无\n  edit:\n    label: 编辑设置\n    changeSetting: \"修改设置\"\n    trueOption: \"True\"\n    falseOption: \"False\"\n    value: 值\n    useDefault: 复制默认值\n    invalidJSON: 无效的JSON - 请在保存前检查并修改您的输入。\n  descriptions:\n    'cacerts': \"验证服务器的证书所需的CA证书。\"\n    'cluster-defaults': '在创建新集群时覆盖RKE默认值。'\n    'engine-install-url': '默认的Docker引擎安装URL（用于大多数节点驱动）。'\n    'engine-iso-url': '默认的操作系统安装URL（用于vSphere驱动）。'\n    'engine-newest-version': '在本次发布时，最新的Docker支持版本。 不满足支持的Docker范围但比这更新的Docker版本将被标记为未测试。'\n    'engine-supported-range': '支持Docker引擎版本的Semver范围。 不符合这个范围的版本将在用户界面中被标记为不支持。'\n    'ingress-ip-domain': '用于自动生成Ingress主机名的通配符DNS域。<ingress-name>.<namespace-name>.<ingress controller的ip地址>将被添加到该域。'\n    'server-url': '默认的{appName}安装网址。必须是HTTPS。你的集群中的所有节点都必须能够到达这里。'\n    'system-default-registry': '用于所有系统Docker镜像的私有仓库。'\n    'ui-index': 'UI的HTML索引。'\n    'ui-pl': 'Private-Label company name.'\n    'ui-issues': '使用一个URL地址来发送新的 \"提交问题 \"报告，而不是将用户发送到GitHub问题页面。'\n    'telemetry-opt': '遥测报告opt-in。'\n    'auth-user-info-max-age-seconds': '在进行认证提供者组成员同步之前，用户认证令牌的最大存活时间。'\n    'auth-user-info-resync-cron': '重新同步认证提供者组成员资格的默认cron时间表。'\n    'cluster-template-enforcement': '非管理员将被限制只能通过预先批准的RKE模板启动集群。'\n    'auth-user-session-ttl-minutes': '用户认证会话的自定义TTL（以分钟为单位）。'\n    'auth-token-max-ttl-minutes': '自定义一个授权令牌的最大TTL（以分钟为单位）。'\n    'rke-metadata-config': '配置RKE元数据刷新参数。'\n    'ui-banners': '分类横幅是用来在页眉、页脚或两者中显示一个自定义的固定横幅。'\n    'ui-default-landing': '用户在登录后登陆的默认页面。'\n  editHelp:\n    'ui-banners': 这个设置需要一个JSON对象，包含3个根参数；<code>banner</code>, <code>showHeader</code>, <code>showFooter</code>。<code>banner</code>是一个包含；<code>textColor</code>, <code>background</code>, 和<code>text</code>的对象，其中<code>textColor</code>和<code>background</code>是任何有效的CSS颜色值。\n  #enum:\n    #'ui-default-landing':\n     # ember: Cluster Manager\n      #vue: Cluster Explorer\n    #'telemetry-opt':\n      #prompt: Prompt\n      #in: Opt-in to Telemetry\n      #out: Opt-out of Telemetry\n\nfeatureFlags:\n  label: 功能标志\n  warning: 功能标志允许Rancher将某些功能关在标志后面。你应该谨慎地启用这些功能，它们应该被视为测试版功能，可能会给你的系统带来问题。此外，有些功能（非动态）需要重新启动Rancher服务器才能启用。改变非动态功能将重新启动Rancher pods，这将导致短暂的停电。\n  promptActivate: 请确认您要激活功能标志\"{flag}\"。\n  promptDeactivate: 请确认你想停用功能标志\"{flag}\"。\n  restartRequired: \"注意：更新该功能标志需要重新启动\"\n\n##############################\n### Support Page\n##############################\n\nsupport:\n  community:\n    title: SUSE Rancher 提供世界一流的支持\n    register: 已经购买支持？请添加您的SUSE订阅ID。\n    addSubscription: 添加订阅ID\n    linksTitle: 社区支持\n    learnMore: 了解更多关于SUSE Rancher支持的信息\n  suse:\n    title: \"好消息--你得到了保障\"\n  promos:\n    one:\n      title: 全天候7x24小时支持\n      text: 我们提供严格定义的服务水平协议，并提供全天候的支持选项。\n    two:\n      title: 解决问题\n      text: 满怀信心地运行 SUSE Rancher 产品，因为我们知道构建这些产品的开发人员可以快速解决问题。\n    three:\n      title: 问题排查\n      text: 我们专注于发现任何问题的根源，无论它是否与Rancher产品、Kubernetes、Docker或你的底层基础设施有关。\n    four:\n      title: 自由创新\n      text: 利用我们与众多的Kubernetes厂商、操作系统和开源软件的认证兼容性。\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/__tests__/backendHelper.spec.ts",
    "content": "/** @jest-environment node */\n\nimport _ from 'lodash';\n\nimport type { VMExecutor } from '@pkg/backend/backend';\nimport type BackendHelperType from '@pkg/backend/backendHelper';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nconst modules = mockModules({\n  electron: undefined,\n});\n\ndescribe('BackendHelper', () => {\n  let BackendHelper: typeof BackendHelperType;\n  beforeAll(async() => {\n    BackendHelper = (await import('@pkg/backend/backendHelper')).default;\n  });\n\n  describe('configureMobyStorage', () => {\n    const snapshotterDir = '/var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots/';\n    const classicDir = '/var/lib/docker/image/overlay2/imagedb/content/sha256/'; // no-spell-check\n    const DOCKER_DAEMON_JSON = '/etc/docker/daemon.json';\n\n    interface Options {\n      hasSnapshotter: boolean;\n      hasClassic:     boolean;\n      useWASM:        boolean;\n      storageDriver:  'classic' | 'snapshotter' | 'auto';\n    }\n\n    class mockExecutor implements Partial<VMExecutor> {\n      readonly options: Omit<Options, 'useWASM' | 'storageDriver'> & { existingConfig?: string; };\n      readonly backend = 'mock';\n      result:           any;\n\n      constructor(options: typeof this.options) {\n        this.options = options;\n      }\n\n      execCommand(...command: string[]): Promise<void>;\n      execCommand(options: unknown, ...command: string[]): Promise<void>;\n      execCommand(options: unknown, ...command: string[]): Promise<string>;\n      execCommand(options?: unknown, ...command: string[]): Promise<void> | Promise<string> {\n        if (typeof options === 'string') {\n          command.unshift(options);\n        }\n        switch (command[0]) {\n        case '/usr/bin/find':\n          expect(options).toHaveProperty('capture', true);\n          if (command.includes(snapshotterDir)) {\n            return Promise.resolve(this.options.hasSnapshotter ? 'some text\\n' : '\\n');\n          }\n          if (command.includes(classicDir)) {\n            return Promise.resolve(this.options.hasClassic ? 'not empty\\n' : '\\n');\n          }\n          break;\n        case 'mkdir':\n          return Promise.resolve();\n        }\n        throw new Error(`Unexpected command: ${ JSON.stringify(command) }`);\n      }\n\n      readFile(filePath: string): Promise<string> {\n        if (filePath === DOCKER_DAEMON_JSON) {\n          if (this.options.existingConfig) {\n            return Promise.resolve(this.options.existingConfig);\n          }\n          return Promise.reject<string>(new Error('file does not exist'));\n        }\n        throw new Error(`Unexpected readFile: ${ filePath }`);\n      }\n\n      writeFile(filePath: string, fileContents: string): Promise<void> {\n        expect(filePath).toEqual(DOCKER_DAEMON_JSON);\n        const config = JSON.parse(fileContents);\n\n        this.result = config;\n        expect(config).toHaveProperty('features.containerd-snapshotter');\n        return Promise.resolve();\n      }\n    }\n\n    async function runTest(options: Options): Promise<boolean> {\n      const vmx = new mockExecutor(options);\n\n      await BackendHelper.configureMobyStorage(\n        vmx as unknown as VMExecutor,\n        options.storageDriver,\n        options.useWASM);\n\n      expect(vmx.result).toHaveProperty('features.containerd-snapshotter');\n\n      return vmx.result.features['containerd-snapshotter'] ?? false;\n    }\n\n    function generateCases(alwaysUseWASM: boolean) {\n      const cases: Omit<Options, 'storageDriver'>[] = [];\n      const bools = [true, false];\n\n      for (const hasSnapshotter of bools) {\n        for (const hasClassic of bools) {\n          if (!alwaysUseWASM) {\n            for (const useWASM of bools) {\n              cases.push({ hasSnapshotter, hasClassic, useWASM });\n            }\n          } else {\n            cases.push({ hasSnapshotter, hasClassic, useWASM: true });\n          }\n        }\n      }\n\n      return cases;\n    }\n\n    it.concurrent.each(generateCases(false))(\n      'should use classic storage driver when specified (snapshotter:$hasSnapshotter classic:$hasClassic wasm:$useWASM)', async(options) => {\n        await expect(runTest({ ...options, storageDriver: 'classic' })).resolves.toBeFalsy();\n      });\n\n    it.concurrent.each(generateCases(false))(\n      'should use snapshotter storage driver when specified (snapshotter:$hasSnapshotter classic:$hasClassic wasm:$useWASM)', async(options) => {\n        await expect(runTest({ ...options, storageDriver: 'snapshotter' })).resolves.toBeTruthy();\n      });\n\n    it.concurrent.each(generateCases(true))(\n      'should choose storage driver based on WASM configuration when set to auto (snapshotter:$hasSnapshotter classic:$hasClassic)', async(options) => {\n        await expect(runTest({ ...options, useWASM: true, storageDriver: 'auto' })).resolves.toBeTruthy();\n      });\n\n    it.concurrent.each`\n    hasSnapshotter   | hasClassic | expected\n    ${ true }        | ${ true }  | ${ true }\n    ${ true }        | ${ false } | ${ true }\n    ${ false }       | ${ true }  | ${ false }\n    ${ false }       | ${ false } | ${ true }\n    `('should choose storage driver based on existing usage when set to auto and WASM disabled (snapshotter:$hasSnapshotter classic:$hasClassic)', async(options) => {\n      await expect(runTest({ ...options, useWASM: false, storageDriver: 'auto' })).resolves.toBe(options.expected);\n    });\n\n    it('should preserve existing docker daemon settings', async() => {\n      const existingConfig = {\n        hello: 'world',\n      };\n\n      const vmx = new mockExecutor({\n        hasSnapshotter:  false,\n        hasClassic:      true,\n        existingConfig:  JSON.stringify(existingConfig),\n      });\n\n      await BackendHelper.configureMobyStorage(\n        vmx as unknown as VMExecutor,\n        'auto',\n        false);\n\n      expect(vmx.result).toHaveProperty('features.containerd-snapshotter');\n      expect(vmx.result).toHaveProperty('hello', 'world');\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/__tests__/k3sHelper.spec.ts",
    "content": "/** @jest-environment node */\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\n\nimport { jest } from '@jest/globals';\nimport semver from 'semver';\n\nimport { SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport paths from '@pkg/utils/paths';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nimport type { ReleaseAPIEntry } from '../k3sHelper';\n\nconst cachePath = path.join(paths.cache, 'k3s-versions.json');\n\nconst modules = mockModules({\n  '@pkg/utils/logging': undefined,\n  electron:             undefined,\n});\n\nconst { default: K3sHelper, buildVersion, ChannelMapping, NoCachedK3sVersionsError } = await import('../k3sHelper');\n\nlet cacheData: Buffer | null;\n\nbeforeAll(() => {\n  try {\n    cacheData = fs.readFileSync(cachePath);\n  } catch (err) {\n    cacheData = null;\n  }\n});\nafterAll(() => {\n  if (cacheData) {\n    fs.writeFileSync(cachePath, cacheData);\n  } else {\n    fs.rmSync(cachePath);\n  }\n});\n\nbeforeEach(() => {\n  modules.electron.net.fetch.mockReset();\n});\n\ndescribe(buildVersion, () => {\n  test('parses the build number', () => {\n    expect(buildVersion(new semver.SemVer('v1.99.3+k3s4'))).toEqual(4);\n  });\n\n  test('handles non-conforming versions', () => {\n    expect(buildVersion(new semver.SemVer('v1.99.3'))).toEqual(-1);\n  });\n});\n\ndescribe(K3sHelper, () => {\n  describe('processVersion', () => {\n    let subject: InstanceType<typeof K3sHelper>;\n    const process = (name: string, existing: string[] = [], hasAssets = false) => {\n      const assets: ReleaseAPIEntry['assets'] = [];\n\n      if (hasAssets) {\n        for (const name of Object.values(subject['filenames'])) {\n          if (typeof name === 'string') {\n            assets.push({ name, browser_download_url: name });\n          } else {\n            assets.push({ name: name[0], browser_download_url: name[0] });\n          }\n        }\n      }\n\n      for (const version of existing) {\n        const parsed = new semver.SemVer(version);\n\n        subject['versions'][parsed.version] = new SemanticVersionEntry(parsed);\n      }\n\n      return subject['processVersion']({ tag_name: name, assets });\n    };\n\n    beforeEach(() => {\n      subject = new K3sHelper('x86_64');\n      // Note that we _do not_ initialize this, i.e. we don't trigger an\n      // initial fetch of the releases.  Instead, we pretend that is done.\n      subject['pendingInitialize'] = Promise.resolve();\n    });\n    it('should skip invalid versions', async() => {\n      expect(process('xxx')).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(0);\n    });\n    it('should skip prereleases', async() => {\n      expect(process('1.99.3-beta1')).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(0);\n    });\n    it('should skip valid but erroneous versions', async() => {\n      expect(process('1.99.3+rk3s1')).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(0);\n    });\n    it('should ignore old versions', async() => {\n      expect(process('1.2.0')).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(0);\n    });\n    it('should ignore obsolete builds', async() => {\n      expect(process('1.99.3+k3s4', ['1.99.3+k3s5'])).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(1);\n    });\n    it('should ignore existing builds', async() => {\n      expect(process('1.99.3+k3s4', ['1.99.3+k3s4'])).toEqual(false);\n      expect(await subject.availableVersions).toHaveLength(1);\n    });\n    it('should ignore versions with missing assets', async() => {\n      expect(process('1.99.3+k3s4')).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(0);\n    });\n    it('should add versions', async() => {\n      expect(process('1.99.3+k3s4', [], true)).toEqual(true);\n      expect(await subject.availableVersions).toHaveLength(1);\n    });\n  });\n\n  test('cache read/write', async() => {\n    const subject = new K3sHelper('x86_64');\n    const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-test-cache-'));\n    const versions: Record<string, SemanticVersionEntry> = {\n      '1.99.3': new SemanticVersionEntry(semver.parse('1.99.3+k3s1')!, ['stable']),\n      '2.3.4':  new SemanticVersionEntry(semver.parse('2.3.4+k3s3')!),\n    };\n    const versionStrings = Object.values(versions)\n      .map(v => v.version)\n      .sort((a, b) => a.compare(b))\n      .map(v => v.raw);\n\n    try {\n      // We need to cast to any in order to override readonly.\n      (subject as any).cachePath = path.join(workDir, 'cache.json');\n      subject['versions'] = versions;\n      await subject['writeCache']();\n\n      const actual = JSON.parse(await fs.promises.readFile(subject['cachePath'], 'utf8'));\n      const { versions: actualStrings, channels }: { versions: string[], channels: Record<string, string> } = actual;\n\n      expect(actual).toHaveProperty('cacheVersion');\n      expect(semver.sort(actualStrings)).toEqual(versionStrings);\n      expect(channels).toEqual({ stable: '1.99.3' });\n\n      // Check that we can load the values back properly\n      subject['versions'] = {};\n      await subject['readCache']();\n      expect(subject['versions']).toEqual(versions);\n    } finally {\n      await util.promisify(fs.rm)(workDir, { recursive: true, force: true });\n    }\n  });\n\n  test('updateCache', async() => {\n    const subject = new K3sHelper('x86_64');\n    const validAssets = Object.values(subject['filenames']).map((name) => {\n      if (typeof name === 'string') {\n        return { name, browser_download_url: name };\n      } else {\n        return { name: name[0], browser_download_url: name[0] };\n      }\n    });\n\n    // Override cache reading to return a fake existing cache.\n    // The first read returns nothing to trigger a synchronous update;\n    // the rest of the reads return mocked values.\n    jest.spyOn(subject as any, 'readCache')\n      .mockResolvedValueOnce(undefined)\n      .mockImplementation(function(this: InstanceType<typeof K3sHelper>) {\n        const result = new ChannelMapping();\n\n        for (const [version, tags] of Object.entries({\n          'v1.99.1+k3s1': ['stale-tag'],\n          'v1.99.3+k3s1': ['stable'],\n        })) {\n          const parsedVersion = new semver.SemVer(version);\n\n          this.versions[parsedVersion.version] = new SemanticVersionEntry(parsedVersion, tags);\n          for (const tag of tags) {\n            result[tag] = parsedVersion;\n          }\n        }\n\n        return Promise.resolve(result);\n      });\n    subject['writeCache'] = jest.fn(() => Promise.resolve());\n    // On rate limiting, continue immediately.\n    subject['delayForWaitLimiting'] = jest.fn(() => Promise.resolve());\n\n    // Fake out the results\n    modules.electron.net.fetch\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual(subject['channelApiUrl']);\n\n        return Promise.resolve(new Response(\n          JSON.stringify({\n            resourceType: 'channels',\n            data:         [{\n              type:   'channel',\n              name:   'stable',\n              latest: 'v1.99.3+k3s3',\n            }],\n          }),\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual(subject['releaseApiUrl']);\n\n        return Promise.resolve(new Response(\n          JSON.stringify([\n            { tag_name: 'v1.99.3+k3s2', assets: validAssets },\n            { tag_name: 'v1.99.3+k3s3', assets: validAssets },\n            // The next one is skipped because there's a newer build\n            { tag_name: 'v1.99.3+k3s1', assets: validAssets },\n            { tag_name: 'v1.99.4+k3s1', assets: [] },\n            { tag_name: 'v1.99.1+k3s2', assets: validAssets },\n          ]),\n          { headers: { link: '<url>; rel=\"next\"' } },\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual('url');\n\n        return Promise.resolve(new Response(\n          undefined,\n          { status: 403, headers: { 'X-RateLimit-Remaining': '0' } },\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual('url');\n\n        return Promise.resolve(new Response(\n          JSON.stringify([\n            { tag_name: 'Invalid tag name', assets: validAssets },\n            { tag_name: 'v1.99.0+k3s5', assets: validAssets },\n          ]),\n          { headers: { link: '<url>; rel=\"first\"' } },\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        throw new Error(`Unexpected fetch call to ${ url }`);\n      });\n\n    // Ensure the Latch is set up in K3sHelper\n    subject.networkReady();\n\n    await subject.initialize();\n    expect(modules.electron.net.fetch).toHaveBeenCalledTimes(4);\n    expect(subject['delayForWaitLimiting']).toHaveBeenCalledTimes(1);\n    expect(await subject.availableVersions).toEqual([\n      new SemanticVersionEntry(new semver.SemVer('v1.99.3+k3s3'), ['stable']),\n      new SemanticVersionEntry(new semver.SemVer('v1.99.1+k3s2')),\n      new SemanticVersionEntry(new semver.SemVer('v1.99.0+k3s5')),\n    ]);\n  });\n\n  test('updateCache with new versions', async() => {\n    const subject = new K3sHelper('x86_64');\n    const validAssets = Object.values(subject['filenames']).map((name) => {\n      if (typeof name === 'string') {\n        return { name, browser_download_url: name };\n      } else {\n        return { name: name[0], browser_download_url: name[0] };\n      }\n    });\n\n    // Override cache reading to return a fake existing cache.\n    // The first read returns nothing to trigger a synchronous update;\n    // the rest of the reads return mocked values.\n    jest.spyOn(subject, 'readCache' as any)\n      .mockResolvedValueOnce(undefined)\n      .mockImplementation(function(this: InstanceType<typeof K3sHelper>) {\n        const result = new ChannelMapping();\n\n        for (const [version, tags] of Object.entries({\n          'v1.96.0+k3s2': [],\n          'v1.96.1+k3s1': [],\n          'v1.96.2+k3s1': [],\n          'v1.96.3+k3s1': ['v1.96', 'stable'],\n          'v1.97.1+k3s1': [],\n          'v1.97.2+k3s1': [],\n          'v1.97.3+k3s1': [],\n          'v1.97.4+k3s1': [],\n          'v1.97.5+k3s1': ['v1.97', 'latest'],\n        })) {\n          const parsedVersion = new semver.SemVer(version);\n\n          this.versions[parsedVersion.version] = new SemanticVersionEntry(parsedVersion, tags);\n          for (const tag of tags) {\n            result[tag] = parsedVersion;\n          }\n        }\n\n        subject['versionFromChannel'] = {\n          stable:  '1.96.3',\n          latest:  '1.97.5',\n          'v1.96': '1.96.3',\n          'v1.97': '1.97.5',\n        };\n\n        return Promise.resolve(result);\n      });\n    subject['writeCache'] = jest.fn(() => Promise.resolve());\n\n    // Fake out the results\n    modules.electron.net.fetch\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual(subject['channelApiUrl']);\n\n        return Promise.resolve(new Response(\n          JSON.stringify({\n            resourceType: 'channels',\n            data:         [\n              {\n                type: 'channel', name: 'v1.96', latest: '1.96.9+k3s1',\n              },\n              {\n                type: 'channel', name: 'v1.97', latest: '1.97.7+k3s1',\n              },\n              {\n                type: 'channel', name: 'stable', latest: '1.97.7+k3s1',\n              },\n              {\n                type: 'channel', name: 'latest', latest: '1.98.3+k3s1',\n              },\n              {\n                type: 'channel', name: 'v1.98', latest: '1.98.3+k3s1',\n              },\n            ],\n          }),\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        expect(url).toEqual(subject['releaseApiUrl']);\n\n        return Promise.resolve(new Response(\n          JSON.stringify([\n            { tag_name: 'v1.98.3+k3s2', assets: validAssets },\n            { tag_name: 'v1.98.2+k3s2', assets: validAssets },\n            { tag_name: 'v1.98.1+k3s2', assets: validAssets },\n            { tag_name: 'v1.97.7+k3s2', assets: validAssets },\n            { tag_name: 'v1.97.6+k3s1', assets: validAssets },\n          ]),\n          { headers: { link: '<url>; rel=\"first\"' } },\n        ));\n      })\n      .mockImplementationOnce((url) => {\n        throw new Error(`Unexpected fetch call to ${ url }`);\n      });\n\n    // Ensure the Latch is set up in K3sHelper\n    subject.networkReady();\n\n    await subject.initialize();\n    expect(modules.electron.net.fetch).toHaveBeenCalledTimes(2);\n    const availableVersions = await subject.availableVersions;\n\n    expect(availableVersions).toEqual([\n      new SemanticVersionEntry(new semver.SemVer('v1.98.3+k3s2'), ['latest', 'v1.98']),\n      new SemanticVersionEntry(new semver.SemVer('v1.98.2+k3s2')),\n      new SemanticVersionEntry(new semver.SemVer('v1.98.1+k3s2')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.7+k3s2'), ['stable', 'v1.97']),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.6+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.5+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.4+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.3+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.2+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.97.1+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.96.3+k3s1'), ['v1.96']),\n      new SemanticVersionEntry(new semver.SemVer('v1.96.2+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.96.1+k3s1')),\n      new SemanticVersionEntry(new semver.SemVer('v1.96.0+k3s2')),\n    ]);\n\n    // Verify versionFromChannel is updated after channel assignment\n    expect(subject['versionFromChannel']).toEqual({\n      'v1.96':  '1.96.3',\n      'v1.97':  '1.97.7',\n      'v1.98':  '1.98.3',\n      stable:  '1.97.7',\n      latest:  '1.98.3',\n    });\n  });\n\n  describe('initialize', () => {\n    it('should finish initialize without network if cache is available', async() => {\n      const writer = new K3sHelper('x86_64');\n\n      writer['versions'] = { 'v1.99.0': new SemanticVersionEntry(new semver.SemVer('v1.99.0')) };\n      await writer['writeCache']();\n\n      // We want to check that initialize() returns before updateCache() does.\n\n      const subject = new K3sHelper('x86_64');\n      const pendingInit = new Promise((resolve) => {\n        // We need a cast on updateCache here since it's a protected method.\n        jest.spyOn(subject, 'updateCache' as any).mockImplementation(async() => {\n          // This will be called, but will not block initialization.\n          await pendingInit;\n\n          return [];\n        });\n        subject.initialize().then(resolve);\n      });\n\n      expect(await subject.availableVersions).toContainEqual({\n        version:  semver.parse('v1.99.0'),\n        channels: undefined,\n      });\n      await pendingInit;\n    });\n\n    it('should not reset versionFromChannel when called multiple times', async() => {\n      const subject = new K3sHelper('x86_64');\n\n      // Mock readCache to populate versionFromChannel\n      jest.spyOn(subject as any, 'readCache').mockImplementation(function(this: InstanceType<typeof K3sHelper>) {\n        this.versions['1.99.3'] = new SemanticVersionEntry(semver.parse('1.99.3+k3s1')!, ['stable']);\n        this.versionFromChannel['stable'] = '1.99.3';\n\n        return Promise.resolve();\n      });\n\n      // Mock updateCache to not actually run\n      jest.spyOn(subject as any, 'updateCache').mockResolvedValue(undefined);\n\n      // First initialization\n      await subject.initialize();\n      expect(subject['versionFromChannel']).toEqual({ stable: '1.99.3' });\n\n      // Second initialization should not reset versionFromChannel\n      await subject.initialize();\n      expect(subject['versionFromChannel']).toEqual({ stable: '1.99.3' });\n    });\n  });\n\n  describe('selectClosestSemVer', () => {\n    const subject = K3sHelper;\n    const table = [\n      ['finds the oldest newer major version', 'v3.1.2+k3s3',\n        ['v1.2.9+k3s1', 'v1.2.9+k3s4', 'v4.2.8+k3s1', 'v4.3.0+k3s1'], 'v4.2.8+k3s1'],\n      ['finds the oldest newer minor version', 'v1.12.2+k3s3',\n        ['v1.2.9+k3s1', 'v1.7.0+k3s1', 'v1.29.9+k3s4', 'v2.12.8+k3s1'], 'v1.29.9+k3s4'],\n      ['finds the oldest newer patch version at the start of the list', 'v1.12.2+k3s3',\n        ['v1.12.4+k3s1', 'v1.12.4+k3s4', 'v1.12.8+k3s1', 'v1.12.9+k3s4'], 'v1.12.4+k3s4'],\n      ['finds the oldest newer patch version inside the list', 'v1.12.10+k3s99',\n        ['v1.12.4+k3s1', 'v1.12.8+k3s1', 'v1.12.9+k3s1', 'v1.12.20+k3s4'], 'v1.12.20+k3s4'],\n      ['settles on the newest older version', 'v1.12.11+k3s5',\n        ['v1.12.4+k3s1', 'v1.12.4+k3s4', 'v1.12.8+k3s1', 'v1.12.9+k3s4'], 'v1.12.9+k3s4'],\n      ['favor a lower build number for same version over a newer version', 'v1.2.9+k3s2',\n        ['v1.2.8+k3s1', 'v1.2.9+k3s1', 'v1.2.10+k3s1', 'v1.2.10+k3s2'], 'v1.2.9+k3s1'],\n      ['finds the highest build version over single digits', 'v1.2.9+k3s2',\n        ['v1.2.8+k3s1', 'v1.2.9+k3s1', 'v1.2.9+k3s4', 'v1.3.0+k3s1'], 'v1.2.9+k3s4'],\n      ['finds the highest build version over double digits', 'v1.2.9+k3s11',\n        ['v1.2.9+k3s9', 'v1.2.9+k3s15', 'v1.2.9+k3s16', 'v1.3.0+k3s1'], 'v1.2.9+k3s16'],\n      ['can handle non-conforming inputs', 'v1.2.3+k3s4',\n        ['v1.2.2+k3s1', 'oswald', 'rabbit', 'v1.2.4+k3s4'], 'v1.2.4+k3s4'],\n    ] as const;\n\n    test.each(table)('%s', (title: string, desiredVersion: string, cachedFilenames: readonly [string, string, string, string], expected: string) => {\n      const desiredSemver = new semver.SemVer(desiredVersion);\n      const selectedSemVer = subject['selectClosestSemVer'](desiredSemver, cachedFilenames as unknown as string[]);\n\n      expect(selectedSemVer).toHaveProperty('raw', expected);\n    });\n\n    test('can handle zero choices', () => {\n      const desiredSemver = new semver.SemVer('v1.99.3+k3s4');\n\n      expect(() => subject['selectClosestSemVer'](desiredSemver, [])).toThrow(NoCachedK3sVersionsError);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/backend.ts",
    "content": "import fs from 'fs';\nimport stream from 'stream';\n\nimport { Settings } from '@pkg/config/settings';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport EventEmitter from '@pkg/utils/eventEmitter';\nimport { RecursiveKeys, RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';\n\nimport type { ContainerEngineClient } from './containerClient';\nimport type { KubernetesBackend } from './k8s';\n\nexport enum State {\n  STOPPED = 'STOPPED', // The engine is not running.\n  STARTING = 'STARTING', // The engine is attempting to start.\n  STARTED = 'STARTED', // The engine is started; the dashboard is not yet ready.\n  STOPPING = 'STOPPING', // The engine is attempting to stop.\n  ERROR = 'ERROR', // There is an error and we cannot recover automatically.\n  DISABLED = 'DISABLED', // The container backend is ready but the Kubernetes engine is disabled.\n}\n\nexport class BackendError extends Error {\n  constructor(name: string, message: string, fatal = false) {\n    super(message);\n    this.name = name;\n    this.fatal = fatal;\n  }\n\n  readonly fatal: boolean;\n}\n\nexport interface BackendProgress {\n  /** The current progress; valid values are 0 to max. */\n  current:         number,\n  /** Maximum progress possible; if less than zero, the progress is indeterminate. */\n  max:             number,\n  /** Details on the current action. */\n  description?:    string,\n  /** When we entered this progress state. */\n  transitionTime?: Date,\n}\n\nexport type Architecture = 'x86_64' | 'aarch64';\n\nexport interface FailureDetails {\n  /** The last lima/wsl command run: */\n  lastCommand?:       string,\n  lastCommandComment: string,\n  lastLogLines:       string[],\n}\n\n/**\n * KubernetesBackendEvents describes the events that may be emitted by a\n * Kubernetes backend (as an EventEmitter).  Each property name is the name of\n * an event, and the property type is the type of the callback function expected\n * for the given event.\n */\nexport interface BackendEvents {\n  /**\n   * Emitted when there has been a change in the progress in the current action.\n   * The progress can be read off the `progress` member on the backend.\n   */\n  'progress'(): void;\n\n  /**\n   * Emitted when the state of the backend has changed.\n   */\n  'state-changed'(state: State): void;\n\n  /**\n   * Show a notification to the user.\n   */\n  'show-notification'(options: Electron.NotificationConstructorOptions): void;\n}\n\n/**\n * Settings that KubernetesBackend can access.\n */\nexport type BackendSettings = RecursiveReadonly<Settings>;\n\n/**\n * Reasons that the backend might need to restart, as returned from\n * `requiresRestartReasons()`.\n * @returns A mapping of the preference causing the restart to the changed\n *          values.\n */\nexport type RestartReasons = Partial<Record<RecursiveKeys<Settings>, {\n  /**\n   * The currently active value.\n   */\n  current:  any;\n  /**\n   * The desired value (which must be different from the current value to\n   * require a restart).\n   */\n  desired:  any;\n  /**\n   * The severity of the restart; this may be set to `reset` for some values\n   * indicating that there will be data loss.\n   */\n  severity: 'restart' | 'reset';\n}>>;\n\n/**\n * VMBackend describes a controller for managing a virtual machine upon which\n * Rancher Desktop runs.\n */\nexport interface VMBackend extends EventEmitter<BackendEvents> {\n  /** The name of the VM backend */\n  readonly backend: 'wsl' | 'lima' | 'mock';\n\n  readonly state: State;\n\n  /** The number of CPUs in the running VM, or 0 if the VM is not running. */\n  readonly cpus: Promise<number>;\n\n  /** The amount of memory in the VM, in MiB, or 0 if the VM is not running. */\n  readonly memory: Promise<number>;\n\n  /** Progress for the current action. */\n  readonly progress: Readonly<BackendProgress>;\n\n  /**\n   * Whether debug mode is enabled. If this is set, the implementation should\n   * emit extra debug logging if possible.\n   */\n  debug: boolean;\n\n  /**\n   * Check if the current backend is valid.\n   * @returns Null if the backend is valid; otherwise, an error describing why\n   * the backend is invalid that can be shown to the user.\n   */\n  getBackendInvalidReason(): Promise<BackendError | null>;\n\n  /**\n   * Start the Kubernetes cluster.  If it is already started, it will be\n   * restarted.\n   */\n  start(config: BackendSettings): Promise<void>;\n\n  /** Stop the Kubernetes cluster.  If applicable, shut down the VM. */\n  stop(): Promise<void>;\n\n  /** Delete the Kubernetes cluster, returning the exit code. */\n  del(): Promise<void>;\n\n  /** Reset the Kubernetes cluster, removing all workloads. */\n  reset(config: BackendSettings): Promise<void>;\n\n  /**\n   * Apply the settings update that does not require a backend restart.\n   */\n  handleSettingsUpdate(config: BackendSettings): Promise<void>;\n\n  /**\n   * Check if applying the given settings would require the backend to restart.\n   */\n  requiresRestartReasons(config: RecursivePartial<BackendSettings>): Promise<RestartReasons>;\n\n  /**\n   * Get the external IP address where the services would be listening on, if\n   * available.  For VM-based systems, this would be the address of the VM's\n   * network interface.  This address may be undefined if the backend is\n   * currently not in a state that supports services; for example, if the VM is\n   * off.\n   */\n  readonly ipAddress: Promise<string | undefined>;\n\n  /**\n   * If called after a backend operation fails, this returns a block of data that attempts\n   * to give more information about what command was being run when the error happened.\n   *\n   * @param [exception] The associated exception.\n   */\n  getFailureDetails(exception: any): Promise<FailureDetails>;\n\n  /**\n   * If true, the backend cannot invoke any dialog boxes and needs to find an alternative.\n   */\n  noModalDialogs: boolean;\n\n  readonly executor:              VMExecutor;\n  readonly kubeBackend:           KubernetesBackend;\n  readonly containerEngineClient: ContainerEngineClient;\n}\n\n/**\n * execOptions is options for VMExecutor.\n */\nexport type execOptions = childProcess.CommonOptions & {\n  /** Expect the command to fail; do not log on error.  Exceptions are still thrown. */\n  expectFailure?: boolean;\n  /** A custom log stream to write to; must have a file descriptor. */\n  logStream?:     stream.Writable;\n  /**\n   * If set, ensure that the command is run as the privileged user.\n   * @note The command is always run as root on WSL.\n   */\n  root?:          boolean;\n};\n\n/**\n * VMExecutor describes how to run commands in the virtual machine.\n */\nexport interface VMExecutor {\n  /**\n   * The backend in use.\n   */\n  readonly backend: VMBackend['backend'];\n\n  /**\n   * execCommand runs the given command in the virtual machine.\n   * @param execOptions Execution options.  If capture is set, standard output is\n   *    returned.\n   * @param command The command to execute.\n   */\n  execCommand(...command: string[]): Promise<void>;\n  execCommand(options: execOptions, ...command: string[]): Promise<void>;\n  execCommand(options: execOptions & { capture: true }, ...command: string[]): Promise<string>;\n\n  /**\n   * spawn the given command in the virtual machine, returning the child\n   * process itself.\n   * @note On Windows, this will be within the network / pid namespace.\n   * @param options Execution options.\n   * @param command The command to execute.\n   */\n  spawn(...command: string[]): childProcess.ChildProcess;\n  spawn(options: execOptions, ...command: string[]): childProcess.ChildProcess;\n\n  /**\n   * Read the contents of the given file.  If the file is a symlink, the target\n   * will be read instead.\n   * @param filePath The path inside the VM to read.\n   * @param [options.encoding='utf-8'] The encoding of the file.\n   * @returns The contents of the file.\n   */\n  readFile(filePath: string): Promise<string>;\n  readFile(filePath: string, options: Partial<{ encoding: BufferEncoding }>): Promise<string>;\n\n  /**\n   * Write the given contents to a given file name in the VM.\n   * The file will be owned by root.\n   * @param filePath The destination file path, in the VM.\n   * @param fileContents The contents of the file.\n   * @param permissions The file permissions. Defaults to 0o644.\n   */\n  writeFile(filePath: string, fileContents: string): Promise<void>;\n  writeFile(filePath: string, fileContents: string, permissions: fs.Mode): Promise<void>;\n\n  /**\n   * Copy the given file from the host into the VM.\n   * @param hostPath The source path, on the host.\n   * @param vmPath The destination path, inside the VM.\n   * @note The behaviour of copying a directory is undefined.\n   */\n  copyFileIn(hostPath: string, vmPath: string): Promise<void>;\n\n  /**\n   * Copy the given file from the VM into the host.\n   * @param vmPath The source path, inside the VM.\n   * @param hostPath The destination path, on the host.\n   * @note The behaviour of copying a directory is undefined.\n   */\n  copyFileOut(vmPath: string, hostPath: string): Promise<void>;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/backendHelper.ts",
    "content": "import path from 'path';\n\nimport Electron from 'electron';\nimport merge from 'lodash/merge';\nimport semver from 'semver';\nimport yaml from 'yaml';\n\nimport CERT_MANAGER from '@pkg/assets/scripts/cert-manager.yaml';\nimport INSTALL_CONTAINERD_SHIMS_SCRIPT from '@pkg/assets/scripts/install-containerd-shims';\nimport CONTAINERD_CONFIG from '@pkg/assets/scripts/k3s-containerd-config.toml';\nimport SPIN_OPERATOR from '@pkg/assets/scripts/spin-operator.yaml';\nimport { BackendSettings, VMExecutor } from '@pkg/backend/backend';\nimport { LockedFieldError } from '@pkg/config/commandLineOptions';\nimport { ContainerEngine, Settings } from '@pkg/config/settings';\nimport * as settingsImpl from '@pkg/config/settingsImpl';\nimport SettingsValidator from '@pkg/main/commandServer/settingsValidator';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { minimumUpgradeVersion, SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { showMessageBox } from '@pkg/window';\n\nconst CONTAINERD_CONFIG_TOML = '/etc/containerd/config.toml';\nconst DOCKER_DAEMON_JSON = '/etc/docker/daemon.json';\n\nconst MANIFEST_DIR = '/var/lib/rancher/k3s/server/manifests';\n\n// Manifests are applied in sort order, so use a prefix to load them last, in the required sequence.\n// Names should start with `z` followed by a digit, so that `install-k3s` cleans them up on restart.\nexport const MANIFEST_RUNTIMES = 'z100-runtimes';\nexport const MANIFEST_CERT_MANAGER_CRDS = 'z110-cert-manager.crds';\nexport const MANIFEST_CERT_MANAGER = 'z115-cert-manager';\nexport const MANIFEST_SPIN_OPERATOR_CRDS = 'z120-spin-operator.crds';\nexport const MANIFEST_SPIN_OPERATOR = 'z125-spin-operator';\n\nconst STATIC_DIR = '/var/lib/rancher/k3s/server/static/rancher-desktop';\nconst STATIC_CERT_MANAGER_CHART = `${ STATIC_DIR }/cert-manager.tgz`;\nconst STATIC_SPIN_OPERATOR_CHART = `${ STATIC_DIR }/spin-operator.tgz`;\n\nconst console = Logging.kube;\n\nexport default class BackendHelper {\n  /**\n   * Workaround for upstream error https://github.com/containerd/nerdctl/issues/1308\n   * Nerdctl client (version 0.22.0 +) wants a populated auths field when credsStore gives credentials.\n   * Note that we don't have to actually provide credentials in the value part of the `auths` field.\n   * The code currently wants to see a `ServerURL` that matches the well-known docker hub registry URL,\n   * even though it isn't needed, because at that point the code knows it's using the well-known registry.\n   */\n  static ensureDockerAuth(existingConfig: Record<string, any>): Record<string, any> {\n    return merge({ auths: { 'https://index.docker.io/v1/': {} } }, existingConfig);\n  }\n\n  /**\n   * Replacer function for string.replaceAll(/(\\\\*)(\")/g, this.escapeChar)\n   * It will backslash-escape the specified character unless it is already\n   * preceded by an odd number of backslashes.\n   */\n  private static escapeChar(_: any, slashes: string, char: string) {\n    if (slashes.length % 2 === 0) {\n      slashes += '\\\\';\n    }\n\n    return `${ slashes }${ char }`;\n  }\n\n  /**\n   * Turn allowedImages patterns into a list of nginx regex rules.\n   */\n  static createAllowedImageListConf(allowedImages: BackendSettings['containerEngine']['allowedImages']): string {\n    /**\n     * The image allow list config file consists of one line for each pattern using nginx pattern matching syntax.\n     * It starts with '~*' for case-insensitive matching, followed by a regular expression, which should be\n     * anchored to the beginning and end of the string with '^...$'. The pattern must be followed by ' 0;' and\n     * a newline. The '0' means that this pattern is **not** forbidden (the table defaults to '1').\n     */\n\n    // TODO: remove hard-coded defaultSandboxImage from cri-dockerd\n    let patterns = '\"~*^registry\\\\.k8s\\\\.io(:443)?/v2/pause/manifests/[^/]+$\" 0;\\n';\n\n    // TODO: remove hardcoded CDN redirect target for registry.k8s.io\n    patterns += '\"~*^[^./]+\\\\.pkg\\\\.dev(:443)?/v2/.+/manifests/[^/]+$\" 0;\\n';\n\n    // TODO: remove hard-coded sandbox_image from our /etc/containerd/config.toml\n    patterns += '\"~*^registry-1\\\\.docker\\\\.io(:443)?/v2/rancher/mirrored-pause/manifests/[^/]+$\" 0;\\n';\n\n    for (const pattern of allowedImages.patterns) {\n      let host = 'registry-1.docker.io';\n      // escape all unescaped double-quotes because the final pattern will be quoted to avoid nginx syntax errors\n      let repo = pattern.replaceAll(/(\\\\*)(\")/g, this.escapeChar).split('/');\n\n      // no special cases for 'localhost' and 'host-without-dot:port'; they won't work within the VM\n      if (repo[0].includes('.')) {\n        host = repo.shift()!;\n        if (host === 'docker.io') {\n          host = 'registry-1.docker.io';\n          // 'docker.io/busybox' means 'registry-1.docker.io/library/busybox'\n          if (repo.length === 1) {\n            repo.unshift('library');\n          }\n        }\n        // registry without repo is the same as 'registry//'\n        if (repo.length === 0) {\n          repo = ['', ''];\n        }\n      } else if (repo.length < 2) {\n        repo.unshift('library');\n      }\n\n      // all dots in the host name are literal dots, but don't escape them if they are already escaped\n      host = host.replaceAll(/(\\\\*)(\\.)/g, this.escapeChar);\n      // matching against http_host header, which may or may not include the port\n      if (!host.includes(':')) {\n        host += '(:443)?';\n      }\n\n      // match for \"image:tag@digest\" (tag and digest are both optional)\n      const match = /^(?<image>.*?)(:(?<tag>.*?))?(@(?<digest>.*))?$/.exec(repo[repo.length - 1]);\n      let tag = '[^/]+';\n\n      // Strip tag and digest from last fragment of the image name.\n      // `match` and `match.groups` can't be `null` because the regular expression will match the empty string,\n      // but TypeScript can't know that.\n      if (match?.groups?.tag || match?.groups?.digest) {\n        repo.pop();\n        repo.push(match.groups.image);\n        // actual tag is ignored when a digest is specified\n        tag = match.groups.digest || match.groups.tag;\n      }\n\n      // special wildcard rules: 'foo//' means 'foo/.+' and 'foo/' means 'foo/[^/]+'\n      if (repo[repo.length - 1] === '') {\n        repo.pop();\n        if (repo.length > 0 && repo[repo.length - 1] === '') {\n          repo.pop();\n          repo.push('.+');\n        } else {\n          repo.push('[^/]+');\n        }\n      }\n      patterns += `\"~*^${ host }/v2/${ repo.join('/') }/manifests/${ tag }$\" 0;\\n`;\n    }\n\n    return patterns;\n  }\n\n  static requiresCRIDockerd(engineName: string, kubeVersion: string | semver.SemVer): boolean {\n    if (engineName !== ContainerEngine.MOBY) {\n      return false;\n    }\n    const ranges = [\n      // versions 1.24.1 to 1.24.3 don't support the --docker option\n      '1.24.1 - 1.24.3',\n      // cri-dockerd bundled with k3s is not compatible with docker 25.x (using API 1.44)\n      // see https://github.com/k3s-io/k3s/issues/9279\n      '1.26.8 - 1.26.13',\n      '1.27.5 - 1.27.10',\n      '1.28.0 - 1.28.6',\n      '1.29.0 - 1.29.1',\n    ];\n\n    return semver.satisfies(kubeVersion, ranges.join('||'));\n  }\n\n  static checkForLockedVersion(newVersion: semver.SemVer, cfg: BackendSettings, sv: SettingsValidator): void {\n    const [, errors] = sv.validateSettings(cfg as Settings, { kubernetes: { version: newVersion.raw } }, settingsImpl.getLockedSettings());\n\n    if (errors.length > 0) {\n      if (errors.some(err => /field \".*\" is locked/.exec(err))) {\n        throw new LockedFieldError(`Error in deployment profiles:\\n${ errors.join('\\n') }`);\n      } else {\n        throw new Error(`Validation errors for requested version ${ newVersion }: ${ errors.join('\\n') }`);\n      }\n    }\n  }\n\n  /**\n   * Validate the cfg.kubernetes.version string\n   * If it's valid and available, use it.\n   * Otherwise fall back to the minimum upgrade version (highest patch release of lowest available version).\n   */\n  static async getDesiredVersion(cfg: BackendSettings, availableVersions: SemanticVersionEntry[], noModalDialogs: boolean, settingsWriter: (_: any) => void): Promise<semver.SemVer> {\n    const currentConfigVersionString = cfg?.kubernetes?.version;\n    let storedVersion: semver.SemVer | null;\n    let matchedVersion: SemanticVersionEntry | undefined;\n    const invalidK8sVersionMainMessage = `Requested kubernetes version '${ currentConfigVersionString }' is not a supported version.`;\n    const sv = new SettingsValidator();\n    const lockedSettings = settingsImpl.getLockedSettings();\n    const versionIsLocked = lockedSettings.kubernetes?.version ?? false;\n\n    // If we're here either there's no existing cfg.k8s.version, or it isn't valid\n    if (!availableVersions.length) {\n      if (currentConfigVersionString) {\n        console.log(invalidK8sVersionMainMessage);\n      } else {\n        console.log('Internal error: no available kubernetes versions found.');\n      }\n      throw new Error('No kubernetes version available.');\n    }\n\n    const upgradeVersion = minimumUpgradeVersion(availableVersions);\n\n    if (!upgradeVersion) {\n      // This should never be reached, as `availableVersions` isn't empty.\n      throw new Error('Failed to find upgrade version.');\n    }\n\n    sv.k8sVersions = availableVersions.map(v => v.version.version);\n    if (currentConfigVersionString) {\n      storedVersion = semver.parse(currentConfigVersionString);\n      if (storedVersion) {\n        matchedVersion = availableVersions.find((v) => {\n          try {\n            return semver.eq(v.version, storedVersion!);\n          } catch (err: any) {\n            console.error(`Can't compare versions ${ storedVersion } and ${ v }: `, err);\n            if (!(err instanceof TypeError)) {\n              return false;\n            }\n            // We haven't seen a non-TypeError exception here, but it would be worthwhile to have it reported.\n            // This throw will cause the exception to appear in a non-fatal error reporting dialog box.\n            throw err;\n          }\n        });\n        if (matchedVersion) {\n          // This throws a LockedFieldError if it fails.\n          this.checkForLockedVersion(matchedVersion.version, cfg, sv);\n\n          return matchedVersion.version;\n        } else if (versionIsLocked) {\n          // This is a bit subtle. If we're here, the user specified a nonexistent version in the locked manifest.\n          // We can't switch to the default version, so throw a fatal error.\n          throw new LockedFieldError(`Locked kubernetes version ${ currentConfigVersionString } isn't available.`);\n        }\n      } else if (versionIsLocked) {\n        // If we're here, the user specified a non-version in the locked manifest.\n        // We can't switch to the default version, so throw a fatal error.\n        throw new LockedFieldError(`Locked kubernetes version '${ currentConfigVersionString }' isn't a valid version.`);\n      }\n      const message = invalidK8sVersionMainMessage;\n      const detail = `Falling back to recommended minimum upgrade version of ${ upgradeVersion.version.version }`;\n\n      if (noModalDialogs) {\n        console.log(`${ message } ${ detail }`);\n      } else {\n        const options: Electron.MessageBoxOptions = {\n          message,\n          detail,\n          type:    'warning',\n          buttons: ['OK'],\n          title:   'Invalid Kubernetes Version',\n        };\n\n        await showMessageBox(options, true);\n      }\n    }\n    // No (valid) stored version; save the default one.\n    // Because no version was specified, there can't be a locked version field, so no need to call checkForLockedVersion.\n    settingsWriter({ kubernetes: { version: upgradeVersion.version.version } });\n\n    return upgradeVersion.version;\n  }\n\n  /**\n   * Return a dictionary of all containerd shims installed in /usr/local/bin.\n   * Keys are the shim names and values are the filenames.\n   */\n  static async containerdShims(vmx: VMExecutor): Promise<Record<string, string>> {\n    const shims: Record<string, string> = {};\n\n    try {\n      const files = await vmx.execCommand({ capture: true }, '/bin/ls', '-1', '-p', '/usr/local/bin');\n\n      for (const file of files.split(/\\n/)) {\n        const match = /^containerd-shim-([-a-z]+)-v\\d+$/.exec(file);\n\n        if (match) {\n          shims[match[1]] = file;\n        }\n      }\n    } catch (e: any) {\n      console.log('containerdShims: Got exception:', e);\n      throw e;\n    }\n\n    return shims;\n  }\n\n  private static manifestFilename(manifest: string): string {\n    return `${ MANIFEST_DIR }/${ manifest }.yaml`;\n  }\n\n  /**\n   * Write a k3s manifest to define a runtime class for each installed containerd shim.\n   */\n  static async configureRuntimeClasses(vmx: VMExecutor) {\n    const runtimes = [];\n\n    for (const shim in await BackendHelper.containerdShims(vmx)) {\n      runtimes.push({\n        apiVersion: 'node.k8s.io/v1',\n        kind:       'RuntimeClass',\n        metadata:   { name: shim },\n        handler:    shim,\n      });\n    }\n\n    // Don't let k3s define runtime classes, only use the ones defined by Rancher Desktop.\n    await vmx.execCommand({ root: true }, 'touch', `${ MANIFEST_DIR }/runtimes.yaml.skip`);\n\n    if (runtimes.length > 0) {\n      const manifest = runtimes.map(r => yaml.stringify(r)).join('---\\n');\n\n      await vmx.writeFile(this.manifestFilename(MANIFEST_RUNTIMES), manifest, 0o644);\n    }\n  }\n\n  /**\n   * Write k3s manifests to install cert-manager and spinkube operator\n   */\n  static async configureSpinOperator(vmx: VMExecutor) {\n    await Promise.all([\n      vmx.copyFileIn(path.join(paths.resources, 'cert-manager.crds.yaml'), this.manifestFilename(MANIFEST_CERT_MANAGER_CRDS)),\n      vmx.copyFileIn(path.join(paths.resources, 'cert-manager.tgz'), STATIC_CERT_MANAGER_CHART),\n      vmx.writeFile(this.manifestFilename(MANIFEST_CERT_MANAGER), CERT_MANAGER, 0o644),\n\n      vmx.copyFileIn(path.join(paths.resources, 'spin-operator.crds.yaml'), this.manifestFilename(MANIFEST_SPIN_OPERATOR_CRDS)),\n      vmx.copyFileIn(path.join(paths.resources, 'spin-operator.tgz'), STATIC_SPIN_OPERATOR_CHART),\n      vmx.writeFile(this.manifestFilename(MANIFEST_SPIN_OPERATOR), SPIN_OPERATOR, 0o644),\n    ]);\n  }\n\n  /**\n   * Install containerd-wasm shims into /usr/local/containerd-shims (and symlinks into /usr/local/bin).\n   */\n  static async installContainerdShims(vmx: VMExecutor, configureWASM: boolean) {\n    // Calling install-containerd-shims without source dirs will remove the symlinks from /usr/local/bin.\n    const sourceDirs: string[] = [];\n\n    if (configureWASM) {\n      sourceDirs.push(\n        // Copy shims bundled with the app itself first, user-managed shims may override.\n        path.join(paths.resources, 'linux', 'internal'),\n        paths.containerdShims,\n      );\n    }\n    await vmx.execCommand({ root: true }, 'mkdir', '-p', '/root');\n    await vmx.writeFile('/root/install-containerd-shims', INSTALL_CONTAINERD_SHIMS_SCRIPT, 'a+x');\n    await vmx.execCommand({ root: true }, '/root/install-containerd-shims', ...sourceDirs);\n  }\n\n  /**\n   * Write the containerd config file. If WASM is enabled, include a runtime definition\n   * for each installed containerd shim.\n   */\n  static async writeContainerdConfig(vmx: VMExecutor, configureWASM: boolean): Promise<void> {\n    let config = CONTAINERD_CONFIG;\n\n    if (configureWASM) {\n      const shims = await BackendHelper.containerdShims(vmx);\n\n      for (const shim in shims) {\n        config += '\\n';\n        config += `[plugins.\"io.containerd.grpc.v1.cri\".containerd.runtimes.${ shim }]\\n`;\n        config += `  runtime_type = \"/usr/local/bin/${ shims[shim] }\"\\n`;\n      }\n    }\n\n    await vmx.writeFile(CONTAINERD_CONFIG_TOML, config);\n  }\n\n  /**\n   * Configure the Moby containerd-snapshotter feature if WASM support is\n   * requested, or if we have not previously run the daemon.\n   */\n  static async configureMobyStorage(vmx: VMExecutor, storageDriver: 'classic' | 'snapshotter' | 'auto', configureWASM: boolean) {\n    // Due to issues with a botched migration, we will need to provide more logic\n    // to determine whether to use the containerd-snapshotter backend for moby\n    // storage.  See https://github.com/rancher-sandbox/rancher-desktop/issues/9732\n    // for more details.\n\n    // If this directory is not empty, we assume that there is data in the containerd snapshotter.\n    const snapshotterDir = '/var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots/';\n    // If this directory is not empty, we assume that there is data in the classic storage.\n    const classicDir = '/var/lib/docker/image/overlay2/imagedb/content/sha256/'; // no-spell-check\n\n    let useSnapshotter: boolean | undefined;\n\n    // Check if a directory (in the VM) has any subdirectories or files.\n    async function dirHasChildren(dir: string): Promise<boolean> {\n      try {\n        const stdout = await vmx.execCommand(\n          { root: true, expectFailure: true, capture: true },\n          '/usr/bin/find', dir, '-maxdepth', '0', '-not', '-empty');\n\n        return stdout.trim().length > 0;\n      } catch {\n        // Directory does not exist.\n        return false;\n      }\n    }\n\n    const hasSnapshotterData = await dirHasChildren(snapshotterDir);\n    const hasClassicData = await dirHasChildren(classicDir);\n\n    // If `storageDriver` is explicitly set, use that setting.\n    if (storageDriver !== 'auto') {\n      useSnapshotter = (storageDriver === 'snapshotter');\n    } else if (configureWASM) {\n      // WASM requires the containerd snapshotter.\n      useSnapshotter = true;\n    } else if (hasSnapshotterData) {\n      // If there is data in the containerd snapshotter store, use it.\n      useSnapshotter = true;\n    } else {\n      // If there is no data in the classic storage, use containerd snapshotter.\n      useSnapshotter = !hasClassicData;\n    }\n    mainEvents.emit('diagnostics-event', {\n      id: 'moby-storage',\n      hasClassicData,\n      hasSnapshotterData,\n      useSnapshotter,\n    });\n\n    let config: Record<string, any>;\n\n    try {\n      config = JSON.parse(await vmx.readFile(DOCKER_DAEMON_JSON));\n    } catch (err: any) {\n      await vmx.execCommand({ root: true }, 'mkdir', '-p', path.dirname(DOCKER_DAEMON_JSON));\n      config = {};\n    }\n    config['min-api-version'] = '1.41';\n    config['features'] ??= {};\n    config['features']['containerd-snapshotter'] = useSnapshotter;\n\n    if (config['features']['containerd-snapshotter']) {\n      // If we are using the containerd snapshotter, create /var/lib/docker/image\n      // to avoid breaking cri-dockerd.\n      await vmx.execCommand({ root: true }, 'mkdir', '-p', '/var/lib/docker/image');\n    }\n    await vmx.writeFile(DOCKER_DAEMON_JSON, jsonStringifyWithWhiteSpace(config), 0o644);\n  }\n\n  static async configureContainerEngine(vmx: VMExecutor, configureWASM: boolean, mobyStorageDriver: 'classic' | 'snapshotter' | 'auto') {\n    await BackendHelper.installContainerdShims(vmx, configureWASM);\n    await BackendHelper.writeContainerdConfig(vmx, configureWASM);\n    await BackendHelper.configureMobyStorage(vmx, mobyStorageDriver, configureWASM);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/__tests__/auth.spec.ts",
    "content": "/** @jest-environment node */\n\nimport { jest } from '@jest/globals';\n\nimport * as childProcess from '@pkg/utils/childProcess';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nconst modules = mockModules({\n  '@pkg/utils/childProcess': {\n    ...childProcess,\n    spawnFile: jest.fn(childProcess.spawnFile),\n  },\n  electron: undefined,\n});\n\nconst { default: RegistryAuth } = await import('@pkg/backend/containerClient/auth');\n\ndescribe('RegistryAuth', () => {\n  describe('parseAuthHeader', () => {\n    const testCases: {\n      input:    string,\n      expected: { scheme: string, parameters?: Record<string, string> }[],\n    }[] = [\n      { input: '', expected: [] },\n      {\n        input:    'Basic',\n        expected: [{ scheme: 'basic' }],\n      },\n      {\n        input:    'Bearer realm=\"https://auth.docker.io/token\",service=\"registry.docker.io\"',\n        expected: [{ scheme: 'bearer', parameters: { realm: 'https://auth.docker.io/token', service: 'registry.docker.io' } }],\n      },\n      {\n        input:    'one,two,three',\n        expected: [{ scheme: 'one' }, { scheme: 'two' }, { scheme: 'three' }],\n      },\n      {\n        input:    'broken quotes=\"value starts but never ends',\n        expected: [{ scheme: 'broken', parameters: { quotes: 'value starts but never ends' } }],\n      },\n      {\n        input:    'Token one=1,two=2, Other three=\"3\", four=\"4\"',\n        expected: [{ scheme: 'token', parameters: { one: '1', two: '2' } }, { scheme: 'other', parameters: { three: '3', four: '4' } }],\n      },\n      {\n        input:    'parameter=unused, token',\n        expected: [{ scheme: 'token' }],\n      },\n      {\n        input:    'token parameter=, other',\n        expected: [{ scheme: 'token', parameters: { parameter: '' } }, { scheme: 'other' }],\n      },\n      {\n        // From RFC 9110, section 11.6.1\n        input:    'Basic realm=\"simple\", Newauth realm=\"apps\", type=1, title=\"Login to \\\\\"apps\\\\\"\"',\n        expected: [\n          { scheme: 'basic', parameters: { realm: 'simple' } },\n          {\n            scheme:     'newauth',\n            parameters: {\n              realm: 'apps', type: '1', title: 'Login to \"apps\"',\n            },\n          },\n        ],\n      },\n    ];\n\n    test.each(testCases)('$#: $input', ({ input, expected }) => {\n      const actual = RegistryAuth['parseAuthHeader'](input);\n\n      expect(actual).toEqual(expected.map(v => ({ parameters: {}, ...v })));\n    });\n  });\n\n  describe('findAuth', () => {\n    it('should not fail when failing to list known credentials', async() => {\n      const exception = new Error('failed to spawn file');\n\n      modules['@pkg/utils/childProcess'].spawnFile.mockRejectedValue(exception);\n      await expect(RegistryAuth['findAuth']('example.test')).resolves.toBeUndefined();\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/__tests__/client.spec.ts",
    "content": "/** @jest-environment node */\n\nimport { jest } from '@jest/globals';\n\nimport { ContainerEngineClient } from '@pkg/backend/containerClient/types';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nconst modules = mockModules({\n  '@pkg/backend/mock': {\n    default: jest.fn(),\n  },\n  '@pkg/backend/containerClient/registry': {\n    default: {\n      getTags: jest.fn((_name: string) => Promise.resolve<string[]>([])),\n    },\n  },\n  electron: undefined,\n});\n\nconst { default: MockBackend } = await import('@pkg/backend/mock');\nconst { NerdctlClient } = await import('@pkg/backend/containerClient/nerdctlClient');\nconst { MobyClient } = await import('@pkg/backend/containerClient/mobyClient');\n\ndescribe.each(['nerdctl', 'moby'] as const)('%s', (clientName) => {\n  let subject: ContainerEngineClient;\n\n  beforeEach(() => {\n    const executor = new MockBackend() as jest.Mocked<InstanceType<typeof MockBackend>>;\n\n    switch (clientName) {\n    case 'nerdctl':\n      subject = new NerdctlClient(executor);\n      break;\n    case 'moby':\n      subject = new MobyClient(executor, '');\n      break;\n    default:\n      throw new Error(`Unexpected client name ${ clientName }`);\n    }\n  });\n\n  describe('getTags', () => {\n    const repository = 'registry.test/name';\n    let registryTags: string[];\n    let localTags: string[];\n    let localExtras: string[];\n\n    beforeEach(() => {\n      registryTags = [];\n      localTags = [];\n      localExtras = [];\n\n      modules['@pkg/backend/containerClient/registry'].default.getTags.mockImplementation((name) => {\n        expect(name).toEqual(repository);\n\n        if (registryTags.length) {\n          return Promise.resolve(registryTags);\n        }\n\n        return Promise.reject('Could not get tags from registry');\n      });\n\n      jest.spyOn(subject, 'runClient').mockImplementation((args, stdio) => {\n        expect(args).toEqual(expect.arrayContaining(['image', 'list']));\n        expect(stdio).toEqual('pipe');\n\n        const results: string[] = [];\n\n        if (localTags.length) {\n          results.push(...localTags.map(t => `${ repository }:${ t }`));\n        }\n        if (localExtras.length) {\n          results.push(...localExtras);\n        }\n\n        if (results.length) {\n          return Promise.resolve({ stdout: results.join('\\n') });\n        }\n\n        // We need the cast to any because `runClient()` is overloaded and\n        // it's hard to convince TypeScript that the return value is fine.\n        return Promise.reject('Could not get tags locally') as any;\n      });\n    });\n\n    afterEach(() => {\n      jest.restoreAllMocks();\n      jest.resetAllMocks();\n    });\n\n    it('should list tags from the registry', async() => {\n      registryTags = ['apple', 'banana'];\n\n      await expect(subject.getTags(repository)).resolves.toEqual(new Set(registryTags));\n    });\n\n    it('should list local tags', async() => {\n      localTags = ['carrot', 'durian'];\n      localExtras = ['irrelevant:grape', 'registry.invalid/other:honeydew'];\n\n      await expect(subject.getTags(repository)).resolves.toEqual(new Set(localTags));\n    });\n\n    it('should merge tags', async() => {\n      registryTags = ['jackfruit', 'kiwi'];\n      localTags = ['kiwi', 'lemon'];\n\n      await expect(subject.getTags(repository)).resolves.toEqual(new Set([...registryTags, ...localTags]));\n    });\n\n    it('should ignore errors', async() => {\n      await expect(subject.getTags(repository)).resolves.toEqual(new Set());\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/__tests__/registry.spec.ts",
    "content": "/** @jest-environment node */\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nconst modules = mockModules({ electron: undefined });\nconst { default: dockerRegistry } = await import('@pkg/backend/containerClient/registry');\n\ndescribe('DockerRegistry', () => {\n  beforeEach(() => {\n    // We need to send actual network requests in this test.\n    modules.electron.net.fetch.mockImplementation(fetch);\n  });\n  describe('getTags', () => {\n    it.skip('should get tags from unauthenticated registry', async() => {\n      // Sometimes this URL is broken, returning 504 Gateway Time-out\n      // It shouldn't be used for a unit test anyway.\n      const reference = 'registry.opensuse.org/opensuse/leap';\n\n      await expect(dockerRegistry.getTags(reference))\n        .resolves\n        .toEqual(expect.arrayContaining(['15.4']));\n    });\n\n    it('should get tags from docker hub', async() => {\n      await expect(dockerRegistry.getTags('hello-world'))\n        .resolves\n        .toEqual(expect.arrayContaining(['linux']));\n    });\n\n    it('should fail trying to get tags from invalid registry', async() => {\n      await expect(dockerRegistry.getTags('host.invalid/name'))\n        .rejects\n        .toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/auth.ts",
    "content": "import { net } from 'electron';\n\nimport runCredentialCommand from '@pkg/main/credentialServer/credentialUtils';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.background;\n\ninterface tokenCacheEntry {\n  /** The expiry of this token, as milliseconds since Unix epoch. */\n  expiry: number;\n  /** The raw token. */\n  token:  string;\n}\n\n/**\n * RegistryAuth handles HTTP authentication for the registries\n */\nclass RegistryAuth {\n  /**\n   * A cache of still-valid tokens, keyed by (registry) host.\n   */\n  protected tokenCache: Record<string, tokenCacheEntry> = {};\n\n  /**\n   * Ask the credential helpers for authentication for the given host.\n   * @param hosts The hosts to find auth for; possibly a URL instead.\n   * @returns The value of the `Authorization` header to use.\n   */\n  protected async findAuth(...hosts: string[]): Promise<string | undefined> {\n    const candidates: string[] = [];\n    const hostCandidates: string[] = [];\n    const suffixes = ['/', ''];\n\n    for (const host of hosts) {\n      if (!host) {\n        continue;\n      }\n      if (host.includes('://')) {\n        // This is a full URL, parse it.\n        const url = new URL(host);\n\n        candidates.push(host);\n        hostCandidates.push(url.host, url.hostname);\n        suffixes.push(url.pathname);\n      } else {\n        hostCandidates.push(host);\n      }\n    }\n\n    if (hostCandidates.some(h => h === 'docker.io' || h.endsWith('.docker.io'))) {\n      // Special handling for docker (typically, `https://auth.docker.io/token`).\n      hostCandidates.push('index.docker.io');\n      suffixes.push('/v1/');\n    }\n\n    for (const protocol of ['http', 'https']) {\n      for (const hostPart of hostCandidates) {\n        for (const suffix of suffixes) {\n          candidates.push(`${ protocol }://${ hostPart }${ suffix }`);\n        }\n      }\n    }\n\n    let knownAuths: Record<string, { Username: string, Secret: string }>;\n\n    try {\n      knownAuths = JSON.parse(await runCredentialCommand('list'));\n    } catch (ex) {\n      // if we fail to list credentials, that's not an error (there's probably\n      // no docker config or something).\n      console.debug(`Failed to list known credentials: ${ ex }`);\n\n      return;\n    }\n\n    for (const candidate of candidates) {\n      if (candidate in knownAuths) {\n        try {\n          const auth = JSON.parse(await runCredentialCommand('get', candidate));\n          const login = Buffer.from(`${ auth.Username }:${ auth.Secret }`, 'utf-8');\n\n          return `Basic ${ login.toString('base64') }`;\n        } catch {\n          // Failure to get credentials from one helper isn't fatal.\n          continue;\n        }\n      }\n    }\n  }\n\n  /**\n   * HTTP Basic Authentication\n   */\n  protected async basicAuth(host: string): Promise<Record<string, string>> {\n    const auth = await this.findAuth(host);\n\n    if (auth) {\n      return { Authorization: auth };\n    }\n\n    throw new Error(`Could not find auth for ${ host }`);\n  }\n\n  /**\n   * HTTP Bearer Authentication\n   * @param host The host we're trying to authenticate against\n   * @param parameters The WWW-Authenticate header parameters.\n   */\n  protected async bearerAuth(host: string, parameters: Record<string, string>): Promise<Record<string, string>> {\n    // If we have a token in the cache, return it.\n    if (host in this.tokenCache) {\n      const cachedToken = this.tokenCache[host];\n\n      if (cachedToken.expiry > Date.now()) {\n        return { Authorization: `Bearer ${ cachedToken.token }` };\n      }\n      delete this.tokenCache[host];\n    }\n\n    const url = new URL(parameters.realm ?? (host.includes('://') ? host : `https://${ host }`));\n    const auth = await this.findAuth(parameters.realm, host);\n    const headers: HeadersInit = auth ? { Authorization: auth } : {};\n\n    if (parameters.service) {\n      url.searchParams.set('service', parameters.service);\n    }\n    if (parameters.scope) {\n      for (const scope of parameters.scope.split(/\\s+/)) {\n        url.searchParams.append('scope', scope);\n      }\n    }\n\n    const resp = await net.fetch(url.toString(), { headers });\n\n    if (!resp.ok) {\n      throw new Error(`Could not get authorization token from ${ url } (for ${ url }): ${ JSON.stringify(resp) }`);\n    }\n\n    let result: any;\n\n    try {\n      result = await resp.json();\n    } catch (ex) {\n      const error = new Error(`Failed to parse authorization response`);\n\n      (error as any).cause = ex;\n      throw error;\n    }\n\n    const parsed = {\n      token:      result.token || result.access_token,\n      issued_at:  result.issued_at ?? (new Date()).toISOString(),\n      expires_in: result.expires_in ?? 300,\n    };\n\n    type parsedKey = keyof typeof parsed;\n    const types: Record<parsedKey, 'string' | 'number'> = {\n      token:      'string',\n      issued_at:  'string',\n      expires_in: 'number',\n    };\n    let issuedDate: number;\n\n    for (const [k, type] of Object.entries(types) as [parsedKey, typeof types[parsedKey]][]) {\n      // eslint-disable-next-line valid-typeof -- The set is hard-coded.\n      if (typeof parsed[k] !== type) {\n        throw new TypeError(`Failed to read authorization response: ${ k } is not a ${ type } (${ typeof parsed[k] })`);\n      }\n    }\n\n    try {\n      issuedDate = Date.parse(parsed.issued_at);\n    } catch (ex) {\n      const error = new Error(`Failed to parse authorization response issued_at ${ parsed.issued_at }`);\n\n      (error as any).cause = ex;\n      throw error;\n    }\n\n    this.tokenCache[host] = {\n      expiry: issuedDate + parsed.expires_in * 1_000,\n      token:  parsed.token,\n    };\n\n    return { Authorization: `Bearer ${ parsed.token }` };\n  }\n\n  protected parseAuthHeader(header: string): { scheme: string, parameters: Record<string, string> }[] {\n    // This header is a bit tricky (hence a separate method for testing):\n    // The header may contain multiple comma-separated challenge specifications,\n    // each of which consists of one word (\"scheme\") plus zero or more comma-\n    // separated parameters for that scheme.  Parameters may have quoted values\n    // which may internally contain commas.\n\n    const results: { scheme: string, parameters: Record<string, string> }[] = [];\n    let scheme = '';\n    let parameters: Record<string, string> = {};\n\n    function push() {\n      if (scheme) {\n        results.push({ scheme, parameters });\n        parameters = {};\n      }\n    }\n\n    header = header.trim();\n    // From now on, `header` should never have leading/trailing whitespace.\n    while (header) {\n      const posMapping = {\n        space: /\\s/.exec(header)?.index ?? -1,\n        equal: header.indexOf('='),\n        comma: header.indexOf(','),\n        end:   header.length,\n      } as const;\n      const posList = (Object.entries(posMapping) as [keyof typeof posMapping, number][])\n        .filter(([, v]) => v >= 0)\n        .sort(([, l], [, r]) => l - r);\n      const [type, pos] = posList[0];\n\n      switch (type) {\n      case 'equal': {\n        // An equals sign precedes any spaces etc.; this is a parameter.\n        const key = header.substring(0, pos);\n        let value = '';\n\n        header = header.substring(pos + 1).trimStart();\n        if (header.startsWith('\"')) {\n          let quoteEnded = false;\n\n          header = header.substring(1);\n\n          while (!quoteEnded) {\n            const quotePosMapping = {\n              backslash: header.indexOf('\\\\'),\n              quote:     header.indexOf('\"'),\n              end:       header.length,\n            } as const;\n            const quotePosList = (Object.entries(quotePosMapping) as [keyof typeof quotePosMapping, number][])\n              .filter(([, v]) => v >= 0)\n              .sort(([, l], [, r]) => l - r);\n            const [quoteType, quotePos] = quotePosList[0];\n\n            switch (quoteType) {\n            case 'backslash': {\n              // We can get away with just treating the next character as\n              // a literal (no `\\n` for newline, etc.).\n              value += header.substring(0, quotePos);\n              header = header.substring(quotePos + 1);\n              if (header) {\n                value += header.substring(0, 1);\n                header = header.substring(1);\n              }\n              break;\n            }\n            case 'quote': {\n              value += header.substring(0, quotePos);\n              header = header.substring(quotePos + 1).replace(/^[,\\s]*/, '');\n              quoteEnded = true;\n              break;\n            }\n            case 'end': {\n              // Could not find end of quote\n              value += header;\n              header = '';\n              quoteEnded = true;\n              break;\n            }\n            }\n          }\n        } else {\n          // This value is not quoted\n          const commaPos = header.indexOf(',');\n\n          if (commaPos < 0) {\n            // No comma, the parameter runs to the end of the header.\n            value = header;\n            header = '';\n          } else {\n            value = header.substring(0, commaPos);\n            header = header.substring(commaPos).replace(/^[,\\s]*/, '');\n          }\n        }\n\n        if (scheme) {\n          // Only allow adding parameters if we already found a scheme.\n          parameters[key] = value;\n        }\n        break;\n      }\n      case 'space': {\n        // A space precedes any equals signs; this is a scheme.\n        push();\n        scheme = header.substring(0, pos).toLowerCase();\n        header = header.substring(pos).replace(/^[,\\s]*/, '');\n        break;\n      }\n      case 'end': {\n        // Neither space nor equal found.\n        // This is a bare scheme.\n        push();\n        scheme = header.trim().toLowerCase();\n        header = header.substring(scheme.length).trim();\n        break;\n      }\n      case 'comma': {\n        // This is a bare scheme.\n        push();\n        scheme = header.substring(0, pos);\n        header = header.substring(pos + 1).replace(/^[,\\s]*/, '');\n      }\n      }\n    }\n\n    push();\n\n    return results;\n  }\n\n  /**\n   * Determine authentication required.\n   * @param endpoint The endpoint to use to test for authentication requirements.\n   * @returns The headers needed for authentication.\n   */\n  async authenticate(endpoint: URL): Promise<Headers> {\n    if (endpoint.host in this.tokenCache) {\n      // If we have a valid cached token, use it directly.\n      const cachedToken = this.tokenCache[endpoint.host];\n\n      if (cachedToken.expiry > Date.now()) {\n        return new Headers({ Authorization: `Bearer ${ cachedToken.token }` });\n      }\n    }\n\n    const resp = await net.fetch(endpoint.toString());\n\n    if (resp.status !== 401) {\n      console.debug(`${ endpoint } does not require authentication`);\n\n      return new Headers();\n    }\n\n    const authenticateHeader = resp.headers.get('WWW-Authenticate') ?? '';\n\n    for (const challenge of this.parseAuthHeader(authenticateHeader)) {\n      if (challenge.scheme === 'basic') {\n        try {\n          return new Headers(await this.basicAuth(endpoint.toString()));\n        } catch (ex) {\n          console.debug(`Could not do Basic authentication for ${ endpoint }`, ex);\n        }\n      } else if (challenge.scheme === 'bearer') {\n        try {\n          return new Headers(await this.bearerAuth(endpoint.toString(), challenge.parameters));\n        } catch (ex) {\n          console.debug(`Could not do Bearer authentication for ${ endpoint }:`, ex);\n        }\n      } else {\n        console.debug(`Don't know how to do ${ challenge.scheme } authentication for ${ endpoint }, skipping`);\n      }\n    }\n\n    // If we reach here, we got a HTTP 401, but couldn't figure out how to do\n    // authentication.\n    throw new Error(`Failed to find compatible authentication scheme for ${ endpoint }`);\n  }\n}\n\nconst auth = new RegistryAuth();\n\nexport default auth;\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/index.ts",
    "content": "export * from './types';\nexport { MobyClient } from './mobyClient';\nexport { NerdctlClient } from './nerdctlClient';\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/mobyClient.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\nimport util from 'util';\n\nimport _ from 'lodash';\nimport tar from 'tar-stream';\n\nimport {\n  ContainerComposeExecOptions, ReadableProcess, WritableReadableProcess, ContainerComposeOptions,\n  ContainerEngineClient, ContainerRunOptions, ContainerStopOptions,\n  ContainerRunClientOptions, ContainerComposePortOptions, ContainerBasicOptions,\n} from './types';\n\nimport { VMExecutor } from '@pkg/backend/backend';\nimport dockerRegistry from '@pkg/backend/containerClient/registry';\nimport { ErrorCommand, spawn, spawnFile } from '@pkg/utils/childProcess';\nimport { parseImageReference } from '@pkg/utils/dockerUtils';\nimport Logging, { Log } from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport { defined } from '@pkg/utils/typeUtils';\n\nconst console = Logging.moby;\n\ntype runClientOptions = ContainerRunClientOptions & {\n  /** The executable to run, defaulting to this.executable (i.e. \"docker\") */\n  executable?: string;\n};\n\nexport class MobyClient implements ContainerEngineClient {\n  constructor(vm: VMExecutor, endpoint: string) {\n    this.vm = vm;\n    this.endpoint = endpoint;\n  }\n\n  readonly vm:       VMExecutor;\n  readonly executable = executable('docker');\n  readonly endpoint: string;\n\n  /**\n   * Run a list of cleanup functions in reverse.\n   */\n  protected async runCleanups(cleanups: (() => Promise<unknown>)[]) {\n    for (const cleanup of cleanups.reverse()) {\n      try {\n        await cleanup();\n      } catch (e) {\n        console.error('Failed to run cleanup:', e);\n      }\n    }\n  }\n\n  protected async makeContainer(imageID: string): Promise<string> {\n    const { stdout, stderr } = await this.runClient(['create', '--entrypoint=/', imageID], 'pipe');\n    const container = stdout.trim();\n\n    console.debug(stderr.trim());\n    if (!container) {\n      throw new Error(`Failed to create container ${ imageID }`);\n    }\n\n    return container;\n  }\n\n  async waitForReady(): Promise<void> {\n    let successCount = 0;\n    let failureCount = 0;\n    let lastOutput = { stdout: '', stderr: '' };\n\n    // Wait for ten consecutive successes, clearing out successCount whenever we\n    // hit an error.  In the ideal case this is a five-second delay in startup\n    // time.  We use `docker system info` because that needs to talk to the\n    // socket to fetch data about the engine (and it returns an error if it\n    // fails to do so).\n    while (successCount < 10) {\n      try {\n        await this.runClient(['system', 'info'], 'pipe');\n        successCount++;\n        failureCount = 0;\n      } catch (ex) {\n        successCount = 0;\n        failureCount++;\n        // If we've been erroring for a while, log the output.\n        if (failureCount > 10 && ex && typeof ex === 'object') {\n          const output = { stdout: '', stderr: '' };\n\n          if ('stdout' in ex && typeof ex.stdout === 'string') {\n            output.stdout = ex.stdout;\n          }\n          if ('stderr' in ex && typeof ex.stderr === 'string') {\n            output.stderr = ex.stderr;\n          }\n          if (output.stdout !== lastOutput.stdout || output.stderr !== lastOutput.stderr) {\n            console.error(`Failed to run docker system info after ${ failureCount } failures (will retry):`, output);\n            lastOutput = output;\n          }\n        }\n      }\n      await util.promisify(setTimeout)(500);\n    }\n  }\n\n  readFile(imageID: string, filePath: string): Promise<string>;\n  readFile(imageID: string, filePath: string, options: { encoding?: BufferEncoding; }): Promise<string>;\n  async readFile(imageID: string, filePath: string, options?: { encoding?: BufferEncoding }): Promise<string> {\n    const encoding = options?.encoding ?? 'utf-8';\n\n    console.debug(`Reading file ${ imageID }:${ filePath }`);\n\n    const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-moby-readfile-'));\n    const tempFile = path.join(workDir, path.basename(filePath));\n\n    // `docker cp ... -` returns a tar file, which isn't what we want.  It's\n    // easiest to just copy the file to disk and read it.\n    try {\n      await this.copyFile(imageID, filePath, workDir, { silent: true });\n\n      return await fs.promises.readFile(tempFile, { encoding });\n    } finally {\n      await fs.promises.rm(workDir, { recursive: true, maxRetries: 3 });\n    }\n  }\n\n  copyFile(imageID: string, sourcePath: string, destinationPath: string): Promise<void>;\n  copyFile(imageID: string, sourcePath: string, destinationPath: string, options: { silent?: true }): Promise<void>;\n  async copyFile(imageID: string, sourcePath: string, destinationPath: string, options?: { silent?: boolean }): Promise<void> {\n    const cleanups: (() => Promise<unknown>)[] = [];\n\n    if (!options?.silent) {\n      console.debug(`Copying ${ imageID }:${ sourcePath } to ${ destinationPath }`);\n    }\n\n    const container = await this.makeContainer(imageID);\n\n    cleanups.push(() => this.runClient(['rm', container], console));\n\n    try {\n      if (this.vm.backend === 'wsl') {\n        // On Windows, non-Administrators by default do not have the privileges\n        // to create symlinks.  However, `docker cp --follow-link` doesn't\n        // dereference symlinks it encounters when recursively copying a file.\n        // We work around this by copying it into a tarball in the VM and then\n        // extracting it from there.\n        const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-moby-cp-'));\n\n        cleanups.push(() => fs.promises.rm(workDir, {\n          recursive: true, force: true, maxRetries: 3,\n        }));\n        const archive = path.join(workDir, 'archive.tar');\n        const wslArchive = (await this.vm.execCommand({ capture: true }, '/bin/wslpath', '-u', archive)).trim();\n\n        await this.vm.execCommand(\n          '/bin/sh', '-c',\n          `/usr/bin/docker cp '${ container }:${ sourcePath }' - > '${ wslArchive }'`);\n        if (sourcePath.endsWith('/')) {\n          await this.extractArchive(archive, destinationPath, sourcePath);\n        } else {\n          // If we only archived a single file, there is no prefix in the archive.\n          await this.extractArchive(archive, destinationPath);\n        }\n      } else {\n        if (sourcePath.endsWith('/')) {\n          // If we're copying a directory, add \".\" so we don't create an extra\n          // directory.\n          sourcePath += '.';\n        }\n        await this.runClient(\n          ['cp', '--follow-link', `${ container }:${ sourcePath }`, destinationPath],\n          console);\n      }\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  /**\n   * Extract the given archive into the given directory, dereferencing symbolic\n   * links (because they are not supported on Windows).\n   * @param archive The archive to extract, as a host path.\n   * @param destination The destination directory, as a host path.\n   * @param stripPrefix A prefix to strip from the file path.\n   */\n  protected async extractArchive(archive: string, destination: string, stripPrefix = ''): Promise<void> {\n    const stripPrefixWithoutSlash = stripPrefix.replace(/^\\/+/, '');\n    // Because tar is a streaming format, we need to go over it twice: first, to\n    // extract the non-linked files, and to collect all links; then again, to\n    // extract any files that were pointed to by links.\n    const links: Record<string, string> = {};\n\n    // Convert a given path to an absolute path, ensuring that it resides\n    // within the destination.  If the name does not start with the prefix to be\n    // stripped, returns `undefined` and this entry should not be processed.\n    const absPath = (rawPath: string): string | undefined => {\n      let mungedPath = rawPath;\n\n      if (stripPrefix) {\n        if (mungedPath.startsWith(stripPrefixWithoutSlash)) {\n          mungedPath = mungedPath.substring(stripPrefixWithoutSlash.length);\n        } else {\n          // A prefix is given, but we found a file that doesn't match; we\n          // should skip this file.\n          return;\n        }\n      }\n      const normalized = path.normalize(path.join(destination, mungedPath));\n\n      if (/[/\\\\]\\.\\.[/\\\\]/.test(path.relative(destination, normalized))) {\n        throw new Error(`Error extracting archive: ${ normalized } is not in ${ destination }`);\n      }\n\n      return normalized;\n    };\n\n    for await (const entry of fs.createReadStream(archive).pipe(tar.extract())) {\n      switch (entry.header.type) {\n      case 'link': case 'symlink': {\n        const linkName = entry.header.name;\n        const realName = entry.header.linkname;\n\n        if (!realName) {\n          throw new Error(`Error extracting archive: ${ linkName } has no destination`);\n        }\n        if (realName.startsWith('/')) {\n          links[linkName] = realName;\n        } else {\n          links[linkName] = path.posix.join(path.posix.dirname(entry.header.name), realName);\n        }\n        await stream.promises.finished(entry.resume() as any);\n        break;\n      }\n      case 'directory': {\n        const dirName = absPath(entry.header.name);\n\n        if (!dirName) {\n          console.warn(`Skipping unexpected directory ${ entry.header.name }`);\n          continue;\n        }\n        await fs.promises.mkdir(dirName, { recursive: true });\n        await stream.promises.finished(entry.resume() as any);\n        console.debug(`Created directory ${ dirName }`);\n\n        break;\n      }\n      case 'file': case 'contiguous-file': {\n        const fileName = absPath(entry.header.name);\n\n        if (!fileName) {\n          console.warn(`Skipping unexpected file ${ entry.header.name }`);\n          continue;\n        }\n        await fs.promises.mkdir(path.dirname(fileName), { recursive: true });\n        await stream.promises.finished(entry.pipe(fs.createWriteStream(fileName)));\n        console.debug(`Wrote ${ fileName }`);\n\n        break;\n      }\n      default:\n        console.info(`Ignoring unsupported file type ${ entry.header.name } (${ entry.header.type })`);\n      }\n    }\n\n    /**\n     * Mapping from link destination to the link name.\n     * @note There can be multiple links pointing to the same file.\n     */\n    const reverseLinks: Record<string, string[]> = {};\n\n    for (const linkName in links) {\n      while (links[links[linkName]] && links[linkName] !== linkName) {\n        // The link points to another link; flatten it.\n        links[linkName] = links[links[linkName]];\n      }\n\n      reverseLinks[links[linkName]] ||= [];\n      reverseLinks[links[linkName]].push(linkName);\n    }\n\n    if (Object.keys(reverseLinks).length === 0) {\n      return;\n    }\n\n    for await (const entry of fs.createReadStream(archive).pipe(tar.extract())) {\n      const linkNames = reverseLinks[entry.header.name] ?? [];\n\n      if (linkNames.length === 0) {\n        // This entry isn't a link target\n        await stream.promises.finished(entry.resume() as any);\n        continue;\n      }\n      switch (entry.header.type) {\n      case 'directory':\n        await Promise.all(linkNames.map(async(linkName) => {\n          const dirName = absPath(linkName);\n\n          if (!dirName) {\n            console.warn(`Skipping unexpected directory ${ entry.header.name } -> ${ linkName }`);\n\n            return;\n          }\n          await fs.promises.mkdir(dirName, { recursive: true });\n          delete links[linkName];\n          console.debug(`Created directory ${ dirName }`);\n        }));\n        break;\n      case 'file': case 'contiguous-file': {\n        const fileNames = linkNames.map((linkName) => {\n          const fileName = absPath(linkName);\n\n          if (!fileName) {\n            console.warn(`Skipping unexpected file ${ entry.header.name } -> ${ linkName }`);\n          }\n\n          return fileName;\n        }).filter(defined);\n\n        await Promise.all(fileNames.map((fileName) => {\n          return fs.promises.mkdir(path.dirname(fileName), { recursive: true });\n        }));\n\n        const writers = fileNames.map(f => fs.createWriteStream(f));\n\n        entry.on('data', async(chunk) => {\n          entry.pause();\n          try {\n            await Promise.all(writers.map(async(writer) => {\n              await new Promise<void>((resolve, reject) => {\n                writer.write(chunk, 'utf-8', (error) => {\n                  if (error) {\n                    reject(error);\n                  } else {\n                    resolve();\n                  }\n                });\n              });\n            }));\n            entry.resume();\n          } catch (ex: any) {\n            entry.destroy(ex);\n          }\n        });\n        entry.on('end', () => {\n          writers.map(writer => writer.end());\n        });\n\n        for (const linkName of linkNames) {\n          delete links[linkName];\n        }\n\n        break;\n      }\n      default:\n        console.info(`Ignoring unsupported file type ${ entry.header.name } (${ entry.header.type })`);\n      }\n      await stream.promises.finished(entry.resume() as any);\n    }\n\n    // Handle symlinks that were not found\n    for (const [linkName, linkTarget] of Object.entries(links)) {\n      console.warn(`Skipping missing link ${ linkName } -> ${ linkTarget }`);\n    }\n  }\n\n  async getTags(imageName: string, options?: ContainerBasicOptions) {\n    let results = new Set<string>();\n\n    try {\n      results = new Set(await dockerRegistry.getTags(imageName));\n    } catch (ex) {\n      // We may fail here if the image doesn't exist / has an invalid host.\n      console.debugE(`Could not get tags from registry for ${ imageName }, ignoring:`, ex);\n    }\n\n    try {\n      const desired = parseImageReference(imageName);\n      const { stdout } = await this.runClient(\n        ['image', 'list', '--format={{ .Repository }}:{{ .Tag }}'], 'pipe', options);\n\n      for (const imageRef of stdout.split(/\\s+/).filter(v => v)) {\n        const info = parseImageReference(imageRef);\n\n        if (info?.tag && info.equalName(desired)) {\n          results.add(info.tag);\n        }\n      }\n    } catch (ex) {\n      // Failure to list images is acceptable.\n      console.debugE(`Could not get tags of existing images for ${ imageName }, ignoring:`, ex);\n    }\n\n    return results;\n  }\n\n  async run(imageID: string, options?: ContainerRunOptions): Promise<string> {\n    const args = ['container', 'run', '--detach'];\n\n    args.push('--restart', options?.restart === 'always' ? 'always' : 'no');\n    if (options?.name) {\n      args.push('--name', options.name);\n    }\n    args.push(imageID);\n\n    try {\n      const { stdout, stderr } = await this.runClient(args, 'pipe');\n\n      console.debug(stderr.trim());\n\n      return stdout.trim();\n    } catch (ex: any) {\n      if (Object.prototype.hasOwnProperty.call(ex, ErrorCommand)) {\n        const match = /container name \"[^\"]*\" is already in use by container \"(?<id>[0-9a-f]+)\"./.exec(ex.stderr ?? '');\n        const result = match?.groups?.['id'];\n\n        if (result) {\n          return result;\n        }\n      }\n      throw ex;\n    }\n  }\n\n  async stop(container: string, options?: ContainerStopOptions): Promise<void> {\n    if (options?.delete && options.force) {\n      const { stderr } = await this.runClient(['container', 'rm', '--force', container], 'pipe');\n\n      if (!/Error: No such container: \\S+/.test(stderr)) {\n        console.debug(stderr.trim());\n      }\n\n      return;\n    }\n\n    await this.runClient(['container', 'stop', container]);\n    if (options?.delete) {\n      await this.runClient(['container', 'rm', container]);\n    }\n  }\n\n  async composeUp(options: ContainerComposeOptions): Promise<void> {\n    const args = ['--project-directory', options.composeDir];\n\n    if (options.name) {\n      args.push('--project-name', options.name);\n    }\n    args.push('up', '--quiet-pull', '--wait', '--remove-orphans');\n\n    await this.runClient(args, console, { ...options, executable: 'docker-compose' });\n    console.debug('ran docker compose up');\n  }\n\n  async composeDown(options: ContainerComposeOptions): Promise<void> {\n    const args = [\n      options.name ? ['--project-name', options.name] : [],\n      ['--project-directory', options.composeDir, 'down'],\n    ].flat();\n\n    await this.runClient(args, console, { ...options, executable: 'docker-compose' });\n    console.debug('ran docker compose down');\n  }\n\n  composeExec(options: ContainerComposeExecOptions): Promise<ReadableProcess> {\n    const args = [\n      options.name ? ['--project-name', options.name] : [],\n      ['--project-directory', options.composeDir, 'exec'],\n      options.user ? ['--user', options.user] : [],\n      options.workdir ? ['--workdir', options.workdir] : [],\n      [options.service, ...options.command],\n    ].flat();\n\n    return Promise.resolve(this.runClient(args, 'stream',\n      { ...options, executable: 'docker-compose' }));\n  }\n\n  async composePort(options: ContainerComposePortOptions): Promise<string> {\n    const args = [\n      options.name ? ['--project-name', options.name] : [],\n      ['--project-directory', options.composeDir, 'port'],\n      options.protocol ? ['--protocol', options.protocol] : [],\n      [options.service, options.port.toString()],\n    ].flat();\n    const { stdout } = await this.runClient(args, 'pipe', { ...options, executable: 'docker-compose' });\n\n    return stdout.trim();\n  }\n\n  runClient(args: string[], stdio?: 'ignore', options?: runClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: Log, options?: runClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: 'pipe', options?: runClientOptions): Promise<{ stdout: string; stderr: string; }>;\n  runClient(args: string[], stdio: 'stream', options?: runClientOptions): ReadableProcess;\n  runClient(args: string[], stdio: 'interactive', options?: runClientOptions): WritableReadableProcess;\n  runClient(args: string[], stdio?: 'ignore' | 'pipe' | 'stream' | 'interactive' | Log, options?: runClientOptions) {\n    // Always add the `bin` directory, as docker CLI plugins may need them too.\n    const dirsToAdd = [path.join(paths.resources, process.platform, 'bin')];\n    const executableName = options?.executable ?? this.executable;\n    const isCLIPlugin = /^docker-(?!credential-)/.test(executableName);\n\n    const binType = isCLIPlugin ? 'docker-cli-plugins' : 'bin';\n    const executableDir = path.join(paths.resources, process.platform, binType);\n    const executable = path.resolve(executableDir, executableName);\n\n    if (isCLIPlugin) {\n      dirsToAdd.push(executableDir);\n    }\n\n    const opts = _.merge({ env: _.merge({}, process.env) }, options ?? {}, {\n      env: {\n        DOCKER_HOST: this.endpoint,\n        PATH:        `${ process.env.PATH }${ path.delimiter }${ dirsToAdd.join(path.delimiter) }`,\n      },\n    });\n\n    // Due to TypeScript reasons, we have to make each branch separately.\n    switch (stdio) {\n    case 'ignore':\n    case undefined:\n      return spawnFile(executable, args, { ...opts, stdio: 'ignore' });\n    case 'stream':\n      return spawn(executable, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });\n    case 'interactive':\n      return spawn(executable, args, { ...opts, stdio: 'pipe' });\n    case 'pipe':\n      return spawnFile(executable, args, { ...opts, stdio: 'pipe' });\n    }\n\n    return spawnFile(executable, args, { ...opts, stdio });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/nerdctlClient.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\nimport util from 'util';\n\nimport _ from 'lodash';\nimport tar from 'tar-stream';\n\nimport {\n  ContainerComposeExecOptions, ReadableProcess, WritableReadableProcess, ContainerComposeOptions,\n  ContainerEngineClient, ContainerRunOptions, ContainerStopOptions,\n  ContainerRunClientOptions, ContainerComposePortOptions, ContainerBasicOptions,\n} from './types';\n\nimport { execOptions, VMExecutor } from '@pkg/backend/backend';\nimport dockerRegistry from '@pkg/backend/containerClient/registry';\nimport { spawn, spawnFile } from '@pkg/utils/childProcess';\nimport { parseImageReference } from '@pkg/utils/dockerUtils';\nimport Logging, { Log } from '@pkg/utils/logging';\nimport { executable } from '@pkg/utils/resources';\nimport { defined } from '@pkg/utils/typeUtils';\n\nconst console = Logging.nerdctl;\n\n/**\n * NerdctlClient manages nerdctl/containerd.\n */\nexport class NerdctlClient implements ContainerEngineClient {\n  constructor(vm: VMExecutor) {\n    this.vm = vm;\n  }\n\n  /** The VM backing Rancher Desktop */\n  readonly vm: VMExecutor;\n  readonly executable = executable('nerdctl');\n\n  /**\n   * Run nerdctl with the given arguments, returning the standard output.\n   */\n  protected async nerdctl(...args: string[]): Promise<string>;\n  protected async nerdctl(options: { env?: Record<string, string> }, ...args: string[]): Promise<string>;\n  protected async nerdctl(optionOrArg: any, ...args: string[]): Promise<string> {\n    const finalArgs = args.concat();\n    const options: { env?: Record<string, string> } = {};\n\n    if (typeof optionOrArg === 'string') {\n      finalArgs.unshift(optionOrArg);\n    } else {\n      _.merge(options, { env: { ...process.env, ...optionOrArg.env } });\n    }\n\n    const { stdout } = await spawnFile(\n      executable('nerdctl'),\n      finalArgs,\n      { stdio: ['ignore', 'pipe', console], ...options },\n    );\n\n    return stdout;\n  }\n\n  /**\n   * Run a list of cleanup functions in reverse.\n   */\n  protected async runCleanups(cleanups: (() => Promise<unknown>)[]) {\n    for (const cleanup of cleanups.reverse()) {\n      try {\n        await cleanup();\n      } catch (e) {\n        console.error('Failed to run cleanup:', e);\n      }\n    }\n  }\n\n  /**\n   * Like running this.vm.execCommand, but retries the command if no output\n   * is produced. Is a workaround for a strange behavior of this.vm.execCommand:\n   * sometimes nothing is returned from stdout, as though it did not run at\n   * all. See https://github.com/rancher-sandbox/rancher-desktop/issues/4473\n   * for more info.\n   */\n  protected async execCommandWithRetries(options: execOptions & { capture: true }, ...command: string[]): Promise<string> {\n    const maxRetries = 10;\n    let result = '';\n\n    for (let i = 0; i < maxRetries && !result; i++) {\n      result = await this.vm.execCommand({ ...options, capture: true }, ...command);\n    }\n\n    return result;\n  }\n\n  /**\n   * Mount the given image inside the VM.\n   * @param imageID The ID of the image to mount.\n   * @returns The path that the image has been mounted on, plus an array of\n   * cleanup functions that must be called in reverse order when done.\n   * @note Due to https://github.com/containerd/nerdctl/issues/1058 we can't\n   * just do `nerdctl create` + `nerdctl cp`.  Instead, we need to make mounts\n   * manually.\n   */\n  protected async mountImage(imageID: string, namespace?: string): Promise<[string, (() => Promise<void>)[]]> {\n    const cleanups: (() => Promise<void>)[] = [];\n\n    try {\n      const namespaceArgs = namespace === undefined ? [] : ['--namespace', namespace];\n      const container = (await this.execCommandWithRetries({ capture: true },\n        '/usr/local/bin/nerdctl', ...namespaceArgs, 'create', '--entrypoint=/', imageID)).trim();\n\n      cleanups.push(() => this.vm.execCommand(\n        '/usr/local/bin/nerdctl', ...namespaceArgs, 'rm', '--force', '--volumes', container));\n\n      const workdir = (await this.execCommandWithRetries({ capture: true }, '/bin/mktemp', '-d', '-t', 'rd-nerdctl-cp-XXXXXX')).trim();\n\n      cleanups.push(() => this.vm.execCommand('/bin/rm', '-rf', workdir));\n\n      const command = await this.execCommandWithRetries({ capture: true, root: true },\n        '/usr/bin/ctr', ...namespaceArgs,\n        '--address=/run/k3s/containerd/containerd.sock', 'snapshot', 'mounts', workdir, container);\n\n      await this.vm.execCommand({ root: true }, ...command.trim().split(' '));\n      cleanups.push(async() => {\n        try {\n          await this.vm.execCommand({ root: true }, '/bin/umount', workdir);\n        } catch (ex) {\n          // Unmount might fail due to being busy; just detach and let it go\n          // away by itself later.\n          await this.vm.execCommand({ root: true }, '/bin/umount', '-l', workdir);\n        }\n      });\n\n      return [workdir, cleanups];\n    } catch (ex) {\n      await this.runCleanups(cleanups);\n      throw ex;\n    }\n  }\n\n  async waitForReady(): Promise<void> {\n    // We need to check two things: containerd, and buildkitd.\n    const commandsToCheck = [\n      ['/usr/local/bin/nerdctl', 'system', 'info'],\n      ['/usr/local/bin/buildctl', 'debug', 'info'],\n    ];\n\n    for (const cmd of commandsToCheck) {\n      while (true) {\n        try {\n          await this.vm.execCommand({ expectFailure: true, root: true }, ...cmd);\n          break;\n        } catch (ex) {\n          // Ignore the error, try again\n          await util.promisify(setTimeout)(1_000);\n        }\n      }\n    }\n  }\n\n  readFile(imageID: string, filePath: string): Promise<string>;\n  readFile(imageID: string, filePath: string, options: { encoding?: BufferEncoding, namespace?: string }): Promise<string>;\n  async readFile(imageID: string, filePath: string, options?: { encoding?: BufferEncoding, namespace?: string }): Promise<string> {\n    const encoding = options?.encoding ?? 'utf-8';\n    const [workdir, cleanups] = await this.mountImage(imageID, options?.namespace);\n\n    try {\n      // The await here is needed to ensure we read the result before running\n      // any cleanups\n      return await this.vm.readFile(path.posix.join(workdir, filePath), { encoding });\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  copyFile(imageID: string, sourcePath: string, destinationDir: string): Promise<void>;\n  copyFile(imageID: string, sourcePath: string, destinationDir: string, options: { namespace?: string }): Promise<void>;\n  async copyFile(imageID: string, sourcePath: string, destinationDir: string, options?: { namespace?: string }): Promise<void> {\n    const [imageDir, cleanups] = await this.mountImage(imageID, options?.namespace);\n\n    try {\n      // Archive the file(s) into the VM\n      const workDir = (await this.execCommandWithRetries({ capture: true }, '/bin/mktemp', '-d', '-t', 'rd-nerdctl-cp-XXXXXX')).trim();\n      const archive = path.posix.join(workDir, 'archive.tgz');\n      const fileList = path.posix.join(workDir, 'files.txt');\n\n      cleanups.push(() => this.vm.execCommand('/bin/rm', '-rf', workDir));\n      let sourceName: string, sourceDir: string;\n\n      if (sourcePath.endsWith('/')) {\n        sourceName = '.';\n        sourceDir = path.posix.join(imageDir, sourcePath);\n      } else {\n        sourceName = path.posix.basename(sourcePath);\n        sourceDir = path.posix.join(imageDir, path.posix.dirname(sourcePath));\n      }\n      // Compute the list of all files to archive, but only including things\n      // that (after resolving symlinks) point into the mount.\n      // This means that absolute links to /proc etc. are skipped.\n      await this.vm.execCommand({ root: true, cwd: sourceDir },\n        '/usr/bin/find', '-L', sourceName, '-xdev',\n        '-type', 'f', // After resolving symlinks, the target is a regular file\n        '-exec', '/bin/sh', '-c', `readlink -f {} | grep -q '${ imageDir }'`, ';',\n        '-exec', '/bin/sh', '-c', `echo '{}' >> ${ fileList }`, ';');\n\n      const args = [\n        '--create', '--gzip', '--file', archive, '--directory', sourceDir,\n        '--dereference', '--one-file-system', '--sparse', '--files-from', fileList,\n      ].filter(defined);\n\n      await this.vm.execCommand({ root: true }, '/bin/tar', ...args);\n\n      // Copy the archive to the host\n      const hostWorkDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-nerdctl-copy-'));\n\n      cleanups.push(() => fs.promises.rm(hostWorkDir, { recursive: true, maxRetries: 3 }));\n      const hostArchive = path.join(hostWorkDir, 'copy-file.tgz');\n\n      await this.vm.copyFileOut(archive, hostArchive);\n\n      // Extract the archive into the destination.\n      // Note that on Windows, we need to use the system-provided tar to handle Windows paths.\n      const tar = process.platform === 'win32' ? path.join(process.env.SystemRoot ?? `C:\\\\Windows`, 'system32', 'tar.exe') : '/usr/bin/tar';\n      const extractArgs = ['xzf', hostArchive, '-C', destinationDir];\n\n      await fs.promises.mkdir(path.normalize(destinationDir), { recursive: true });\n      await spawnFile(tar, extractArgs, { stdio: console });\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  async getTags(imageName: string, options?: ContainerBasicOptions) {\n    let results = new Set<string>();\n\n    try {\n      results = new Set(await dockerRegistry.getTags(imageName));\n    } catch (ex) {\n      // We may fail here if the image doesn't exist / has an invalid host.\n      console.debugE(`Could not get tags from registry for ${ imageName }, ignoring:`, ex);\n    }\n\n    try {\n      const desired = parseImageReference(imageName);\n      const { stdout } = await this.runClient(\n        ['image', 'list', '--format={{ .Name }}'], 'pipe', options);\n\n      for (const imageRef of stdout.split(/\\s+/).filter(v => v)) {\n        const info = parseImageReference(imageRef);\n\n        if (info?.tag && info.equalName(desired)) {\n          results.add(info.tag);\n        }\n      }\n    } catch (ex) {\n      // Failure to list images is acceptable.\n      console.debugE(`Could not get tags of existing images for ${ imageName }, ignoring:`, ex);\n    }\n\n    return results;\n  }\n\n  async run(imageID: string, options?: ContainerRunOptions): Promise<string> {\n    const args = ['container', 'run', '--detach'];\n\n    args.push('--restart', options?.restart === 'always' ? 'always' : 'no');\n    if (options?.name) {\n      args.push('--name', options.name);\n    }\n    if (options?.namespace) {\n      args.unshift('--namespace', options.namespace);\n    }\n    args.push(imageID);\n\n    return (await this.nerdctl(...args)).trim();\n  }\n\n  async stop(container: string, options?: ContainerStopOptions): Promise<void> {\n    function addNS(...args: string[]) {\n      if (options?.namespace) {\n        return [`--namespace=${ options.namespace }`, ...args];\n      }\n\n      return args;\n    }\n\n    if (options?.delete && options.force) {\n      await this.nerdctl(...addNS('container', 'rm', '--force', container));\n\n      return;\n    }\n\n    await this.nerdctl(...addNS('container', 'stop', container));\n    if (options?.delete) {\n      await this.nerdctl(...addNS('container', 'rm', container));\n    }\n  }\n\n  /**\n   * Copy the given host directory into a temporary directory in the VM\n   * @param hostPath The path on the host to a directory.\n   * @returns The temporary path in the VM holding the results.\n   */\n  protected async copyDirectoryIn(hostPath: string): Promise<string> {\n    const cleanups: (() => Promise<void>)[] = [];\n    let succeeded = false;\n\n    try {\n      const workDir = (await this.vm.execCommand({ capture: true },\n        '/bin/mktemp', '--directory', '--tmpdir', 'rd-nerdctl-copy-in-XXXXXX')).trim();\n\n      cleanups.push(() => this.vm.execCommand('/bin/rm', '-rf', workDir));\n\n      const resultDir = (await this.vm.execCommand({ capture: true },\n        '/bin/mktemp', '--directory', '--tmpdir', 'rd-nerdctl-copy-in-XXXXXX')).trim();\n\n      cleanups.push(async() => {\n        if (!succeeded) {\n          await this.vm.execCommand('/bin/rm', '-rf', workDir);\n        }\n      });\n\n      const archiveName = 'nerdctl-copy-in.tar';\n      const hostDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-nerdctl-copy-in-'));\n\n      cleanups.push(() => fs.promises.rm(hostDir, { recursive: true, maxRetries: 3 }));\n\n      const tarStream = fs.createWriteStream(path.join(hostDir, archiveName));\n      const archive = tar.pack();\n      const archiveFinished = util.promisify(stream.finished)(archive as any);\n      const newEntry = util.promisify(archive.entry.bind(archive));\n      const baseHeader: Partial<tar.Headers> = {\n        mode:  0o755,\n        uid:   0,\n        uname: 'root',\n        gname: 'wheel',\n        type:  'directory',\n      };\n      const walk = async(dir: string) => {\n        const fullPath = path.normalize(path.join(hostPath, dir));\n\n        for (const basename of await fs.promises.readdir(fullPath)) {\n          const name = path.normalize(path.join(dir, basename));\n          const info = await fs.promises.lstat(path.join(fullPath, basename));\n\n          if (info.isDirectory()) {\n            await newEntry({ ...baseHeader, name });\n            await walk(path.join(dir, basename));\n          } else if (info.isFile()) {\n            const readStream = fs.createReadStream(path.join(fullPath, basename));\n            const entry = archive.entry({\n              ...baseHeader,\n              ..._.pick(info, 'mode', 'mtime', 'size'),\n              type: 'file',\n              name,\n            });\n            const entryFinished = util.promisify(stream.finished)(entry);\n\n            readStream.pipe(entry);\n            await entryFinished;\n          } else if (info.isSymbolicLink()) {\n            await newEntry({\n              ...baseHeader,\n              ..._.pick(info, 'mode', 'mtime'),\n              name,\n              type:     'symlink',\n              linkname: await fs.promises.readlink(path.join(fullPath, basename)),\n            });\n          }\n        }\n      };\n\n      archive.pipe(tarStream);\n      await walk('.');\n      archive.finalize();\n      await archiveFinished;\n\n      await this.vm.copyFileIn(path.join(hostDir, archiveName), path.posix.join(workDir, archiveName));\n      await this.vm.execCommand('/bin/tar', 'xf', path.posix.join(workDir, archiveName), '-C', resultDir);\n      succeeded = true;\n\n      return resultDir;\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  /**\n   * Sets up the environment for compose.\n   * @returns [projectDir] The compose project directory to use.\n   * @returns [envFile] The environment file to use.\n   * @returns [cleanups] Any cleanups we need to run after\n   */\n  protected async composePrep(options: ContainerComposeOptions): Promise<{\n    projectDir: string,\n    envFile:    string,\n    cleanups:   (() => Promise<void>)[],\n  }> {\n    const cleanups: (() => Promise<void>)[] = [];\n    const envData = Object.entries(options.env ?? {})\n      .map(([k, v]) => `${ k }='${ v.replaceAll(\"'\", \"\\\\'\") }'\\n`)\n      .join('');\n\n    try {\n      if (this.vm.backend === 'wsl') {\n        // For WSL, we don't need to copy anything; nerdctl-stub will translate\n        // the paths correctly.\n\n        const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-compose-'));\n        const envFile = path.join(workDir, 'env.txt');\n\n        cleanups.push(() => fs.promises.rm(workDir, { recursive: true, maxRetries: 3 }));\n        await fs.promises.writeFile(envFile, envData);\n\n        return {\n          projectDir: options.composeDir, envFile, cleanups,\n        };\n      }\n\n      const projectDir = await this.copyDirectoryIn(options.composeDir);\n\n      cleanups.push(() => this.vm.execCommand('/bin/rm', '-rf', projectDir));\n\n      const envFile = (await (this.vm.execCommand({ capture: true },\n        '/bin/mktemp', '--tmpdir', 'rd-nerdctl-compose-XXXXXX'))).trim();\n\n      cleanups.push(() => this.vm.execCommand('/bin/rm', '-f', envFile));\n\n      await this.vm.writeFile(envFile, envData);\n\n      return {\n        projectDir, envFile, cleanups,\n      };\n    } catch (ex) {\n      await this.runCleanups(cleanups);\n      throw ex;\n    }\n  }\n\n  async composeUp(options: ContainerComposeOptions): Promise<void> {\n    const { projectDir, envFile, cleanups } = await this.composePrep(options);\n\n    try {\n      const args = ['compose', '--project-directory', projectDir];\n\n      if (options.name) {\n        args.push('--project-name', options.name);\n      }\n      if (options.namespace) {\n        args.unshift('--namespace', options.namespace);\n      }\n      if (options.env) {\n        args.push('--env-file', envFile);\n      }\n      // nerdctl doesn't support --wait, so make do with --detach.\n      args.push('up', '--quiet-pull', '--detach');\n\n      const result = await this.nerdctl({ env: options.env ?? {} }, ...args);\n\n      console.log('ran nerdctl compose up', result);\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  async composeDown(options: ContainerComposeOptions): Promise<void> {\n    const { projectDir, envFile, cleanups } = await this.composePrep(options);\n\n    try {\n      const args = [\n        options.namespace ? ['--namespace', options.namespace] : [],\n        ['compose'],\n        options.name ? ['--project-name', options.name] : [],\n        ['--project-directory', projectDir, 'down'],\n        options.env ? ['--env-file', envFile] : [],\n      ].flat();\n\n      const result = await this.nerdctl(...args);\n\n      console.debug('ran nerdctl compose down:', result);\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  async composeExec(options: ContainerComposeExecOptions): Promise<ReadableProcess> {\n    const { projectDir, envFile, cleanups } = await this.composePrep(options);\n\n    try {\n      const args = [\n        options.namespace ? ['--namespace', options.namespace] : [],\n        ['compose'],\n        options.name ? ['--project-name', options.name] : [],\n        ['--project-directory', projectDir],\n        options.env ? ['--env-file', envFile] : [],\n        ['exec', '--tty=false'],\n        options.user ? ['--user', options.user] : [],\n        options.workdir ? ['--workdir', options.workdir] : [],\n        [options.service, ...options.command],\n      ].flat();\n\n      const result = spawn(executable('nerdctl'), args, { stdio: ['ignore', 'pipe', 'pipe'] });\n      const delayedCleanups = cleanups.concat();\n\n      // Delay running cleanups until the process has finished to avoid removing\n      // files that may still be necessary.\n      result.on('exit', () => this.runCleanups(delayedCleanups));\n      result.on('error', () => this.runCleanups(delayedCleanups));\n      cleanups.splice(0, cleanups.length);\n\n      return result;\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  async composePort(options: ContainerComposePortOptions): Promise<string> {\n    const { projectDir, envFile, cleanups } = await this.composePrep(options);\n\n    try {\n      const args = [\n        options.namespace ? ['--namespace', options.namespace] : [],\n        ['compose'],\n        options.name ? ['--project-name', options.name] : [],\n        ['--project-directory', projectDir],\n        options.env ? ['--env-file', envFile] : [],\n        ['port'],\n        options.protocol ? ['--protocol', options.protocol] : [],\n        [options.service, options.port.toString(10)],\n      ].flat();\n\n      return (await this.nerdctl(...args)).trim();\n    } finally {\n      await this.runCleanups(cleanups);\n    }\n  }\n\n  runClient(args: string[], stdio?: 'ignore', options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: Log, options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: 'pipe', options?: ContainerRunClientOptions): Promise<{ stdout: string; stderr: string; }>;\n  runClient(args: string[], stdio: 'stream', options?: ContainerRunClientOptions): ReadableProcess;\n  runClient(args: string[], stdio: 'interactive', options?: ContainerRunClientOptions): WritableReadableProcess;\n  runClient(args: string[], stdio?: 'ignore' | 'pipe' | 'stream' | 'interactive' | Log, options?: ContainerRunClientOptions) {\n    const opts = _.merge({ env: process.env }, options);\n\n    if (opts.namespace) {\n      args = ['--namespace', opts.namespace].concat(args);\n    }\n    // Due to TypeScript reasons, we have to make each branch separately.\n    switch (stdio) {\n    case 'ignore':\n    case undefined:\n      return spawnFile(this.executable, args, { ...opts, stdio: 'ignore' });\n    case 'stream':\n      return spawn(this.executable, args, { ...opts, stdio: ['ignore', 'pipe', 'pipe'] });\n    case 'interactive':\n      return spawn(this.executable, args, { ...opts, stdio: 'pipe' });\n    case 'pipe':\n      return spawnFile(this.executable, args, { ...opts, stdio: 'pipe' });\n    }\n\n    return spawnFile(this.executable, args, { ...opts, stdio });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/registry.ts",
    "content": "import { net } from 'electron';\n\nimport registryAuth from '@pkg/backend/containerClient/auth';\nimport { parseImageReference } from '@pkg/utils/dockerUtils';\n\n/**\n * Registry interaction, with both Docker Hub and Docker Registry V2 APIs.\n */\nclass DockerRegistry {\n  /**\n   * Fetch some API endpoint from the registry\n   * @param endpoint The API endpoint, including the registry host.\n   */\n  async get(endpoint: URL): ReturnType<typeof net.fetch> {\n    const headers = await this.authenticate(endpoint);\n\n    return await net.fetch(endpoint.toString(), { headers });\n  }\n\n  /**\n   * List all tags for the given image name.\n   * @param name An image name, including registry as needed.\n   */\n  async getTags(name: string): Promise<string[]> {\n    const info = parseImageReference(name);\n    const tags: string[] = [];\n\n    if (!info) {\n      throw new Error(`Invalid image name: \"${ name }\"`);\n    }\n\n    let endpoint = new URL(`/v2/${ info.name }/tags/list?n=65536`, info.registry);\n    let hasMore = true;\n\n    while (hasMore) {\n      const resp = await this.get(endpoint);\n\n      if (!resp.ok) {\n        throw new Error(`Failed to fetch ${ endpoint }: ${ resp.status } ${ resp.statusText }`);\n      }\n\n      const result: { name: string, tags: string[] } = await resp.json();\n\n      if (result.name !== info.name) {\n        throw new Error(`Invalid tags: incorrect response name ${ result.name } from ${ endpoint }`);\n      }\n\n      tags.push(...result.tags);\n      hasMore = false;\n\n      for (const link of (resp.headers.get('Link') ?? '').split(', ')) {\n        const fields = link.split(/;\\s*/);\n\n        if (!fields.some(field => /^rel=(\"?)next\\1$/i.test(field))) {\n          continue;\n        }\n        // The `Link` header defined in RFC 8288 always has angle brackets\n        // around the (possibly relative) URL:\n        // https://www.rfc-editor.org/rfc/rfc8288#section-3\n        endpoint = new URL(fields[0].replace(/^<(.+)>$/, '$1'), endpoint);\n        hasMore = true;\n      }\n    }\n\n    return tags;\n  }\n\n  protected authenticate(endpoint: URL): Promise<HeadersInit> {\n    return registryAuth.authenticate(endpoint);\n  }\n}\n\nconst registry = new DockerRegistry();\n\nexport default registry;\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/containerClient/types.ts",
    "content": "import type { Log } from '@pkg/utils/logging';\n\nimport type { ChildProcessByStdio, SpawnOptions } from 'child_process';\nimport type { Readable, Writable } from 'stream';\n\nexport interface ContainerBasicOptions {\n  /**\n   * Namespace the container should be created in.\n   * @note Silently ignored when using moby.\n   */\n  namespace?: string;\n}\n\n/**\n * ContainerRunOptions are the options that can be passed to\n * ContainerEngineClient.run().  All fields are optional.\n */\nexport type ContainerRunOptions = ContainerBasicOptions & {\n  /** The name of the container. */\n  name?:    string;\n  /** Container restart policy, defaults to \"no\". */\n  restart?: 'always' | 'no';\n};\n\n/**\n * ContainerStopOptions are the options that can be passed to\n * ContainerEngineClient.stop().  All fields are optional.\n */\nexport type ContainerStopOptions = ContainerBasicOptions & {\n  /** Force stop the container (killing it uncleanly). */\n  force?:  true;\n  /** Delete the container after stopping. */\n  delete?: true;\n};\n\n/**\n * ContainerComposeOptions are options that can be passed to\n * ContainerEngineClient.composeUp() and .composeDown().  All fields are\n * optional.\n */\nexport type ContainerComposeOptions = ContainerBasicOptions & {\n  /** The directory holding the compose files. */\n  composeDir: string;\n  /** The name of the project */\n  name?:      string;\n  /** Environment variables to set on build */\n  env?:       Record<string, string>;\n};\n\nexport type ContainerComposeExecOptions = ContainerComposeOptions & {\n  /** The service to exec in. */\n  service:  string;\n  /** The command (and arguments) to execute. */\n  command:  string[];\n  /** Run the command as the given (in-container) user. */\n  user?:    string,\n  /** Run the command in the given (in-container) directory */\n  workdir?: string;\n};\n\n/** ReadableProcess describes a process that is capturing output */\nexport type ReadableProcess = ChildProcessByStdio<null, Readable, Readable>;\n\n/** WritableReadableProcess describes a process with stdin, stdout, and stderr all piped */\nexport type WritableReadableProcess = ChildProcessByStdio<Writable, Readable, Readable>;\n\nexport type ContainerComposePortOptions = ContainerComposeOptions & {\n  /** The service to find the port for */\n  service:  string;\n  /** The private port to map */\n  port:     number;\n  /** The protocol to use */\n  protocol: 'tcp' | 'udp';\n};\n\n/**\n * ContainerRunClientOptions describes arguments to\n * ContainerEngineClient.runClient()\n */\nexport type ContainerRunClientOptions = SpawnOptions & { namespace?: string };\n\n/**\n * ContainerEngineClient is used to run commands on the container engine.\n */\nexport interface ContainerEngineClient {\n  /**\n   * Block until the container engine is ready.\n   */\n  waitForReady(): Promise<void>;\n\n  /**\n   * Read the file from the given container image.\n   * @param imageID The ID of the image to read.\n   * @param filePath The file to read, relative to the root of the container.\n   * @param [options.encoding='utf-8'] The encoding to read.\n   * @param [options.namespace] Namespace the image is in, if supported.\n   */\n  readFile(imageID: string, filePath: string): Promise<string>;\n  readFile(imageID: string, filePath: string, options: { encoding?: BufferEncoding, namespace?: string }): Promise<string>;\n\n  /**\n   * Copy the given file to disk.\n   * @param imageID The ID of the image to copy files from.\n   * @param sourcePath The source path (inside the image) to copy from.\n   * This may be the path to a file or a directory.  If this is a directory, it\n   * must end with a slash.\n   * @param destinationDir The destination path (on the host) to copy to.\n   * If sourcePath is a directory, then its contents will be place here without\n   * an extra directory.  Otherwise, this is the parent directory, and the\n   * named file will be created within this directory with the same base name as\n   * in the VM.\n   * @param [options.namespace] Namespace the image is in, if supported.\n   * @note Symbolic links are always resolved, as some hosts might not support\n   * them.\n   */\n  copyFile(imageID: string, sourcePath: string, destinationDir: string): Promise<void>;\n  copyFile(imageID: string, sourcePath: string, destinationDir: string, options: { namespace?: string }): Promise<void>;\n\n  /**\n   * Get all tags available for the given image name.\n   * @param imageName the image name, possibly including the registry, but\n   *        excluding the tag.\n   */\n  getTags(imageName: string, options?: ContainerBasicOptions): Promise<Set<string>>;\n\n  /**\n   * Start a container.\n   * @param imageID The ID of the image to use.\n   * @note The container will be run detached (no IO).\n   * @returns The container ID.\n   */\n  run(imageID: string, options?: ContainerRunOptions): Promise<string>;\n\n  /**\n   * Stop the given container, if it exists and is running.\n   */\n  stop(container: string, options?: ContainerStopOptions): Promise<void>;\n\n  /**\n   * Start containers via `docker compose` / `nerdctl compose`.\n   */\n  composeUp(options: ContainerComposeOptions): Promise<void>;\n\n  /**\n   * Stop containers via `docker compose` / `nerdctl compose`.\n   */\n  composeDown(options?: ContainerComposeOptions): Promise<void>;\n\n  /**\n   * Spawn a process using `docker compose exec` / `nerdctl ...`, returning a\n   * raw process that has stdout and stderr set to pipe (but nothing for stdin).\n   */\n  composeExec(options: ContainerComposeExecOptions): Promise<ReadableProcess>;\n\n  /**\n   * Get port information for a compose service.\n   * @returns The port information, looking like `0.0.0.0:12345`.\n   */\n  composePort(options: ContainerComposePortOptions): Promise<string>;\n\n  /**\n   * Run the client directly, using the given arguments.  The 'stdio' argument\n   * determines the return value.\n   */\n  runClient(args: string[], stdio?: 'ignore', options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: Log, options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: 'pipe', options?: ContainerRunClientOptions): Promise<{ stdout: string, stderr: string }>;\n  runClient(args: string[], stdio: 'stream', options?: ContainerRunClientOptions): ReadableProcess;\n  runClient(args: string[], stdio: 'interactive', options?: ContainerRunClientOptions): WritableReadableProcess;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/factory.ts",
    "content": "import os from 'os';\n\nimport { Architecture, VMBackend } from './backend';\nimport LimaKubernetesBackend from './kube/lima';\nimport WSLKubernetesBackend from './kube/wsl';\nimport LimaBackend from './lima';\nimport MockBackend from './mock';\nimport WSLBackend from './wsl';\n\nimport { LimaKubernetesBackendMock, WSLKubernetesBackendMock } from '@pkg/backend/mock_screenshots';\n\nexport default function factory(arch: Architecture): VMBackend {\n  const platform = os.platform();\n\n  if (process.env.RD_MOCK_BACKEND === '1') {\n    return new MockBackend();\n  }\n\n  switch (platform) {\n  case 'linux':\n  case 'darwin':\n    return new LimaBackend(arch, (backend: LimaBackend) => {\n      if (process.env.RD_MOCK_FOR_SCREENSHOTS) {\n        return new LimaKubernetesBackendMock(arch, backend);\n      } else {\n        return new LimaKubernetesBackend(arch, backend);\n      }\n    });\n  case 'win32':\n    return new WSLBackend((backend: WSLBackend) => {\n      if (process.env.RD_MOCK_FOR_SCREENSHOTS) {\n        return new WSLKubernetesBackendMock(backend);\n      } else {\n        return new WSLKubernetesBackend(backend);\n      }\n    });\n  default:\n    throw new Error(`OS \"${ platform }\" is not supported.`);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/images/imageFactory.ts",
    "content": "import { VMBackend } from '@pkg/backend/backend';\nimport { ImageProcessor } from '@pkg/backend/images/imageProcessor';\nimport MobyImageProcessor from '@pkg/backend/images/mobyImageProcessor';\nimport NerdctlImageProcessor from '@pkg/backend/images/nerdctlImageProcessor';\nimport { ContainerEngine } from '@pkg/config/settings';\n\nconst cachedImageProcessors: Partial<Record<ContainerEngine, ImageProcessor>> = { };\n\n/**\n * Return the appropriate ImageProcessor singleton for the specified ContainerEngine.\n */\nexport function getImageProcessor(engineName: ContainerEngine, executor: VMBackend): ImageProcessor {\n  if (!(engineName in cachedImageProcessors)) {\n    switch (engineName) {\n    case ContainerEngine.MOBY:\n      cachedImageProcessors[engineName] = new MobyImageProcessor(executor);\n      break;\n    case ContainerEngine.CONTAINERD:\n      cachedImageProcessors[engineName] = new NerdctlImageProcessor(executor);\n      break;\n    default:\n      throw new Error(`No image processor called ${ engineName }`);\n    }\n  }\n\n  return cachedImageProcessors[engineName]!;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/images/imageProcessor.ts",
    "content": "import { Buffer } from 'buffer';\nimport { EventEmitter } from 'events';\nimport timers from 'timers';\n\nimport { VMBackend, VMExecutor } from '@pkg/backend/backend';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { ChildProcess, ErrorCommand } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport * as window from '@pkg/window';\n\nconst REFRESH_INTERVAL = 5 * 1000;\nconst console = Logging.images;\n\n/**\n * The fields that cover the results of a finished process.\n * Not all fields are set for every process.\n */\nexport interface childResultType {\n  stdout:  string;\n  stderr:  string;\n  code:    number;\n  signal?: string;\n}\n\n/**\n * The fields for display in the images table\n */\nexport interface ImageType {\n  imageName: string;\n  tag:       string;\n  imageID:   string;\n  size:      string;\n  digest:    string;\n}\n\n/**\n * Options for `processChildOutput`.\n */\ninterface ProcessChildOutputOptions {\n  /** The name of the executable; defaults to `processorName`. */\n  commandName?:   string;\n  /** The sub-command being executed; typically the first argument. */\n  subcommandName: string;\n  /** What notifications to send. */\n  notifications?: {\n    /** Send stdout as it comes in via `image-process-output`. */\n    stdout?: boolean;\n    /** Send stderr as it comes in via `image-process-output`. */\n    stderr?: boolean;\n    /** Send stdout after the command succeeds to the window via `ok:images-process-output`. */\n    ok?:     boolean;\n  }\n}\n\n/**\n * ImageProcessors take requests, from the UI or caused by state transitions\n * (such as a K8s engine hitting the STARTED state), and invokes the appropriate\n * client to run commands and send output to the UI.\n *\n * Each concrete ImageProcessor is a singleton, with a 1:1 correspondence between\n * the current container engine the user has selected, and its ImageProcessor.\n *\n * Currently, some events are handled directly by the concrete ImageProcessor subclasses,\n * and some are handled by the ImageEventHandler singleton, which calls methods on\n * the current ImageProcessor. Because these events are sent to all imageProcessors, but\n * only one should actually act on them, we use the concept of the `active` processor\n * to determine which processor acts on its events.\n *\n * When all the event-handlers have been moved into the ImageEventHandler the concept of\n * an active ImageProcessor can be dropped.\n */\nexport abstract class ImageProcessor extends EventEmitter {\n  protected backend:       VMBackend;\n  // Sometimes the `images` subcommand repeatedly fires the same error message.\n  // Instead of logging it every time, keep track of the current error and give a count instead.\n  private lastErrorMessage = '';\n  private sameErrorMessageCount = 0;\n  protected showedStderr = false;\n  private refreshInterval: ReturnType<typeof timers.setInterval> | null = null;\n  protected images:        ImageType[] = [];\n  protected _isReady = false;\n  protected isK8sReady = false;\n  private hasImageListeners = false;\n  private isWatching = false;\n  _refreshImages:          () => Promise<void>;\n  protected currentNamespace = 'default';\n  // See https://github.com/rancher-sandbox/rancher-desktop/issues/977\n  // for a task to get rid of the concept of an active imageProcessor.\n  // All the event handlers should be on the imageEventHandler, which knows\n  // which imageProcessor is currently active, and it can direct events to that.\n  protected active = false;\n\n  protected constructor(backend: VMBackend) {\n    super();\n    this.backend = backend;\n    this._refreshImages = this.refreshImages.bind(this);\n    this.on('newListener', (event: string | symbol) => {\n      if (!this.active) {\n        return;\n      }\n      if (event === 'images-changed' && !this.hasImageListeners) {\n        this.hasImageListeners = true;\n        this.updateWatchStatus();\n      }\n    });\n    this.on('removeListener', (event: string | symbol) => {\n      if (!this.active) {\n        return;\n      }\n      if (event === 'images-changed' && this.hasImageListeners) {\n        this.hasImageListeners = this.listeners('images-changed').length > 0;\n        this.updateWatchStatus();\n      }\n    });\n    this.on('readiness-changed', (state: boolean) => {\n      if (!this.active) {\n        return;\n      }\n      window.send('images-check-state', state);\n    });\n    this.on('images-process-output', (data: string, isStderr: boolean) => {\n      if (!this.active) {\n        return;\n      }\n      window.send('images-process-output', data, isStderr);\n    });\n    mainEvents.on('settings-update', (cfg) => {\n      if (!this.active) {\n        return;\n      }\n\n      if (this.namespace !== cfg.images.namespace) {\n        this.namespace = cfg.images.namespace;\n        this.refreshImages()\n          .catch((err: Error) => {\n            console.log(`Error refreshing images:`, err);\n          });\n      }\n    });\n  }\n\n  activate() {\n    this.active = true;\n  }\n\n  deactivate() {\n    this.active = false;\n  }\n\n  protected updateWatchStatus() {\n    const shouldWatch = this.isK8sReady && this.hasImageListeners;\n\n    if (this.isWatching === shouldWatch) {\n      return;\n    }\n\n    if (this.refreshInterval) {\n      timers.clearInterval(this.refreshInterval);\n    }\n    if (shouldWatch) {\n      this.refreshInterval = timers.setInterval(this._refreshImages, REFRESH_INTERVAL);\n      timers.setImmediate(this._refreshImages);\n    }\n    this.isWatching = shouldWatch;\n  }\n\n  /**\n   * Are images ready for display in the UI?\n   */\n  get isReady() {\n    return this._isReady;\n  }\n\n  /**\n   * Wrapper around the trivy command to scan the specified image.\n   * @param taggedImageName The name of the image, e.g. `registry.opensuse.org/opensuse/leap:15.6`.\n   * @param namespace The namespace to scan.\n   */\n  abstract scanImage(taggedImageName: string, namespace: string): Promise<childResultType>;\n\n  /**\n   * Scan an image using trivy.\n   * @param taggedImageName The image to scan, e.g. `registry.opensuse.org/opensuse/leap:15.6`.\n   * @param env Extra environment variables to set, e.g. `CONTAINERD_NAMESPACE`.\n   */\n  async runTrivyScan(taggedImageName: string, env?: Record<string, string>) {\n    const imageSrc = {\n      docker:  'docker',\n      nerdctl: 'containerd',\n    }[this.processorName] ?? this.processorName;\n    const args = ['trivy', '--quiet', 'image', '--image-src', imageSrc, '--format', 'json', taggedImageName];\n\n    if (env) {\n      args.unshift('/usr/bin/env', ...Object.entries(env).map(([k, v]) => `${ k }=${ v }`));\n    }\n\n    return await this.processChildOutput(\n      this.backend.executor.spawn({ root: true }, ...args),\n      {\n        commandName:    'trivy',\n        subcommandName: 'image',\n        // Do not set stdout to avoid dumping JSON that nobody ever reads.\n        notifications:  { stderr: true, ok: true },\n      },\n    );\n  }\n\n  /**\n   * Returns the current list of cached images.\n   */\n  listImages(): ImageType[] {\n    return this.images;\n  }\n\n  isChildResultType(object: any): object is childResultType {\n    return 'stderr' in object &&\n      'stdout' in object &&\n      'signal' in object &&\n      'code' in object;\n  }\n\n  /**\n   * Refreshes the current cache of processed images.\n   */\n  async refreshImages() {\n    try {\n      const result:childResultType = await this.getImages();\n\n      if (result.stderr) {\n        if (!this.showedStderr) {\n          console.log(`${ this.processorName } images: ${ result.stderr } `);\n          this.showedStderr = true;\n        }\n      } else {\n        this.showedStderr = false;\n      }\n      this.images = this.parse(result.stdout);\n      if (!this._isReady) {\n        this._isReady = true;\n        this.emit('readiness-changed', true);\n      }\n      this.emit('images-changed', this.images);\n    } catch (err) {\n      if (!this.showedStderr) {\n        if (this.isChildResultType(err) && !err.stdout && !err.signal) {\n          console.log(err.stderr);\n        } else {\n          console.log(err);\n        }\n      }\n      this.showedStderr = true;\n      if (this.isChildResultType(err) && this._isReady) {\n        this._isReady = false;\n        this.emit('readiness-changed', false);\n      }\n    }\n  }\n\n  protected parse(data: string): ImageType[] {\n    const results = data\n      .trimEnd()\n      .split(/\\r?\\n/)\n      .slice(1)\n      .map((line) => {\n        const [imageName, tag, digest, imageID, _created, _platform, size, _blobSize] = line.split(/\\s+/);\n\n        return {\n          imageName, tag, imageID, size, digest,\n        };\n      });\n\n    return results;\n  }\n\n  /**\n   * Takes the `childProcess` returned by a command like `child_process.spawn` and processes the\n   * output streams and exit code and signal.\n   *\n   * @param child The child process to monitor.\n   * @param options Additional options.\n   */\n  async processChildOutput(child: ChildProcess, options: ProcessChildOutputOptions): Promise<childResultType> {\n    const { subcommandName } = options;\n    const result = { stdout: '', stderr: '' };\n    const commandName = options.commandName ?? this.processorName;\n    const command = `${ commandName } ${ subcommandName }`;\n    const sendNotifications = options.notifications ?? {\n      stdout: true, stderr: true, ok: true,\n    };\n\n    return await new Promise((resolve, reject) => {\n      child.stdout?.on('data', (data: Buffer) => {\n        const dataString = data.toString();\n\n        if (sendNotifications.stdout) {\n          this.emit('images-process-output', dataString, false);\n        }\n        result.stdout += dataString;\n      });\n      child.stderr?.on('data', (data: Buffer) => {\n        let dataString = data.toString();\n\n        if (commandName === 'nerdctl' && subcommandName === 'images') {\n          /**\n           * `nerdctl images` issues some dubious error messages\n           *  (see https://github.com/containerd/nerdctl/issues/353 , logged 2021-09-10)\n           *  Pull them out for now\n           */\n          dataString = dataString\n            .replace(/time=\".+?\"\\s+level=.+?\\s+msg=\"failed to compute image\\(s\\) size\"\\s*/g, '')\n            .replace(/time=\".+?\"\\s+level=.+?\\s+msg=\"unparsable image name.*?sha256:[0-9a-fA-F]{64}.*?\\\\\"\"\\s*/g, '');\n          if (!dataString) {\n            return;\n          }\n        }\n        result.stderr += dataString;\n        if (sendNotifications.stderr) {\n          this.emit('images-process-output', dataString, true);\n        }\n      });\n      child.on('exit', (code, signal) => {\n        if (result.stderr) {\n          const timeLessMessage = result.stderr.replace(/\\btime=\".*?\"/g, '');\n\n          if (this.lastErrorMessage !== timeLessMessage) {\n            this.lastErrorMessage = timeLessMessage;\n            this.sameErrorMessageCount = 1;\n\n            console.log(`> ${ command }:\\r\\n${ result.stderr.replace(/(?!<\\r)\\n/g, '\\r\\n') }`);\n          } else {\n            const m = /(Error: .*)/.exec(this.lastErrorMessage);\n\n            this.sameErrorMessageCount += 1;\n            console.log(`${ command }: ${ m ? m[1] : 'same error message' } #${ this.sameErrorMessageCount }\\r`);\n          }\n        } else if (commandName === 'trivy') {\n          console.log(`> ${ command }: returned ${ result.stdout.length } bytes on stdout`);\n        } else {\n          console.log(`> ${ command }:\\n${ result.stdout.replace(/(?!<\\r)\\n/g, '\\r\\n') }`);\n        }\n        if (code === 0) {\n          if (sendNotifications.ok) {\n            window.send('ok:images-process-output', result.stdout);\n          }\n          resolve({ ...result, code });\n        } else if (signal) {\n          reject(Object.create(result, {\n            code:           { value: -1 },\n            signal:         { value: signal },\n            [ErrorCommand]: {\n              enumerable: false,\n              value:      child.spawnargs,\n            },\n          }));\n        } else {\n          reject(Object.create(result, {\n            code:           { value: code },\n            [ErrorCommand]: {\n              enumerable: false,\n              value:      child.spawnargs,\n            },\n          }));\n        }\n      });\n    });\n  }\n\n  /**\n   * Called normally when the UI requests the current list of namespaces\n   * for the current imageProcessor.\n   *\n   * containerd starts with two namespaces: \"k8s.io\" and \"default\".\n   * There's no way to add other namespaces in the UI,\n   * but they can easily be added from the command-line.\n   *\n   * See https://github.com/rancher-sandbox/rancher-desktop/issues/978 for being notified\n   * without polling on changes in the namespaces.\n   */\n  async relayNamespaces() {\n    const namespaces = await this.getNamespaces();\n    const comparator = Intl.Collator(undefined, { sensitivity: 'base' }).compare;\n\n    if (!namespaces.includes('default')) {\n      namespaces.push('default');\n    }\n    window.send('images-namespaces', namespaces.sort(comparator));\n  }\n\n  get namespace() {\n    return this.currentNamespace;\n  }\n\n  set namespace(value: string) {\n    this.currentNamespace = value;\n  }\n\n  /* Subclass-specific method definitions here: */\n\n  protected abstract get processorName(): string;\n\n  abstract getNamespaces(): Promise<string[]>;\n\n  abstract buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<childResultType>;\n\n  abstract deleteImage(imageID: string): Promise<childResultType>;\n\n  abstract deleteImages(imageIDs: string[]): Promise<childResultType>;\n\n  abstract pullImage(taggedImageName: string): Promise<childResultType>;\n\n  abstract pushImage(taggedImageName: string): Promise<childResultType>;\n\n  abstract getImages(): Promise<childResultType>;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/images/mobyImageProcessor.ts",
    "content": "import path from 'path';\n\nimport { VMBackend } from '@pkg/backend/backend';\nimport * as imageProcessor from '@pkg/backend/images/imageProcessor';\nimport * as K8s from '@pkg/backend/k8s';\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport * as window from '@pkg/window';\n\nconst console = Logging.images;\n\nexport default class MobyImageProcessor extends imageProcessor.ImageProcessor {\n  constructor(backend: VMBackend) {\n    super(backend);\n\n    mainEvents.on('k8s-check-state', (mgr: VMBackend) => {\n      if (!this.active) {\n        return;\n      }\n      this.isK8sReady = mgr.state === K8s.State.STARTED || mgr.state === K8s.State.DISABLED;\n      this.updateWatchStatus();\n    });\n  }\n\n  protected get processorName() {\n    return 'docker';\n  }\n\n  protected async runImagesCommand(args: string[], sendNotifications = true): Promise<imageProcessor.childResultType> {\n    const subcommandName = args[0];\n\n    return this.processChildOutput(\n      this.backend.containerEngineClient.runClient(args, 'stream'),\n      {\n        subcommandName,\n        notifications: {\n          stdout: sendNotifications, stderr: sendNotifications, ok: sendNotifications,\n        },\n      });\n  }\n\n  async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<imageProcessor.childResultType> {\n    const args = ['build',\n      '--file', path.join(dirPart, filePart),\n      '--tag', taggedImageName,\n      dirPart];\n\n    return await this.runImagesCommand(args);\n  }\n\n  async deleteImage(imageID: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['rmi', imageID]);\n  }\n\n  async deleteImages(imageIDs: string[]): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['rmi', ...imageIDs]);\n  }\n\n  async pullImage(taggedImageName: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['pull', taggedImageName]);\n  }\n\n  async pushImage(taggedImageName: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['push', taggedImageName]);\n  }\n\n  async getImages(): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(\n      ['images', '--digests', '--format', '{{json .}}'],\n      false);\n  }\n\n  scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {\n    return this.runTrivyScan(taggedImageName);\n  }\n\n  relayNamespaces(): Promise<void> {\n    window.send('images-namespaces', []);\n\n    return Promise.resolve();\n  }\n\n  getNamespaces(): Promise<string[]> {\n    throw new Error(\"docker doesn't support namespaces\");\n  }\n\n  /**\n   * Sample output (line-oriented JSON output, as opposed to one JSON document):\n   *\n   * {\"CreatedAt\":\"2021-10-05 22:04:12 +0000 UTC\",\"CreatedSince\":\"20 hours ago\",\"ID\":\"171689e43026\",\"Repository\":\"\",\"Tag\":\"\",\"Size\":\"119.2 MiB\"}\n   * {\"CreatedAt\":\"2021-10-05 22:04:20 +0000 UTC\",\"CreatedSince\":\"20 hours ago\",\"ID\":\"55fe4b211a51\",\"Repository\":\"rancher/k3d\",\"Tag\":\"v0.1.0-beta.7\",\"Size\":\"46.2 MiB\"}\n   * ...\n   */\n\n  parse(data: string): imageProcessor.ImageType[] {\n    const images: imageProcessor.ImageType[] = [];\n    const records = data\n      .split(/\\r?\\n/)\n      .filter(line => line.trim().length > 0)\n      .map((line) => {\n        try {\n          return JSON.parse(line);\n        } catch (err) {\n          console.log(`Error json-parsing line [${ line }]:`, err);\n\n          return null;\n        }\n      })\n      .filter(record => record);\n\n    for (const record of records) {\n      if (['', 'sha256'].includes(record.Repository)) {\n        continue;\n      }\n      images.push({\n        imageName: record.Repository,\n        tag:       record.Tag,\n        imageID:   record.ID,\n        size:      record.Size,\n        digest:    record.Digest,\n      });\n    }\n\n    return images.sort(imageComparator);\n  }\n}\n\nfunction imageComparator(a: imageProcessor.ImageType, b: imageProcessor.ImageType): number {\n  return a.imageName.localeCompare(b.imageName) ||\n    a.tag.localeCompare(b.tag) ||\n    a.imageID.localeCompare(b.imageID);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/images/nerdctlImageProcessor.ts",
    "content": "import path from 'path';\n\nimport { VMBackend } from '@pkg/backend/backend';\nimport * as imageProcessor from '@pkg/backend/images/imageProcessor';\nimport * as K8s from '@pkg/backend/k8s';\nimport mainEvents from '@pkg/main/mainEvents';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport { executable } from '@pkg/utils/resources';\n\nconst console = Logging.images;\n\nexport default class NerdctlImageProcessor extends imageProcessor.ImageProcessor {\n  constructor(backend: VMBackend) {\n    super(backend);\n\n    mainEvents.on('k8s-check-state', (mgr: VMBackend) => {\n      if (!this.active) {\n        return;\n      }\n      this.isK8sReady = mgr.state === K8s.State.STARTED || mgr.state === K8s.State.DISABLED;\n      this.updateWatchStatus();\n    });\n  }\n\n  protected get processorName() {\n    return 'nerdctl';\n  }\n\n  protected async runImagesCommand(args: string[], sendNotifications = true): Promise<imageProcessor.childResultType> {\n    const subcommandName = args[0];\n\n    return await this.processChildOutput(\n      this.backend.containerEngineClient.runClient(args, 'stream', { namespace: this.currentNamespace }),\n      {\n        subcommandName,\n        notifications: {\n          stdout: sendNotifications, stderr: sendNotifications, ok: sendNotifications,\n        },\n      });\n  }\n\n  async buildImage(dirPart: string, filePart: string, taggedImageName: string): Promise<imageProcessor.childResultType> {\n    const args = ['build',\n      '--buildkit-host', 'unix:///run/buildkit/buildkitd.sock',\n      '--file', path.join(dirPart, filePart),\n      '--tag', taggedImageName,\n      dirPart];\n\n    return await this.runImagesCommand(args);\n  }\n\n  async deleteImage(imageID: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['rmi', imageID]);\n  }\n\n  async deleteImages(imageIDs: string[]): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['rmi', ...imageIDs]);\n  }\n\n  async pullImage(taggedImageName: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['pull', taggedImageName]);\n  }\n\n  async pushImage(taggedImageName: string): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(['push', taggedImageName]);\n  }\n\n  async getImages(): Promise<imageProcessor.childResultType> {\n    return await this.runImagesCommand(\n      ['images', '--digests', '--format', '{{json .}}'],\n      false);\n  }\n\n  scanImage(taggedImageName: string, namespace: string): Promise<imageProcessor.childResultType> {\n    return this.runTrivyScan(\n      taggedImageName,\n      {\n        CONTAINERD_ADDRESS:   '/run/k3s/containerd/containerd.sock',\n        CONTAINERD_NAMESPACE: namespace,\n      },\n    );\n  }\n\n  async getNamespaces(): Promise<string[]> {\n    const { stdout, stderr } = await childProcess.spawnFile(executable('nerdctl'),\n      ['namespace', 'list', '--quiet'],\n      { stdio: ['inherit', 'pipe', 'pipe'] });\n\n    if (stderr) {\n      console.log(`Error getting namespaces: ${ stderr }`, stderr);\n    }\n\n    return stdout.trim().split(/\\r?\\n/).map(line => line.trim()).sort();\n  }\n\n  /**\n   * Sample output (line-oriented JSON output, as opposed to one JSON document):\n   *\n   * {\"CreatedAt\":\"2021-10-05 22:04:12 +0000 UTC\",\"CreatedSince\":\"20 hours ago\",\"ID\":\"171689e43026\",\"Repository\":\"\",\"Tag\":\"\",\"Size\":\"119.2 MiB\"}\n   * {\"CreatedAt\":\"2021-10-05 22:04:20 +0000 UTC\",\"CreatedSince\":\"20 hours ago\",\"ID\":\"55fe4b211a51\",\"Repository\":\"rancher/k3d\",\"Tag\":\"v0.1.0-beta.7\",\"Size\":\"46.2 MiB\"}\n   * ...\n   */\n\n  parse(data: string): imageProcessor.ImageType[] {\n    const images: imageProcessor.ImageType[] = [];\n    const records = data.split(/\\r?\\n/)\n      .filter(line => line.trim().length > 0)\n      .map((line) => {\n        try {\n          return JSON.parse(line);\n        } catch (err) {\n          console.log(`Error json-parsing line [${ line }]:`, err);\n\n          return null;\n        }\n      })\n      .filter(record => record);\n\n    for (const record of records) {\n      if (['', 'sha256'].includes(record.Repository)) {\n        continue;\n      }\n      images.push({\n        imageName: record.Repository,\n        tag:       record.Tag,\n        imageID:   record.ID,\n        size:      record.Size,\n        digest:    record.Digest,\n      });\n    }\n\n    return images.sort(imageComparator);\n  }\n}\n\nfunction imageComparator(a: imageProcessor.ImageType, b: imageProcessor.ImageType): number {\n  return a.imageName.localeCompare(b.imageName) ||\n    a.tag.localeCompare(b.tag) ||\n    a.imageID.localeCompare(b.imageID);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/k3sHelper.ts",
    "content": "import crypto from 'crypto';\nimport events from 'events';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\nimport tls from 'tls';\nimport util from 'util';\n\nimport {\n  CustomObjectsApi, KubeConfig, V1ObjectMeta, findHomeDir, ApiException,\n} from '@kubernetes/client-node';\nimport { ActionOnInvalid } from '@kubernetes/client-node/dist/config_types';\nimport { net } from 'electron';\nimport _ from 'lodash';\nimport semver from 'semver';\nimport yaml from 'yaml';\n\nimport { Architecture, VMExecutor } from './backend';\n\nimport * as K8s from '@pkg/backend/k8s';\nimport { KubeClient } from '@pkg/backend/kube/client';\nimport { loadFromString, exportConfig } from '@pkg/backend/kubeconfig';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { isUnixError } from '@pkg/typings/unix.interface';\nimport DownloadProgressListener from '@pkg/utils/DownloadProgressListener';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport { SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport Latch from '@pkg/utils/latch';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport safeRename from '@pkg/utils/safeRename';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { defined, RecursivePartial, RecursiveReadonly, RecursiveTypes } from '@pkg/utils/typeUtils';\nimport { showMessageBox } from '@pkg/window';\n\nimport type Electron from 'electron';\n\nconst console = Logging.k8s;\n\n/**\n * ShortVersion is the version string without any k3s suffixes, nor a \"v\"\n * prefix; this is the version we present to the user.\n */\nexport type ShortVersion = string;\n\nlet isOnline = true;\n\nmainEvents.on('update-network-status', (connected) => {\n  isOnline = connected;\n});\n\nexport interface ReleaseAPIEntry {\n  tag_name: string;\n  assets: {\n    browser_download_url: string;\n    name:                 string;\n  }[];\n}\n\nexport class NoCachedK3sVersionsError extends Error {\n}\n\nconst CURRENT_CACHE_VERSION = 2 as const;\n\n/** cacheData describes the JSON data we write to the cache. */\ninterface cacheData {\n  cacheVersion?: typeof CURRENT_CACHE_VERSION;\n  /** List of available versions; includes build information. */\n  versions:      string[];\n  /** Mapping of channel labels to current version (excluding build information). */\n  channels:      Record<string, ShortVersion>;\n}\n\n/**\n * RequiresRestartSeverityChecker is a function that will be used to determine\n * whether a given settings change will require a reset (i.e. deleting user\n * workloads).\n * @param currentValue The current value of the setting.\n * @param desiredValue The desired value of the setting.\n * @param allSettings The full merged settings object.\n * @returns 'restart' if a restart is required, 'reset' if a reset is required,\n *         or false if no restart is required.\n */\ntype RequiresRestartSeverityChecker<K extends keyof RecursiveTypes<K8s.BackendSettings>> = (\n  currentValue: RecursiveTypes<K8s.BackendSettings>[K],\n  desiredValue: RecursiveTypes<K8s.BackendSettings>[K],\n  allSettings: RecursiveReadonly<K8s.BackendSettings>,\n) => 'restart' | 'reset' | false;\n\n/**\n * RequiresRestartCheckers defines a mapping of settings (in dot-separated form)\n * to a RequiresRestartSeverityChecker for the given setting.\n */\ntype RequiresRestartCheckers = {\n  [K in keyof RecursiveTypes<K8s.BackendSettings>]?: RequiresRestartSeverityChecker<K>;\n};\n\n/**\n * ExtraRequiresReasons defines a mapping of settings (in dot-separated form) to\n * the current value (that does not always match the stored settings) and a\n * RequiresRestartSeverityChecker for the given setting.\n */\nexport type ExtraRequiresReasons = {\n  [K in keyof RecursiveTypes<K8s.BackendSettings>]?: {\n    current:   RecursiveTypes<K8s.BackendSettings>[K];\n    severity?: RequiresRestartSeverityChecker<K>;\n  }\n};\n\n/**\n * ChannelMapping is an internal structure to map a channel name to its\n * corresponding version.\n *\n * This only exists to aid in debugging.\n * This is only exported for tests.\n */\nexport class ChannelMapping {\n  [channel: string]: semver.SemVer;\n  [util.inspect.custom](depth: number, options: util.InspectOptionsStylized) {\n    const entries = Object.entries(this).map(([channel, version]) => [channel, version.raw]);\n\n    return util.inspect(Object.fromEntries(entries), { ...options, depth });\n  }\n}\n\n/**\n * Given a version, return the K3s build version.\n *\n * Note that this is only exported for testing.\n * @param version The version to parse\n * @returns The K3s build version\n */\nexport function buildVersion(version: semver.SemVer) {\n  const [, numString] = /k3s(\\d+)/.exec(version.build[0]) || [undefined, -1];\n\n  return parseInt(`${ numString || '-1' }`);\n}\n\nexport default class K3sHelper extends events.EventEmitter {\n  protected readonly channelApiUrl = 'https://update.k3s.io/v1-release/channels';\n  protected readonly channelApiAccept = 'application/json';\n  protected readonly releaseApiUrl = 'https://api.github.com/repos/k3s-io/k3s/releases?per_page=100';\n  protected readonly releaseApiAccept = 'application/vnd.github.v3+json';\n  protected readonly resourcesPath = path.join(paths.resources, 'k3s-versions.json');\n  protected readonly cachePath = path.join(paths.cache, 'k3s-versions.json');\n  protected readonly minimumVersion = new semver.SemVer('1.25.3');\n  /**\n   * versionFromChannel is a mapping from the channel name to the latest (short)\n   * version in that channel.\n   */\n  protected versionFromChannel: Record<string, ShortVersion> = {};\n\n  constructor(arch: Architecture) {\n    super();\n    this.arch = arch;\n  }\n\n  /**\n   * Versions that we know to exist.  This is indexed by the version string,\n   * without any build information (since we only ever take the latest build).\n   * Note that the key is in the form `1.0.0` (i.e. without the `v` prefix).\n   */\n  protected versions: Record<ShortVersion, SemanticVersionEntry> = {};\n\n  protected pendingNetworkSetup = Latch();\n  protected pendingInitialize: Promise<void> | undefined;\n\n  /** The current architecture. */\n  protected readonly arch: Architecture;\n\n  /**\n   * Read the cached data and fill out this.versions.\n   * The cache file consists of an array of VersionEntry.\n   */\n  protected async readCache() {\n    try {\n      let cacheData: cacheData;\n\n      try {\n        cacheData = JSON.parse(await fs.promises.readFile(this.cachePath, 'utf-8'));\n        if (cacheData.cacheVersion !== CURRENT_CACHE_VERSION) {\n          throw new Error(`Invalid cache version ${ cacheData.cacheVersion }`);\n        }\n      } catch (ex) {\n        console.debug('Failed to read cached k3s versions; falling back to bundled versions list:', ex);\n        cacheData = JSON.parse(await fs.promises.readFile(this.resourcesPath, 'utf-8'));\n      }\n\n      if (cacheData.cacheVersion !== CURRENT_CACHE_VERSION) {\n        // If the cache format version is different, ignore the cache.\n        console.debug(`Ignoring cache with invalid version ${ cacheData.cacheVersion }`);\n\n        return;\n      }\n\n      for (const versionString of cacheData.versions) {\n        const version = semver.parse(versionString);\n\n        if (version && semver.gte(version, this.minimumVersion)) {\n          this.versions[version.version] = new SemanticVersionEntry(version);\n        }\n      }\n\n      for (const [channel, version] of Object.entries(cacheData.channels)) {\n        if (!this.versions[version]) {\n          console.debug(`Ignoring invalid version cache: ${ channel } has invalid version ${ version }`);\n          continue;\n        }\n        this.versions[version].channels ??= [];\n        this.versions[version].channels?.push(channel);\n        this.versionFromChannel[channel] = version;\n      }\n\n      for (const entry of Object.values(this.versions)) {\n        entry.channels?.sort(this.compareChannels);\n      }\n    } catch (ex) {\n      console.error(`Error reading cached version data, discarding:`, ex);\n      // Clear any versions we may have, to be populated as if we had no cache.\n      this.versions = {};\n    }\n  }\n\n  /** Write this.versions into the cache file. */\n  protected async writeCache() {\n    const cacheData: cacheData = {\n      cacheVersion: CURRENT_CACHE_VERSION,\n      versions:     [],\n      channels:     {},\n    };\n\n    if (!cacheData.versions || !cacheData.channels) {\n      throw new Error('Panic: invalid code flow');\n    }\n\n    for (const [version, data] of Object.entries(this.versions)) {\n      cacheData.versions.push(data.version.raw);\n      for (const channel of data.channels ?? []) {\n        cacheData.channels[channel] = version;\n      }\n    }\n    cacheData.versions.sort((a, b) => semver.parse(a)?.compare(b) ?? a.localeCompare(b));\n    const serializedCacheData = jsonStringifyWithWhiteSpace(cacheData);\n\n    await fs.promises.mkdir(paths.cache, { recursive: true });\n    await fs.promises.writeFile(this.cachePath, serializedCacheData, 'utf-8');\n    console.debug(`Wrote versions cache:`, cacheData);\n  }\n\n  /** The files we need to download for the current architecture.\n   *  images: an array of potential files in order of most preferred to least preferred\n   */\n  protected get filenames() {\n    switch (this.arch) {\n    case 'x86_64':\n      return {\n        exe:      'k3s',\n        images:   ['k3s-airgap-images-amd64.tar.zst', 'k3s-airgap-images-amd64.tar'],\n        checksum: 'sha256sum-amd64.txt',\n      };\n    case 'aarch64':\n      return {\n        exe:      'k3s-arm64',\n        images:   ['k3s-airgap-images-arm64.tar.zst', 'k3s-airgap-images-arm64.tar'],\n        checksum: 'sha256sum-arm64.txt',\n      };\n    }\n  }\n\n  /**\n   * Process one version entry retrieved from GitHub, inserting it into the\n   * cache.  This will not add any channel labels.\n   * @param entry The GitHub API response entry to process.\n   * @returns Whether more entries should be fetched.  Note that we will err on\n   *          the side of getting more versions if we are unsure.\n   */\n  protected processVersion(entry: ReleaseAPIEntry): boolean {\n    const version = semver.parse(entry.tag_name);\n\n    if (!version) {\n      console.log(`Skipping empty version ${ entry.tag_name }`);\n\n      return true;\n    }\n    if (version.prerelease.length > 0) {\n      // Skip any pre-releases.\n      console.log(`Skipping pre-release ${ version.raw }`);\n\n      return true;\n    }\n    if (version.compare(this.minimumVersion) < 0) {\n      console.log(`Version ${ version } is less than the minimum ${ this.minimumVersion }, skipping.`);\n\n      // We may have new patch versions for really old releases; fetch more.\n      return true;\n    }\n    if (!/^v?[0-9.]+(?:-rc\\d+)?\\+k3s\\d+$/.test(version.raw)) {\n      console.log(`Version ${ version.raw } looks like an erroneous version, skipping.`);\n\n      return true;\n    }\n    const build = buildVersion(version);\n    const oldVersion = this.versions[version.version];\n\n    if (oldVersion) {\n      const oldBuild = buildVersion(oldVersion.version);\n\n      if (build < oldBuild) {\n        console.log(`Skipping old version ${ version.raw }, have build ${ oldVersion.version.raw }`);\n\n        // Since we read from newest first, we may end up with older builds of\n        // some newer release, but still need to fetch the last build of an\n        // older release.  So we still need to fetch more.\n        return true;\n      }\n      if (build === oldBuild) {\n        // If we see the _exact_ same version, we've found something we've\n        // already seen before for sure.  This is the only situation where we\n        // can be sure that we will not find more useful versions.\n        console.log(`Found old version ${ version.raw }, stopping.`);\n        console.debug(util.inspect({ version: version.raw, all: Object.keys(this.versions) }));\n\n        return false;\n      }\n    }\n\n    // Check that this release has all the assets we expect.\n    if (entry.assets.find(ea => ea.name === this.filenames.exe) &&\n        entry.assets.find(ea => ea.name === this.filenames.checksum)) {\n      const foundImage = this.filenames.images.find(name => entry.assets.some(v => v.name === name));\n\n      if (foundImage) {\n        this.versions[version.version] = new SemanticVersionEntry(version);\n        console.log(`Adding version ${ version.raw } - ${ foundImage }`);\n      } else {\n        console.debug(`Skipping version ${ version.raw } due to missing image`);\n      }\n    } else {\n      console.debug(`Skipping version ${ version.raw } due to missing files`);\n    }\n\n    return true;\n  }\n\n  /**\n   * Produce a promise that is resolved after a short delay, used for retrying\n   * API requests when GitHub API requests are being rate-limited.\n   */\n  protected async delayForWaitLimiting(duration = 1_000): Promise<void> {\n    // This is a separate method so that we could override it in the tests.\n    // Jest cannot override setTimeout: https://stackoverflow.com/q/52727220/\n    await util.promisify(setTimeout)(duration);\n  }\n\n  /**\n   * Compare two channel names for sorting.\n   */\n  protected compareChannels(a: string, b: string) {\n    // The names are either a word (\"stable\", \"testing\", etc.) or a branch\n    // (\"v1.2\", etc.).  The sort should be words first, then branch.  For words,\n    // list \"stable\" before anything else.  We assume no release can match two\n    // branch channels at once.\n    const versionRegex = /^v(?<major>\\d+)\\.(?<minor>\\d+)/;\n\n    if (a === 'stable' || b === 'stable') {\n      // sort \"stable\" at the front\n      return a === 'stable' ? -1 : 1;\n    }\n    if (versionRegex.test(a) || versionRegex.test(b)) {\n      return versionRegex.test(a) ? 1 : -1;\n    }\n\n    return a.localeCompare(b);\n  }\n\n  /**\n   * Fetch the list of available Kubernetes versions.\n   * @throws If there were issues fetching the list of versions.\n   */\n  protected async updateCache(): Promise<void> {\n    try {\n      let wantMoreVersions = true;\n      let url = this.releaseApiUrl;\n      const channelMapping = new ChannelMapping();\n\n      await this.waitForNetwork();\n      await this.readCache();\n      console.log(`Updating release version cache with ${ Object.keys(this.versions).length } items in cache`);\n      let channelResponse: Response;\n\n      try {\n        channelResponse = await net.fetch(this.channelApiUrl,\n          { headers: { Accept: this.channelApiAccept } });\n      } catch (ex: any) {\n        console.log(`updateCache: error: ${ ex }`);\n        if (!isOnline) {\n          return;\n        }\n\n        throw ex;\n      }\n\n      if (channelResponse.ok) {\n        const ValidResourceTypes = ['channel', 'channels'];\n        const DataTypeChannel = 'channel';\n\n        interface ChannelResponse {\n          resourceType: string;\n          data?: {\n            type:   typeof DataTypeChannel;\n            name:   string;\n            latest: string;\n          }[];\n        }\n        const channels: ChannelResponse = await channelResponse.json();\n\n        console.debug(`Got K3s update channel data: ${ channels.data?.map(ch => ch.name) }`);\n        if (!ValidResourceTypes.includes(channels.resourceType)) {\n          throw new Error(`Channel response does not have correct resource type: ${ channels.resourceType }`);\n        }\n\n        for (const channel of channels.data ?? []) {\n          if (channel.type !== DataTypeChannel) {\n            // The channel entry is invalid; ignore it.\n            continue;\n          }\n\n          const version = semver.parse(channel.latest);\n\n          if (version) {\n            channelMapping[channel.name] = version;\n          }\n        }\n        console.debug('Recommended versions:', channelMapping);\n      }\n\n      while (wantMoreVersions && url) {\n        const headers: HeadersInit = { Accept: this.releaseApiAccept };\n\n        if (process.env.GITHUB_TOKEN) {\n          headers.Authorization = `Bearer ${ process.env.GITHUB_TOKEN }`;\n        }\n        const response = await net.fetch(url, { headers });\n\n        console.debug(`Fetching releases from ${ url } -> ${ response.statusText }`);\n        if (!response.ok) {\n          if (response.status === 403 && response.headers.get('X-RateLimit-Remaining') === '0') {\n            // We hit the rate limit; try again after a delay.\n            // If given, the rate limit reset time is in seconds since UTC epoch.\n            const resetTime = parseInt(response.headers.get('X-RateLimit-Reset') ?? '0', 10);\n            await this.delayForWaitLimiting(Math.min(1_000, resetTime * 1_000 - Date.now()));\n            continue;\n          }\n          throw new Error(`Could not fetch releases: ${ response.statusText }`);\n        }\n\n        const linkHeader = response.headers.get('Link');\n\n        if (linkHeader) {\n          const [, nextURL] = /<([^>]+)>; rel=\"next\"/.exec(linkHeader) || [];\n\n          url = nextURL;\n        } else {\n          url = '';\n        }\n\n        wantMoreVersions = true;\n        for (const entry of (await response.json()) as ReleaseAPIEntry[]) {\n          if (!this.processVersion(entry)) {\n            wantMoreVersions = false;\n            break;\n          }\n        }\n      }\n\n      // Apply channel data\n      for (const [channel, version] of Object.entries(channelMapping)) {\n        const entry = this.versions[version.version];\n\n        if (entry) {\n          if (this.versionFromChannel[channel] && this.versionFromChannel[channel] !== version.version) {\n            const otherEntry = this.versions[this.versionFromChannel[channel]];\n\n            if (otherEntry?.channels) {\n              otherEntry.channels = otherEntry.channels.filter(ch => ch !== channel);\n              if (otherEntry.channels.length === 0) {\n                delete otherEntry.channels;\n              }\n            }\n          }\n          entry.channels ??= [];\n          if (!entry.channels.includes(channel)) {\n            entry.channels.push(channel);\n            entry.channels.sort(this.compareChannels);\n          }\n          this.versionFromChannel[channel] = version.version;\n        }\n      }\n\n      console.log(`Got ${ Object.keys(this.versions).length } versions.`);\n      await this.writeCache();\n      this.emit('versions-updated');\n    } catch (e) {\n      console.error(e);\n      throw e;\n    }\n  }\n\n  /**\n   * Mark the network as ready; this is used as a barrier to ensure we do not\n   * make network requests before setup is complete.\n   */\n  networkReady() {\n    this.pendingNetworkSetup.resolve();\n  }\n\n  /**\n   * This function waits for the `networkReady()` method to be called.\n   */\n  protected async waitForNetwork() {\n    // `this.pendingNetworkSetup` is a Promise with an extra method that can be\n    // used to resolve the promise.  By awaiting on it, we pause execution until\n    // `this.networkReady()` is called (which resolves the promise).\n    await this.pendingNetworkSetup;\n  }\n\n  /**\n   * Initialize the version fetcher.\n   * @returns A promise that is resolved when the initialization is complete.\n   */\n  initialize(): Promise<void> {\n    if (!this.pendingInitialize) {\n      this.versionFromChannel = {};\n      this.pendingInitialize = (async() => {\n        await this.readCache();\n        if (Object.keys(this.versions).length > 0) {\n          // Start a cache update asynchronously without waiting for it\n          this.updateCache().catch((ex: any) => {\n            console.log(`updateCache failed: ${ ex }`);\n          });\n\n          return;\n        }\n        try {\n          await this.updateCache();\n        } catch (ex) {\n          console.log(`Ignoring failure to get initial versions list: ${ ex }`);\n          // At this point this.versions is still empty.\n        }\n      })();\n    }\n\n    return this.pendingInitialize;\n  }\n\n  /**\n   * Return the version of k3s current installed, if available.\n   */\n  static async getInstalledK3sVersion(executor: VMExecutor): Promise<string | undefined> {\n    let stdout: string;\n\n    try {\n      stdout = await executor.execCommand({ capture: true, expectFailure: true }, '/usr/local/bin/k3s', '--version');\n    } catch (ex) {\n      console.debug(`Failed to get k3s version: ${ ex } - assuming not installed.`);\n\n      return undefined;\n    }\n\n    const line = stdout.split('/\\r?\\n/').find(line => line.startsWith('k3s version '));\n\n    if (!line) {\n      console.debug(`K3s version not in --version output.`);\n\n      return undefined;\n    }\n\n    const match = /^k3s version v?((?:\\d+\\.?)+\\+k3s\\d+)/.exec(line);\n\n    if (!match) {\n      console.debug(`Invalid k3s version line: ${ line.trim() }`);\n\n      return undefined;\n    }\n\n    console.debug(`Got installed k3s version: ${ match[1] } (${ match[0] })`);\n\n    return match[1];\n  }\n\n  /**\n   * The versions that are available to install.\n   * @note The list will be empty if the machine is offline and we have no\n   * cached versions.\n   */\n  get availableVersions(): Promise<SemanticVersionEntry[]> {\n    return (async() => {\n      await this.initialize();\n      const wrappedVersions = Object.values(this.versions);\n      const finalOptions = isOnline ? wrappedVersions : await K3sHelper.filterVersionsAgainstCache(wrappedVersions);\n\n      return finalOptions.sort((a, b) => b.version.compare(a.version));\n    })();\n  }\n\n  static cachedVersionsOnly(): Promise<boolean> {\n    return Promise.resolve(!isOnline);\n  }\n\n  static async filterVersionsAgainstCache(fullVersionList: SemanticVersionEntry[]): Promise<SemanticVersionEntry[]> {\n    try {\n      const cacheDir = path.join(paths.cache, 'k3s');\n      const k3sFilenames = (await fs.promises.readdir(cacheDir))\n        .filter(dirname => /^v\\d+\\.\\d+\\.\\d+\\+k3s\\d+$/.test(dirname));\n      const versionSet = new Set(k3sFilenames.map(filename => semver.parse(filename)?.version)\n        .filter(defined));\n\n      return fullVersionList.filter(versionEntry => versionSet.has(versionEntry.version.version));\n    } catch (e: any) {\n      if (e.code === 'ENOENT') {\n        return [];\n      }\n      console.log('filterVersionsAgainstCache: Got exception:', e);\n      throw e;\n    }\n  }\n\n  /** The download URL prefix for K3s releases. */\n  protected get downloadUrl() {\n    return 'https://github.com/k3s-io/k3s/releases/download';\n  }\n\n  /**\n   * Variable to keep track of download progress\n   */\n  progress = {\n    exe:      { current: 0, max: 0 },\n    images:   { current: 0, max: 0 },\n    checksum: { current: 0, max: 0 },\n  };\n\n  /**\n   * Find the cached version closest to the desired version.\n   * @param desiredVersion The semver of the version of k3s the system would prefer to use,\n   *                       with a '+k3s###' suffix\n   * @returns A semver of the version to use, also with a '+k3s###' suffix\n   */\n  static async selectClosestImage(desiredVersion: semver.SemVer): Promise<semver.SemVer> {\n    const cacheDir = path.join(paths.cache, 'k3s');\n    const k3sFilenames = (await fs.promises.readdir(cacheDir))\n      .filter(dirname => /^v\\d+\\.\\d+\\.\\d+\\+k3s\\d+$/.test(dirname));\n\n    return this.selectClosestSemVer(desiredVersion, k3sFilenames);\n  }\n\n  /**\n   * Given a semver for the desired version, and a list of names representing other\n   * k3s versions (matching /v\\d+\\.\\d+\\.\\d+\\+k3s\\d+/), return the semver for the name\n   * that is considered closest to the desired version:\n   *\n   * @precondition the desired version wasn't found\n   * @param desiredVersion a semver for the version currently specified in the config\n   * @param k3sNames typically a list of names like 'v1.2.3+k3s4'\n   * @returns {semver.SemVer} the oldest version newer than the desired version\n   *      If there is more than one such version, favor the one with the highest '+k3s' build version\n   *      If there are none, the newest version older than the desired version\n   * @throws {NoCachedK3sVersionsError} if no names are suitable\n   */\n  protected static selectClosestSemVer(desiredVersion: semver.SemVer, k3sNames: string[]): semver.SemVer {\n    const existingVersions = k3sNames.map(filename => semver.parse(filename)).filter(defined);\n\n    if (existingVersions.length === 0) {\n      throw new NoCachedK3sVersionsError();\n    }\n    existingVersions.sort((v1, v2): number => {\n      return v1.compare(v2) || this.compareBuildVersions(v1, v2);\n    });\n    const filteredVersions = this.keepHighestBuildVersion(existingVersions);\n    const firstAcceptableVersion = filteredVersions.find(v => v.compare(desiredVersion) >= 0);\n\n    return firstAcceptableVersion ?? filteredVersions[filteredVersions.length - 1];\n  }\n\n  // A comparator when the versions are the same so we need to compare the numeric part of the '+k3s...' parts\n  protected static compareBuildVersions(v1: semver.SemVer, v2: semver.SemVer): number {\n    return this.k3sValue(v1) - this.k3sValue(v2);\n  }\n\n  protected static k3sValue(v: semver.SemVer): number {\n    try {\n      return parseInt((v.build[0]).replace('k3s', ''), 10) || 0;\n    } catch {\n      return 0;\n    }\n  }\n\n  /**\n   * Normally we should have only one build version in the cache for any MAJOR.MINOR.PATCH\n   * But if we don't, ignore the lower build versions. This code is used to simplify the selection process\n   * by removing the lower-build versions from consideration.\n   * @precondition existingVersions is sorted such that `a[i].compare(a[i+1]) <= 0` for i in 0..a.length - 2\n   * @param existingVersions {Array<semver.SemVer>} versions to choose from\n   * @returns {Array<semver.SemVer>}: existingVersions,\n   *          with lower-build versions culled out as described above.\n   */\n  protected static keepHighestBuildVersion(existingVersions: semver.SemVer[]): semver.SemVer[] {\n    // Keep only the highest build for each version\n    return existingVersions.filter((v, i) => {\n      const next = existingVersions[i + 1];\n\n      return next === undefined || v.compare(next) < 0;\n    });\n  }\n\n  /**\n  * Ensure that the K3s assets have been downloaded into the cache, which is\n  * at (paths.cache())/k3s.\n  * @param version The version of K3s to download, without the k3s suffix.\n  */\n  async ensureK3sImages(version: semver.SemVer): Promise<void> {\n    const cacheDir = path.join(paths.cache, 'k3s');\n\n    console.log(`Ensuring images available for K3s ${ version }`);\n    const verifyChecksums = async(dir: string): Promise<Error | null> => {\n      try {\n        const sumFile = await fs.promises.readFile(path.join(dir, this.filenames.checksum), 'utf-8');\n        const sums: Record<string, string> = {};\n\n        for (const line of sumFile.split(/[\\r\\n]+/)) {\n          const match = /^\\s*([0-9a-f]+)\\s+(.*)/i.exec(line.trim());\n\n          if (!match) {\n            continue;\n          }\n          const [, sum, filename] = match;\n\n          sums[filename] = sum;\n        }\n\n        let existsIndex;\n\n        for (let index = 0; typeof existsIndex === 'undefined' && index < this.filenames.images.length; index++) {\n          try {\n            await fs.promises.access(path.join(dir, this.filenames.images[index]), fs.constants.R_OK);\n            existsIndex = index;\n          } catch {\n            // ignore access error and try next iteration if any\n          }\n        }\n        if (typeof existsIndex === 'undefined') {\n          existsIndex = 0;\n        }\n        const promises = [this.filenames.exe, this.filenames.images[existsIndex]].map(async(filename) => {\n          const hash = crypto.createHash('sha256');\n\n          await new Promise((resolve) => {\n            hash.on('finish', resolve);\n            fs.createReadStream(path.join(dir, filename)).pipe(hash);\n          });\n\n          const digest = hash.digest('hex');\n\n          if (digest.localeCompare(sums[filename], undefined, { sensitivity: 'base' }) !== 0) {\n            return new Error(`${ filename } has invalid digest ${ digest }, expected ${ sums[filename] }`);\n          }\n\n          return null;\n        });\n\n        return (await Promise.all(promises)).filter(x => x)[0];\n      } catch (ex) {\n        if ((ex as NodeJS.ErrnoException).code !== 'ENOENT') {\n          throw ex;\n        }\n\n        if (!(ex instanceof Error)) {\n          return null;\n        }\n\n        return ex;\n      }\n    };\n\n    await fs.promises.mkdir(cacheDir, { recursive: true });\n    if (!await verifyChecksums(path.join(cacheDir, version.raw))) {\n      console.log(`Cache at ${ cacheDir } is valid.`);\n\n      return;\n    }\n\n    const workDir = await fs.promises.mkdtemp(path.join(cacheDir, `tmp-${ version.raw }-`));\n\n    try {\n      await Promise.all(Object.entries(this.filenames).map(async([filekey, filename]) => {\n        const namearray = Array.isArray(filename) ? filename : [filename];\n\n        let outPath;\n        let response: Response | undefined;\n\n        for (const name of namearray) {\n          const fileURL = `${ this.downloadUrl }/${ version.raw }/${ name }`;\n\n          outPath = path.join(workDir, name);\n          console.log(`Will attempt to download ${ filekey } ${ fileURL } to ${ outPath }`);\n          response = await net.fetch(fileURL);\n          if (response.ok) {\n            break;\n          }\n        }\n\n        if (!response || !outPath) {\n          throw new Error(`Error downloading ${ filename } ${ version }: No ${ filekey }s found`);\n        }\n\n        // response.body implements ReadableStream, but it uses a different set\n        // of typings so TypeScript doesn't understand it natively.\n        const body: NodeJS.ReadableStream | null = response.body as any;\n\n        if (!body) {\n          throw new Error(`Error downloading ${ filename } ${ version }: No response body`);\n        }\n\n        const progresskey = filekey as keyof typeof K3sHelper.prototype.filenames;\n        const status = this.progress[progresskey];\n\n        status.current = 0;\n        const progress = new DownloadProgressListener(status);\n        const writeStream = fs.createWriteStream(outPath);\n\n        status.max = parseInt(response.headers.get('Content-Length') || '0');\n        await util.promisify(stream.pipeline)(body, progress, writeStream);\n      }));\n\n      const error = await verifyChecksums(workDir);\n\n      if (error) {\n        console.log('Error verifying checksums after download', error);\n        throw error;\n      }\n      await safeRename(workDir, path.join(cacheDir, version.raw));\n    } finally {\n      await fs.promises.rm(workDir, {\n        recursive: true, maxRetries: 3, force: true,\n      });\n    }\n  }\n\n  /**\n   * Wait the K3s server to be ready after starting up.\n   *\n   * This will check that the proper TLS certificate is generated by K3s; this\n   * is required as if the VM IP address changes, K3s will use a certificate\n   * that is only valid for the old address for a short while.  If we attempt to\n   * communicate with the cluster at this point, things will fail.\n   *\n   * @param getHost A function to return the IP address that K3s will listen on\n   *                internally.  This may be called multiple times, as the\n   *                address may not be ready yet.\n   * @param port The port that K3s will listen on.\n   */\n  async waitForServerReady(getHost: () => Promise<string | undefined>, port: number): Promise<void> {\n    let host: string | undefined;\n\n    console.log(`Waiting for K3s server to be ready on port ${ port }...`);\n    while (true) {\n      try {\n        host = await getHost();\n\n        if (typeof host === 'undefined') {\n          await util.promisify(setTimeout)(500);\n          continue;\n        }\n\n        await new Promise<void>((resolve, reject) => {\n          const socket = tls.connect(\n            {\n              host, port, rejectUnauthorized: false,\n            },\n            () => {\n              const cert = socket.getPeerCertificate();\n\n              // Check that the certificate contains a SubjectAltName that\n              // includes the host we're looking for; when the server starts, it\n              // may be using an obsolete certificate from a previous run that\n              // doesn't include the current IP address.\n              const names = cert.subjectaltname?.split(',')?.map(s => s.trim()) ?? [];\n              const acceptable = [`IP Address:${ host }`, `DNS:${ host }`];\n\n              if (!names.some(name => acceptable.includes(name))) {\n                return reject({ code: 'EAGAIN' });\n              }\n\n              // Check that the certificate is valid; if the IP address _didn't_\n              // change, but the cert is old, we need to wait for it to be\n              // regenerated.\n              if (Date.parse(cert.valid_from).valueOf() >= Date.now()) {\n                return reject({ code: 'EAGAIN' });\n              }\n\n              resolve();\n            });\n\n          socket.on('error', reject);\n        });\n        break;\n      } catch (error) {\n        if (!isUnixError(error)) {\n          console.error(error);\n\n          return;\n        }\n\n        switch (error.code) {\n        case 'EAGAIN':\n        case 'ECONNREFUSED':\n        case 'ECONNRESET':\n          break;\n        default:\n          // Unrecognized error; log but continue waiting.\n          console.error(error);\n        }\n        await util.promisify(setTimeout)(1_000);\n      }\n    }\n    console.log(`The K3s server is ready on ${ host }:${ port }.`);\n  }\n\n  /**\n   * Find the kubeconfig file containing the given context; if none is found,\n   * return the default kubeconfig path.\n   * @param contextName The name of the context to look for\n   */\n  static async findKubeConfigToUpdate(contextName: string): Promise<string> {\n    const candidatePaths = process.env.KUBECONFIG?.split(path.delimiter) || [];\n\n    for (const kubeConfigPath of candidatePaths) {\n      const config = new KubeConfig();\n\n      try {\n        config.loadFromFile(kubeConfigPath, { onInvalidEntry: ActionOnInvalid.FILTER });\n        if (config.contexts.find(ctx => ctx.name === contextName)) {\n          return kubeConfigPath;\n        }\n      } catch (err) {\n        if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n          throw err;\n        }\n      }\n    }\n    const home = findHomeDir();\n\n    if (home) {\n      const kubeDir = path.join(home, '.kube');\n\n      await fs.promises.mkdir(kubeDir, { recursive: true });\n\n      return path.join(kubeDir, 'config');\n    }\n\n    throw new Error(`Could not find a kubeconfig`);\n  }\n\n  /**\n   * Update the user's kubeconfig such that the K3s context is available and\n   * set as the current context.  This assumes that K3s is already running.\n   *\n   * @param configReader A function that returns the kubeconfig from the K3s VM.\n   */\n  async updateKubeconfig(configReader: () => Promise<string>): Promise<void> {\n    const contextName = 'rancher-desktop';\n    const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rancher-desktop-kubeconfig-'));\n\n    try {\n      const workPath = path.join(workDir, 'kubeconfig');\n\n      // For some reason, using KubeConfig.loadFromFile presents permissions\n      // errors; doing the same ourselves seems to work better.  Since the file\n      // comes from the WSL container, it must not contain any paths, so there\n      // is no need to fix it up.  This also lets us use an external function to\n      // read the kubeconfig.\n      const workConfig = new KubeConfig();\n      const workContents = await configReader();\n\n      workConfig.loadFromString(workContents);\n      // @kubernetes/client-node doesn't have an API to modify the configs...\n      const contextIndex = workConfig.contexts.findIndex(context => context.name === workConfig.currentContext);\n\n      if (contextIndex >= 0) {\n        const context = workConfig.contexts[contextIndex];\n        const userIndex = workConfig.users.findIndex(user => user.name === context.user);\n        const clusterIndex = workConfig.clusters.findIndex(cluster => cluster.name === context.cluster);\n\n        if (userIndex >= 0) {\n          workConfig.users[userIndex] = { ...workConfig.users[userIndex], name: contextName };\n        }\n        if (clusterIndex >= 0) {\n          workConfig.clusters[clusterIndex] = { ...workConfig.clusters[clusterIndex], name: contextName };\n        }\n        workConfig.contexts[contextIndex] = {\n          ...context, name: contextName, user: contextName, cluster: contextName,\n        };\n\n        workConfig.currentContext = contextName;\n      }\n      const userPath = await K3sHelper.findKubeConfigToUpdate(contextName);\n      const userConfig = new KubeConfig();\n\n      // @kubernetes/client-node throws when merging things that already exist\n      const merge = <T extends { name: string }>(list: T[], additions: T[]) => {\n        for (const addition of additions) {\n          const index = list.findIndex(item => item.name === addition.name);\n\n          if (index < 0) {\n            list.push(addition);\n          } else {\n            list[index] = addition;\n          }\n        }\n      };\n\n      console.log(`Updating kubeconfig ${ userPath }...`);\n      try {\n        // Don't use loadFromFile() because it calls MakePathsAbsolute().\n        // Use custom loadFromString() that supports the `proxy-url` cluster field.\n        loadFromString(userConfig, fs.readFileSync(userPath, 'utf8'), { onInvalidEntry: ActionOnInvalid.FILTER });\n      } catch (err) {\n        if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n          console.log(`Error trying to load kubernetes config file ${ userPath }:`, err);\n        }\n        // continue to merge into an empty userConfig == `{ contexts: [], clusters: [], users: [] }`\n      }\n      merge(userConfig.contexts, workConfig.contexts);\n      merge(userConfig.users, workConfig.users);\n      merge(userConfig.clusters, workConfig.clusters);\n      userConfig.currentContext ||= contextName;\n      // Use custom exportConfig() that supports the `proxy-url` cluster field.\n      const userYAML = this.ensureContentsAreYAML(exportConfig(userConfig));\n      const writeStream = fs.createWriteStream(workPath, { mode: 0o600 });\n\n      await new Promise<void>((resolve, reject) => {\n        writeStream.on('error', reject);\n        writeStream.on('finish', resolve);\n        writeStream.end(userYAML, 'utf-8');\n      });\n      await safeRename(workPath, userPath);\n    } finally {\n      await fs.promises.rm(workDir, {\n        recursive: true, force: true, maxRetries: 10,\n      });\n    }\n  }\n\n  /**\n   * We normally parse all the config files, yaml and json, with yaml.parse, so yaml.parse\n   * should work with json here.\n   */\n  protected ensureContentsAreYAML(contents: string): string {\n    try {\n      return yaml.stringify(yaml.parse(contents));\n    } catch (err) {\n      console.log(`Error in k3sHelper.ensureContentsAreYAML: ${ err }`);\n    }\n\n    return contents;\n  }\n\n  /**\n   * Delete state related to Kubernetes.  This will ensure that images are not\n   * deleted.\n   * @param executor The interface to run commands in the VM.\n   */\n  async deleteKubeState(executor: VMExecutor) {\n    const directories = [\n      '/var/lib/kubelet', // https://github.com/kubernetes/kubernetes/pull/86689\n      // We need to keep /var/lib/rancher/k3s/agent/containerd for the images.\n      '/var/lib/rancher/k3s/data',\n      '/var/lib/rancher/k3s/server',\n      '/var/lib/rancher/k3s/storage',\n      '/etc/rancher/k3s',\n      '/run/k3s',\n    ];\n\n    console.log(`Attempting to remove K3s state: ${ directories.sort().join(' ') }`);\n    await Promise.all(directories.map(d => executor.execCommand({ root: true }, 'rm', '-rf', d)));\n  }\n\n  /**\n   * Manually uninstall the K3s-installed copy of Traefik, if it exists.\n   * This exists to work around https://github.com/k3s-io/k3s/issues/5103\n   */\n  async uninstallHelmChart(client: KubeClient, ownerName: string) {\n    const deadline = Date.now() + 10 * 60 * 1_000;\n\n    // If the Kubernetes server is not ready yet, we need to retry until it is.\n    // However, don't attempt that forever; only loop until we hit a deadline.\n    while (Date.now() < deadline) {\n      try {\n        const customApi = client.k8sClient.makeApiClient(CustomObjectsApi);\n        const response = await customApi.listNamespacedCustomObject({\n          group:     'helm.cattle.io',\n          version:   'v1',\n          namespace: 'kube-system',\n          plural:    'helmcharts',\n        });\n        const charts: V1HelmChart[] = response?.items ?? [];\n\n        await Promise.all(charts.filter((chart) => {\n          const annotations = chart.metadata?.annotations ?? {};\n\n          return chart.metadata?.name && (annotations['objectset.rio.cattle.io/owner-name'] === ownerName);\n        }).map((chart) => {\n          const name = chart.metadata?.name;\n\n          if (name) {\n            console.debug(`Will delete helm chart ${ name }`);\n\n            return customApi.deleteNamespacedCustomObject({\n              group:     'helm.cattle.io',\n              version:   'v1',\n              namespace: 'kube-system',\n              plural:    'helmcharts',\n              name,\n            });\n          }\n        }).filter(defined));\n\n        return;\n      } catch (ex) {\n        if (ex instanceof ApiException && [429, 503].includes(ex.code)) {\n          const body = typeof ex.body === 'string' ? JSON.parse(ex.body) : ex.body;\n          const delay = body?.details?.retryAfterSeconds || 1;\n          console.debug(`Got Service Unavailable (${ body.reason }: ${ body.message }), retrying...`);\n          await util.promisify(setTimeout)(delay * 1_000);\n          continue;\n        }\n        console.error(`Error uninstalling ${ ownerName }`, ex);\n\n        return;\n      }\n    }\n\n    console.error('Timed out uninstalling Traefik, giving up');\n  }\n\n  /**\n   * Rancher Desktop's exposed `kubectl` utility is actually a wrapper around `kuberlr`,\n   * which guarantees that the actual true `kubectl` utility is compatible\n   * with the current version of kubernetes on the server.\n   *\n   * Calling `kubectl --context rancher-desktop cluster-info` is a good way to verify\n   * that the correct version of `kubectl` is available, or to let the user know there\n   * was a problem downloading it.\n   *\n   * @param version\n   */\n  async getCompatibleKubectlVersion(version: semver.SemVer): Promise<void> {\n    const commandArgs = ['--context', 'rancher-desktop', 'cluster-info'];\n\n    try {\n      const { stdout, stderr } = await childProcess.spawnFile(executable('kubectl'),\n        commandArgs,\n        { stdio: ['ignore', 'pipe', 'pipe'] });\n\n      if (stdout) {\n        console.info(stdout);\n      }\n      if (stderr) {\n        console.log(stderr);\n      }\n    } catch (ex: any) {\n      console.error(`Error priming kuberlr: ${ ex }`);\n      console.log(`Output from kuberlr:\\nex.stdout: [${ ex.stdout ?? 'none' }],\\nex.stderr: [${ ex.stderr ?? 'none' }]`);\n      const pattern = /Right kubectl missing, downloading.+Error while trying to get contents of .+\\/kubernetes-release/s;\n\n      if (pattern.test(ex.stderr)) {\n        const major = version.major;\n        const minor = version.minor;\n        const lowMinor = minor === 0 ? 0 : minor - 1;\n        const highMinor = minor + 1;\n        const homeDirName = os.platform().startsWith('win') ? (findHomeDir() ?? '%HOME%') : '~';\n        const kuberlrCacheDirName = `${ os.platform() }-${ process.env.M1 ? 'arm64' : 'amd64' }`;\n        const options: Electron.MessageBoxOptions = {\n          message: \"Can't download a compatible version of kubectl in offline-mode\",\n          detail:  `Please acquire a version in the range ${ major }.${ lowMinor } - ${ major }.${ highMinor } and install in '${ path.join(homeDirName, '.kuberlr', kuberlrCacheDirName) }'`,\n          type:    'error',\n          buttons: ['OK'],\n          title:   'Network failure',\n        };\n\n        await showMessageBox(options, true);\n      } else {\n        console.log('Failed to match a kuberlr network access issue.');\n      }\n    }\n  }\n\n  /**\n   * Helper for implementing KubernetesBackend.requiresRestartReasons\n   */\n  requiresRestartReasons(\n    currentSettings: K8s.BackendSettings,\n    desiredSettings: RecursivePartial<K8s.BackendSettings>,\n    checkers: RequiresRestartCheckers,\n    extras: ExtraRequiresReasons = {},\n  ): K8s.RestartReasons {\n    const results: K8s.RestartReasons = {};\n    const NotFound = Symbol('not-found');\n    const mergedSettings = _.merge({}, currentSettings, desiredSettings);\n\n    function restartIfKubernetesEnabled() {\n      return mergedSettings.kubernetes.enabled ? 'restart' : false;\n    }\n\n    /**\n     * defaultRestartReasonCheckers contains the restart reason checkers shared\n     * between backends.\n     */\n    const defaultRestartReasonCheckers: RequiresRestartCheckers = {\n      'containerEngine.mobyStorageDriver': (current, desired, allSettings) => {\n        // We only need to restart if running moby.\n        return allSettings.containerEngine.name === ContainerEngine.MOBY ? 'restart' : false;\n      },\n      'kubernetes.version': (current, desired, allSettings) => {\n        if (!allSettings.kubernetes.enabled) {\n          return false;\n        }\n        return semver.gt(current || '0.0.0', desired) ? 'reset' : 'restart';\n      },\n      'containerEngine.allowedImages.enabled':            undefined,\n      'containerEngine.name':                             undefined,\n      'experimental.containerEngine.webAssembly.enabled': undefined,\n      'experimental.kubernetes.options.spinkube':         undefined,\n      'kubernetes.enabled':                               undefined,\n      'kubernetes.options.flannel':                       restartIfKubernetesEnabled,\n      'kubernetes.options.traefik':                       restartIfKubernetesEnabled,\n      'kubernetes.port':                                  restartIfKubernetesEnabled,\n    };\n\n    /**\n     * Check the given settings against the last-applied settings to see if we\n     * need to restart the backend.\n     * @param key The identifier to use for the UI.\n     */\n    function cmp<K extends keyof K8s.RestartReasons>(key: K, checker?: RequiresRestartSeverityChecker<K>) {\n      const current: RecursiveTypes<K8s.BackendSettings>[K] | typeof NotFound = _.get(currentSettings, key, NotFound) as any;\n      const desired: RecursiveTypes<K8s.BackendSettings>[K] | typeof NotFound = _.get(desiredSettings, key, NotFound) as any;\n\n      if (current === NotFound) {\n        throw new Error(`Invalid restart check: path ${ path } not found on current values`);\n      }\n      if (desired === NotFound) {\n        // desiredSettings does not contain the full set.\n        return;\n      }\n      if (!_.isEqual(current, desired)) {\n        const severity = checker ? checker(current, desired, mergedSettings) : 'restart';\n\n        if (severity) {\n          results[key] = { current, desired, severity };\n        }\n      }\n    }\n\n    for (const [key, checker] of Object.entries({ ...defaultRestartReasonCheckers, ...checkers })) {\n      if (checker === null) {\n        // The custom checker wants to delete a default checker.\n        continue;\n      }\n      // We need the casts here because TypeScript can't match up the key with\n      // its corresponding checker.\n      cmp(key as any, checker as any);\n    }\n\n    for (const [key, entry] of Object.entries(extras)) {\n      if (!entry) {\n        // The list is hard-coded; getting here means a programming error.\n        throw new Error(`Invalid requiresRestartReasons extra key ${ key }`);\n      }\n\n      const desired = _.get(desiredSettings, key);\n      const { current, severity } = entry;\n\n      if (!_.isEqual(current, desired)) {\n        results[key as keyof K8s.RestartReasons] = {\n          current, desired, severity: severity ? (severity as any)(current, desired) : 'restart',\n        };\n      }\n    }\n\n    return results;\n  }\n}\n\ninterface V1HelmChart {\n  apiVersion?: 'helm.cattle.io/v1';\n  kind?:       'HelmChart';\n  metadata?:   V1ObjectMeta;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/k8s.ts",
    "content": "import semver from 'semver';\n\nimport { BackendSettings, RestartReasons } from './backend';\nimport K3sHelper, { ExtraRequiresReasons } from './k3sHelper';\n\nimport EventEmitter from '@pkg/utils/eventEmitter';\nimport { SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { ServiceEntry } from './kube/client';\n\nexport { State, BackendError as KubernetesError } from './backend';\nexport type {\n  BackendSettings, FailureDetails, RestartReasons, BackendProgress as KubernetesProgress,\n} from './backend';\nexport type { ServiceEntry } from './kube/client';\n\n/**\n * KubernetesBackendEvents describes the events that may be emitted by a\n * Kubernetes backend (as an EventEmitter).  Each property name is the name of\n * an event, and the property type is the type of the callback function expected\n * for the given event.\n */\nexport interface KubernetesBackendEvents {\n  /**\n   * Emitted when the set of Kubernetes services has changed.\n   */\n  'service-changed'(services: ServiceEntry[]): void;\n\n  /**\n   * Emitted when an error related to the port forwarding server has occurred.\n   */\n  'service-error'(service: ServiceEntry, errorMessage: string): void;\n\n  /**\n   * Emitted when the versions of Kubernetes available has changed.\n   */\n  'versions-updated'(): void;\n\n  /**\n   * Emitted when k8s is running on a new port\n   */\n  'current-port-changed'(port: number): void;\n}\n\nexport interface KubernetesBackend extends EventEmitter<KubernetesBackendEvents>, KubernetesBackendPortForwarder {\n  /**\n   * The versions that are available to install, sorted as would be displayed to\n   * the user.\n   */\n  availableVersions: Promise<SemanticVersionEntry[]>;\n\n  /**\n   * Used to let the UI know whether it was sent all potentially supported k8s versions.\n   * If this returns true, it means we're only telling the UI which versions we have cached.\n   */\n  cachedVersionsOnly(): Promise<boolean>;\n\n  /** The version of Kubernetes that is currently installed. */\n  version: string;\n\n  /**\n   * The port the Kubernetes server will listen on; this may not reflect the\n   * port correctly if the server is not active.\n   */\n  readonly desiredPort: number;\n\n  /**\n   * Fetch the list of services currently known to Kubernetes.\n   * @param namespace The namespace containing services; omit this to\n   *                  return services across all namespaces.\n   */\n  listServices(namespace?: string): ServiceEntry[];\n\n  /**\n   * Download the version of K3s as specified in the settings.\n   * @returns The version, or undefined if a downgrade is required but the user\n   *          did not agree to it; plus a boolean describing if the result is a\n   *          downgrade.\n   */\n  download(config: BackendSettings): Promise<readonly [semver.SemVer | undefined, boolean]>;\n\n  /**\n   * Delete Kubernetes data that may cause issues if we were to move to the\n   * given version.\n   */\n  deleteIncompatibleData(desiredVersion: semver.SemVer): Promise<void>;\n\n  /**\n   * Install a pre-downloaded version of Kubernetes.\n   */\n  install(config: BackendSettings, kubernetesVersion: semver.SemVer, allowSudo: boolean): Promise<void>;\n\n  /**\n   * Start running a pre-installed version of Kubernetes.\n   */\n  start(config: BackendSettings, kubernetesVersion: semver.SemVer): Promise<void>;\n\n  /**\n   * Stop the Kubernetes backend.\n   */\n  stop(): Promise<void>;\n\n  /**\n   * Assuming Kubernetes was halted, clean up any data that would be stale.\n   */\n  cleanup(): Promise<void>;\n\n  /**\n   * Remove Kubernetes-specific data, assuming it has already been stopped.\n   */\n  reset(): Promise<void>;\n\n  /**\n   * Calculate any reasons that may require us to restart the backend, had the\n   * given new configuration been applied on top of the existing old configuration.\n   */\n  requiresRestartReasons(oldConfig: BackendSettings, newConfig: RecursivePartial<BackendSettings>, extras?: ExtraRequiresReasons): Promise<RestartReasons>;\n\n  readonly k3sHelper: K3sHelper;\n}\n\nexport interface KubernetesBackendPortForwarder {\n  /**\n   * Forward a single service port, returning the resulting local port number.\n   * @param namespace The namespace containing the service to forward.\n   * @param service The name of the service to forward.\n   * @param k8sPort The internal port of the service to forward.\n   * @param hostPort The host port to listen on for the forwarded port. Pass 0 for a random port.\n   * @returns The port listening on localhost that forwards to the service.\n   */\n  forwardPort(namespace: string, service: string, k8sPort: number | string, hostPort: number): Promise<number | undefined>;\n\n  /**\n   * Cancel an existing port forwarding.\n   * @param namespace The namespace containing the service to forward.\n   * @param service The name of the service to forward.\n   * @param k8sPort The internal port of the service to forward.\n   */\n  cancelForward(namespace: string, service: string, k8sPort: number | string): Promise<void>;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/kube/client.ts",
    "content": "// This file contains wrappers to interact with the installed Kubernetes cluster\n\nimport events from 'events';\nimport net from 'net';\nimport stream from 'stream';\nimport util from 'util';\n\nimport * as k8s from '@kubernetes/client-node';\n\nimport Logging from '@pkg/utils/logging';\nimport { defined } from '@pkg/utils/typeUtils';\n\nconst console = Logging.k8s;\n\ninterface clientError {\n  error: string;\n}\n\nfunction isClientError(val: any): val is clientError {\n  return 'error' in val;\n}\n\n/**\n * ErrorSuppressingStdin wraps a socket such that when the 'data' event handler\n * throws, we can suppress the output so we do not get a dialog box, but rather\n * just break silently.\n */\nclass ErrorSuppressingStdin extends stream.Readable {\n  #socket:    net.Socket;\n  #listeners: Record<string, (...args: any[]) => void> = {};\n  /**\n   * @param socket The underlying socket to forward to.\n   */\n  constructor(socket: net.Socket) {\n    super();\n    this.#socket = socket;\n    this.on('newListener', (eventName) => {\n      if (!(eventName in this.#listeners)) {\n        this.#listeners[eventName] = this.listener.bind(this, eventName);\n        this.#socket.on(eventName, this.#listeners[eventName]);\n      }\n    });\n    this.on('removeListener', (eventName) => {\n      if (this.listenerCount(eventName) < 1) {\n        this.#socket.removeListener(eventName, this.#listeners[eventName]);\n        delete this.#listeners[eventName];\n      }\n    });\n  }\n\n  listener(eventName: string, ...args: any[]) {\n    for (const listener of this.listeners(eventName)) {\n      try {\n        listener(...args);\n      } catch (e) {\n        console.error(isClientError(e) ? e.error : e);\n      }\n    }\n  }\n\n  _read(size: number): void {\n    this.#socket.read(size);\n  }\n\n  read(size?: number): any {\n    return this.#socket.read(size);\n  }\n}\n\n/**\n * ForwardingMap holds the outstanding listeners used to do port forwarding;\n * this mainly exists for type safety / ensuring we get the keys correct.\n */\nclass ForwardingMap {\n  protected map = new Map<string, net.Server>();\n  /**\n   * Get a forwarding entry.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param port The port to forward to on the endpoint.\n   */\n  get(namespace: string | undefined, endpoint: string, port: number | string) {\n    return this.map.get(`${ namespace || 'default' }/${ endpoint }:${ port }`);\n  }\n\n  /**\n   * Set a forwarding entry.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param port The port to forward to on the endpoint.\n   * @param server The value to set.\n   */\n  set(namespace: string | undefined, endpoint: string, port: number | string, server: net.Server) {\n    return this.map.set(`${ namespace || 'default' }/${ endpoint }:${ port }`, server);\n  }\n\n  /**\n   * Delete a forwarding entry.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param port The port to forward to on the endpoint.\n   */\n  delete(namespace: string | undefined, endpoint: string, port: number | string) {\n    return this.map.delete(`${ namespace || 'default' }/${ endpoint }:${ port }`);\n  }\n\n  /**\n   * Check if a forwarding entry already exists.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param port The port to forward to on the endpoint.\n   */\n  has(namespace: string | undefined, endpoint: string, port: number | string) {\n    return this.map.has(`${ namespace || 'default' }/${ endpoint }:${ port }`);\n  }\n\n  /**\n   * Iterate through the entries.\n   */\n  * [Symbol.iterator](): IterableIterator<[string, string, number | string, net.Server]> {\n    const iter = this.map[Symbol.iterator]();\n\n    for (const [key, server] of iter) {\n      const match = /^([^/]*)\\/([^:]+):(.+?)$/.exec(key);\n\n      if (match) {\n        const [namespace, endpoint, portString] = match;\n        const port = /^\\d+$/.test(portString) ? parseInt(portString) : portString;\n\n        yield [namespace, endpoint, port, server];\n      }\n    }\n  }\n}\n\n// Set up a watch for services\n// Since the watch API we have _doesn't_ notify us when things have\n// changed, we'll need to do some trickery and wrap the underlying watcher\n// with our own code.\nclass WrappedWatch extends k8s.Watch {\n  callback: () => void;\n\n  constructor(kubeconfig: k8s.KubeConfig, callback: () => void) {\n    super(kubeconfig);\n    this.callback = callback;\n  }\n\n  watch(\n    path: string,\n    queryParams: any,\n    callback: (phase: string, apiObj: any, watchObj?: any) => void,\n    done: (err: any) => void,\n  ): Promise<any> {\n    const wrappedCallback = (phase: string, apiObj: any, watchObj?: any) => {\n      callback(phase, apiObj, watchObj);\n      this.callback();\n    };\n\n    return super.watch(path, queryParams, wrappedCallback, done);\n  }\n}\n\n/** A single port in a service returned by KubeClient.listServices() */\nexport interface ServiceEntry {\n  /** The namespace the service is within. */\n  namespace?:  string;\n  /** The name of the service. */\n  name:        string;\n  /** The name of the port within the service. */\n  portName?:   string;\n  /** The internal port number (or name) of the service. */\n  port:        number | string;\n  /** The forwarded port on localhost (on the host), if any. */\n  listenPort?: number;\n}\n\n/**\n * KubeClient is a Kubernetes client that will _only_ manage the cluster we spin\n * up internally.  The user should call initialize() once the cluster has been\n * created.\n */\nexport class KubeClient extends events.EventEmitter {\n  protected kubeconfig = new k8s.KubeConfig();\n  protected forwarder: k8s.PortForward;\n\n  protected shutdown = false;\n\n  /**\n   * Kubernetes services across all namespaces.\n   */\n  protected services: k8s.ListWatch<k8s.V1Service> | null;\n\n  /**\n   * Active port forwarding servers.  This records the desired state: if an\n   * entry exists, then we want to set up port forwarding for it.\n   */\n  protected servers = new ForwardingMap();\n\n  /**\n   * Collection of active sockets. Used to clean up connections when attempting\n   * to close a server. Keys can be any string, but are formatted as\n   * namespace/endpoint:port to help match sockets to the corresponding server.\n   */\n  protected sockets = new Map<string, net.Socket[]>();\n\n  protected coreV1API: k8s.CoreV1Api;\n\n  /**\n   * initialize the KubeClient so that we are ready to talk to it.\n   */\n  constructor() {\n    super();\n    this.kubeconfig.loadFromDefault();\n    this.kubeconfig.currentContext = 'rancher-desktop';\n    this.forwarder = new k8s.PortForward(this.kubeconfig, true);\n    this.shutdown = false;\n    this.coreV1API = this.kubeconfig.makeApiClient(k8s.CoreV1Api);\n    this.services = null;\n  }\n\n  get k8sClient() {\n    return this.kubeconfig;\n  }\n\n  // This functionality was originally in the constructor, but in order to\n  // avoid the complexity of async constructors, extract it out into an\n  // async method.\n  async waitForServiceWatcher() {\n    const startTime = Date.now();\n    const maxWaitTime = 300_000;\n    const waitTime = 3_000;\n\n    while (true) {\n      const currentTime = Date.now();\n\n      if ((currentTime - startTime) > maxWaitTime) {\n        console.log(`Waited more than ${ maxWaitTime / 1000 } secs for kubernetes to fully start up. Giving up.`);\n        break;\n      }\n      if (await this.getServiceListWatch()) {\n        break;\n      }\n      await util.promisify(setTimeout)(waitTime);\n    }\n  }\n\n  /**\n   * Get the service watcher, ensuring that it's actually ready to react to\n   * changes in the services.\n   */\n  async getServiceListWatch() {\n    if (this.services) {\n      return this.services;\n    }\n    // If this API call reports that there are zero services currently running,\n    // return null (and it's up to the caller to retry later).\n    // This doesn't make complete sense, because if we've reached this point,\n    // the k3s server must be running. But with wsl we've observed that the service\n    // watcher needs more time to start up. When this call returns at least one\n    // service, it's ready.\n    try {\n      const { items } = await this.coreV1API.listServiceForAllNamespaces();\n\n      if (!(items.length > 0)) {\n        return null;\n      }\n    } catch (ex) {\n      console.debug(`Error fetching services: ${ ex }`);\n\n      return null;\n    }\n    this.services = new k8s.ListWatch(\n      '/api/v1/services',\n      new WrappedWatch(this.kubeconfig, () => {\n        this.emit('service-changed', this.listServices());\n      }),\n      () => this.coreV1API.listServiceForAllNamespaces());\n\n    return this.services;\n  }\n\n  /**\n   * Wait for at least one node in the cluster to become ready.  This is taken\n   * as an indication that the cluster is ready to be used.\n   */\n  async waitForReadyNodes(): Promise<void> {\n    while (true) {\n      const { items } = await this.coreV1API.listNode();\n      const conditions = items.flatMap(node => node.status?.conditions ?? []);\n      const ready = conditions.some(condition => condition.type === 'Ready' && condition.status === 'True');\n\n      if (ready) {\n        return;\n      }\n      await util.promisify(setTimeout)(1_000);\n    }\n  }\n\n  // Notify the client that the underlying Kubernetes cluster is about to go\n  // away, and we should remove any pending work.\n  destroy() {\n    this.shutdown = true;\n    for (const [namespace, endpoint, port, server] of this.servers) {\n      this.servers.delete(namespace, endpoint, port);\n      server?.close();\n    }\n    this.removeAllListeners('service-changed');\n  }\n\n  protected async getEndpointSubsets(namespace: string, endpointName: string): Promise<k8s.V1EndpointSubset[] | null> {\n    console.log(`Attempting to locate endpoint subsets ${ endpointName }...`);\n    // Loop fetching endpoints, until it matches at least one subset.\n    let target: k8s.V1EndpointSubset[] | undefined;\n\n    // TODO: switch this to using watch.\n    while (!this.shutdown) {\n      const endpoints = await this.coreV1API.listNamespacedEndpoints({\n        namespace,\n        fieldSelector: `metadata.name==${ endpointName }`,\n      });\n      const items = endpoints.items.filter(item => item.metadata?.name === endpointName);\n\n      target = items.flatMap(item => item.subsets).filter(defined);\n      if (target.length > 0 || this.shutdown) {\n        break;\n      }\n      console.log(`Could not find ${ endpointName } endpoint (${ endpoints ? 'did' : 'did not' } get endpoints), retrying...`);\n      await util.promisify(setTimeout)(1000);\n    }\n\n    return target ?? null;\n  }\n\n  protected async getActivePodFromEndpointSubsets(subsets: k8s.V1EndpointSubset[]) {\n    const addresses = subsets.flatMap(subset => subset.addresses).filter(defined);\n    const address = addresses.find(address => address.targetRef?.kind === 'Pod');\n    const target = address?.targetRef;\n\n    if (!target?.name || !target.namespace) {\n      return null;\n    }\n\n    // Fetch the pod\n    try {\n      return await this.coreV1API.readNamespacedPod({\n        name:      target.name,\n        namespace: target.namespace,\n      });\n    } catch (ex) {\n      if (ex instanceof k8s.ApiException && ex.code === 404) {\n        return null;\n      }\n      throw ex;\n    }\n  }\n\n  /**\n   * Return a pod that is part of a given endpoint and ready to receive traffic.\n   * @param namespace The namespace in which to look for resources.\n   * @param endpointName the name of an endpoint that controls ready pods.\n   */\n  async getActivePod(namespace: string, endpointName: string): Promise<k8s.V1Pod | null> {\n    console.log(`Attempting to locate ${ endpointName } pod...`);\n    while (!this.shutdown) {\n      const subsets = await this.getEndpointSubsets(namespace, endpointName);\n\n      if (!subsets) {\n        await util.promisify(setTimeout)(1000);\n        continue;\n      }\n      const pod = await this.getActivePodFromEndpointSubsets(subsets);\n\n      if (!pod) {\n        await util.promisify(setTimeout)(1000);\n        continue;\n      }\n      console.log(`Got ${ endpointName } pod: ${ pod.metadata?.namespace }:${ pod.metadata?.name }`);\n\n      return pod;\n    }\n\n    return null;\n  }\n\n  /**\n   * Formats the namespace, endpoint, and port as namespace/endpoint:port\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param port The port to forward to on the endpoint.\n   * @returns A formatted string consisting of the namespace/endpoint:port\n   */\n  private targetName =\n    (namespace: string, endpoint: string, port: number | string) => `${ namespace }/${ endpoint }:${ port }`;\n\n  /**\n   * Given a Pod object, returns its namespace, its name and the port number matching\n   * the passed port name/number.\n   * @param pod The pod to extract the info from.\n   * @param k8sPort The port name or number to get the port number from.\n   * @returns An array containing the pod namespace, the pod name and the port number.\n   */\n  protected getPodDetails(pod: k8s.V1Pod, k8sPort: number | string): [string, string, number] {\n    if (!pod.metadata) {\n      throw new Error('Pod has no metadata');\n    }\n    if (!pod.metadata.name) {\n      throw new Error('Pod has no name');\n    }\n    const podNamespace = pod.metadata.namespace ?? 'default';\n    const podName = pod.metadata.name;\n\n    let portNumber: number;\n\n    if (typeof k8sPort === 'number') {\n      portNumber = k8sPort;\n    } else {\n      if (!pod.spec) {\n        throw new Error(`Pod \"${ podName } does not have a spec property`);\n      }\n      const podPorts = pod.spec.containers.flatMap(container => container.ports);\n      const podPort = podPorts.find(port => port?.name === k8sPort);\n\n      if (!podPort) {\n        throw new Error(`Could not find port number for pod \"${ podName }`);\n      }\n      portNumber = podPort.containerPort;\n    }\n\n    return [podNamespace, podName, portNumber];\n  }\n\n  /**\n   * Forward a port to a kubernetes service. The port forwarding will not work\n   * until the endpoint is ready.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param k8sPort The port to forward to on the endpoint.\n   * @param hostPort The host port to listen on for the forwarded port. Pass 0 for a random port.\n   */\n  protected async createForwardingServer(namespace: string, endpoint: string, k8sPort: number | string, hostPort: number): Promise<net.Server> {\n    const targetName = this.targetName(namespace, endpoint, k8sPort);\n    const server = net.createServer(async(socket) => {\n      // We need some helpers to convince TypeScript that our errors have\n      // `code: string` and `error: Error` properties.\n      interface ErrorWithStringCode extends Error { code: string }\n      interface ErrorWithNestedError extends Error { error: Error }\n      const isError = <T extends Error>(error: Error, prop: string): error is T => {\n        return prop in error;\n      };\n\n      socket.on('error', (error) => {\n        // Handle the error, so that we don't get an ugly dialog about it.\n        const code = isError<ErrorWithStringCode>(error, 'code') ? error.code : 'MISSING';\n        const innerError = isError<ErrorWithNestedError>(error, 'error') ? error.error : error;\n\n        console.error(`Error creating proxy for ${ targetName }: code \"${ code }\" error \"${ innerError }\"`);\n      });\n\n      // add socket to this.sockets so it can be cleaned up\n      this.sockets.set(targetName, [...this.sockets.get(targetName) || [], socket]);\n\n      // get the details of the pod we are forwarding to\n      const endpoints = await this.getEndpointSubsets(namespace, endpoint) ?? [];\n\n      console.debug(`Got endpoints subsets: ${ JSON.stringify(endpoints) }`);\n      const pod = await this.getActivePodFromEndpointSubsets(endpoints);\n\n      console.debug(`Got active pod: ${ JSON.stringify(pod) }`);\n\n      if (!pod) {\n        throw new Error(`No active pod found`);\n      }\n\n      const [podNamespace, podName, portNumber] = this.getPodDetails(pod, k8sPort);\n\n      console.debug(`Got podNamespace = \"${ podNamespace }\"`);\n      console.debug(`Got podName = \"${ podName }\"`);\n      console.debug(`Got portNumber = \"${ portNumber }\"`);\n\n      // check if server is still valid\n      if (!this.servers.has(namespace, endpoint, k8sPort)) {\n        throw new Error('Server is no longer valid');\n      }\n\n      // forward the port\n      const stdin = new ErrorSuppressingStdin(socket);\n\n      this.forwarder.portForward(podNamespace, podName, [portNumber], socket, null, stdin)\n        .catch((e) => {\n          console.log(`Failed to create web socket for forwarding to ${ targetName }: ${ e?.error || e }`);\n          socket.destroy(e);\n        });\n    });\n\n    // Start listening, and block until the listener has been established.\n    await new Promise((resolve, reject) => {\n      const cleanup = () => {\n        resolve = reject = () => { };\n        server.off('listening', resolveOnce);\n        server.off('error', rejectOnce);\n      };\n      const resolveOnce = () => {\n        resolve(undefined);\n        cleanup();\n      };\n      const rejectOnce = (error?: any) => {\n        reject(error);\n        cleanup();\n      };\n\n      server.once('close', () => {\n        rejectOnce(new Error('Server closed'));\n      });\n      server.once('listening', resolveOnce);\n      server.once('error', rejectOnce);\n      server.listen({ port: hostPort, host: '127.0.0.1' });\n    });\n\n    return server;\n  }\n\n  /**\n   * Create a port forward for an endpoint, listening on localhost.\n   * @param namespace The namespace containing the end points to forward to.\n   * @param endpoint The endpoint to forward to.\n   * @param k8sPort The port to forward to on the endpoint.\n   * @param hostPort The host port to listen on for the forwarded port. Pass 0 for a random port.\n   * @return The port number for the port forward.\n   */\n  async forwardPort(namespace: string, endpoint: string, k8sPort: number | string, hostPort: number): Promise<number | undefined> {\n    const targetName = this.targetName(namespace, endpoint, k8sPort);\n    let server = this.servers.get(namespace, endpoint, k8sPort);\n\n    if (server) {\n      console.log(`Found existing server for ${ targetName }.`);\n      const currentHostPort = (server.address() as net.AddressInfo).port;\n\n      if (currentHostPort === hostPort) {\n        console.log(`Server listening on ${ hostPort }, which is what we want.`);\n\n        return hostPort;\n      } else {\n        console.log(`Server listening on ${ currentHostPort }, but we want ${ hostPort }. Closing it.`);\n        await this.closeServerAndConns(namespace, endpoint, k8sPort);\n      }\n    }\n\n    // create server\n    console.log(`Setting up new port forwarding to ${ targetName }...`);\n    try {\n      server = await this.createForwardingServer(namespace, endpoint, k8sPort, hostPort);\n    } catch (error: any) {\n      console.error(error);\n      let errorMessage = '';\n\n      if (error.code === 'ERR_SOCKET_BAD_PORT') {\n        errorMessage = `\"${ hostPort }\" is not a valid port.`;\n      } else if (error.code === 'EADDRINUSE') {\n        errorMessage = `Port ${ hostPort } is already in use.`;\n      } else if (error.code === 'EACCES') {\n        errorMessage = `You do not have permission to use port ${ hostPort }.`;\n      }\n\n      if (errorMessage) {\n        const serviceEntry: ServiceEntry = {\n          namespace,\n          name:       endpoint,\n          port:       k8sPort,\n          listenPort: hostPort,\n        };\n\n        this.emit('service-error', serviceEntry, errorMessage);\n\n        return;\n      }\n\n      throw error;\n    }\n    console.log(`Forwarding server for ${ targetName } created.`);\n\n    // add it to this.servers if value for targetName hasn't been filled in meantime\n    if (!this.servers.get(namespace, endpoint, k8sPort)) {\n      this.servers.set(namespace, endpoint, k8sPort, server);\n      console.log(`Forwarding server for ${ targetName } added to server list.`);\n    } else {\n      console.warn(`Another forwarding server for ${ targetName } was found; closing this one.`);\n      server.close();\n    }\n\n    // Trigger a UI refresh\n    this.emit('service-changed', this.listServices());\n\n    const address = server.address() as net.AddressInfo;\n\n    return address.port;\n  }\n\n  /**\n   * Ensure that the forwarding server for a given combination of arguments is closed,\n   * and that all connections related to it are destroyed.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param k8sPort The port to forward to on the endpoint.\n   */\n  protected async closeServerAndConns(namespace: string, endpoint: string, k8sPort: number | string): Promise<void> {\n    const targetName = this.targetName(namespace, endpoint, k8sPort);\n    const server = this.servers.get(namespace, endpoint, k8sPort);\n\n    // close and remove sockets for this server\n    this.sockets.get(targetName)?.forEach(socket => socket.destroy());\n    this.sockets.delete(targetName);\n\n    // close server\n    this.servers.delete(namespace, endpoint, k8sPort);\n    if (server) {\n      await new Promise((resolve) => {\n        server.close(resolve);\n      });\n    }\n  }\n\n  /**\n   * Ensure that a given port forwarding does not exist; if it does, close it.\n   * @param namespace The namespace to forward to.\n   * @param endpoint The endpoint in the namespace to forward to.\n   * @param k8sPort The port to forward to on the endpoint.\n   */\n  async cancelForwardPort(namespace: string, endpoint: string, k8sPort: number | string): Promise<void> {\n    await this.closeServerAndConns(namespace, endpoint, k8sPort);\n    this.emit('service-changed', this.listServices());\n  }\n\n  /**\n   * Get the cached list of services.\n   * @param namespace The namespace to limit fetches to.\n   * @returns The services currently in the system.\n   */\n  listServices(namespace: string | undefined = undefined): ServiceEntry[] {\n    if (!this.services) {\n      return [];\n    }\n\n    return this.services.list(namespace)?.flatMap((service) => {\n      return (service.spec?.ports || []).map((port) => {\n        const namespace = service.metadata?.namespace;\n        const name = service.metadata?.name || '';\n        const portNumber = port.targetPort as unknown as number;\n        const server = this.servers.get(namespace, name, portNumber);\n        const address = server?.address();\n        const listenPort = address !== undefined ? (address as net.AddressInfo).port : undefined;\n\n        return {\n          namespace,\n          name,\n          portName: port.name,\n          port:     portNumber,\n          listenPort,\n        };\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/kube/lima.ts",
    "content": "import events from 'events';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport timers from 'timers';\nimport util from 'util';\n\nimport semver from 'semver';\n\nimport { Architecture, BackendSettings, RestartReasons } from '../backend';\nimport BackendHelper, { MANIFEST_CERT_MANAGER, MANIFEST_SPIN_OPERATOR } from '../backendHelper';\nimport K3sHelper, { ExtraRequiresReasons, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper';\nimport LimaBackend, { Action } from '../lima';\n\nimport INSTALL_K3S_SCRIPT from '@pkg/assets/scripts/install-k3s';\nimport LOGROTATE_K3S_SCRIPT from '@pkg/assets/scripts/logrotate-k3s';\nimport SERVICE_CRI_DOCKERD_SCRIPT from '@pkg/assets/scripts/service-cri-dockerd.initd';\nimport SERVICE_K3S_SCRIPT from '@pkg/assets/scripts/service-k3s.initd';\nimport * as K8s from '@pkg/backend/k8s';\nimport { KubeClient } from '@pkg/backend/kube/client';\nimport { LockedFieldError } from '@pkg/config/commandLineOptions';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { checkConnectivity } from '@pkg/main/networking';\nimport clone from '@pkg/utils/clone';\nimport { SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\nimport { showMessageBox } from '@pkg/window';\n\nconst console = Logging.kube;\n\nexport default class LimaKubernetesBackend extends events.EventEmitter implements K8s.KubernetesBackend {\n  constructor(arch: Architecture, vm: LimaBackend) {\n    super();\n    this.arch = arch;\n    this.vm = vm;\n\n    this.k3sHelper = new K3sHelper(arch);\n    this.k3sHelper.on('versions-updated', () => this.emit('versions-updated'));\n    this.k3sHelper.initialize().catch((err) => {\n      console.log('k3sHelper.initialize failed: ', err);\n      // If we fail to initialize, we still need to continue (with no versions).\n      this.emit('versions-updated');\n    });\n    mainEvents.on('network-ready', () => this.k3sHelper.networkReady());\n  }\n\n  /**\n   * Download K3s images.  This will also calculate the version to download.\n   * @precondition The VM must be running.\n   * @returns The version of K3s images downloaded, and whether this is a\n   * downgrade.\n   */\n  async download(cfg: BackendSettings): Promise<[semver.SemVer | undefined, boolean]> {\n    this.cfg = cfg;\n    const interval = timers.setInterval(() => {\n      const statuses = [\n        this.k3sHelper.progress.checksum,\n        this.k3sHelper.progress.exe,\n        this.k3sHelper.progress.images,\n      ];\n      const sum = (key: 'current' | 'max') => {\n        return statuses.reduce((v, c) => v + c[key], 0);\n      };\n\n      const current = sum('current');\n      const max = sum('max');\n\n      this.progressTracker.numeric('Downloading Kubernetes components', current, max);\n    });\n\n    try {\n      const persistedVersion = await K3sHelper.getInstalledK3sVersion(this.vm);\n      const desiredVersion = await this.desiredVersion;\n\n      if (desiredVersion === undefined) {\n        // If we could not determine the desired version (e.g. we have no cached\n        // versions and the machine is offline), bail out.\n        return [undefined, false];\n      }\n\n      const isDowngrade = (version: semver.SemVer | string) => {\n        return !!persistedVersion && semver.gt(persistedVersion, version);\n      };\n\n      console.debug(`Download: desired=${ desiredVersion } persisted=${ persistedVersion }`);\n      try {\n        await this.progressTracker.action('Checking k3s images', 100, this.k3sHelper.ensureK3sImages(desiredVersion));\n\n        return [desiredVersion, isDowngrade(desiredVersion)];\n      } catch (ex) {\n        if (!await checkConnectivity('github.com')) {\n          throw ex;\n        }\n\n        try {\n          const newVersion = await K3sHelper.selectClosestImage(desiredVersion);\n\n          // Show a warning if we are downgrading from the desired version, but\n          // only if it's not already a downgrade (where the user had already\n          // accepted it).\n          if (desiredVersion.compare(newVersion) > 0 && !isDowngrade(desiredVersion)) {\n            const options: Electron.MessageBoxOptions = {\n              message:   `Downgrading from ${ desiredVersion.raw } to ${ newVersion.raw } will lose existing Kubernetes workloads. Delete the data?`,\n              type:      'question',\n              buttons:   ['Delete Workloads', 'Cancel'],\n              defaultId: 1,\n              title:     'Confirming migration',\n              cancelId:  1,\n            };\n            const result = await showMessageBox(options, true);\n\n            if (result.response !== 0) {\n              return [undefined, true];\n            }\n          }\n          console.log(`Going with alternative version ${ newVersion.raw }`);\n\n          return [newVersion, isDowngrade(newVersion)];\n        } catch (ex: any) {\n          if (ex instanceof NoCachedK3sVersionsError) {\n            throw new K8s.KubernetesError('No version available', 'The k3s cache is empty and there is no network connection.');\n          }\n          throw ex;\n        }\n      }\n    } finally {\n      timers.clearInterval(interval);\n    }\n  }\n\n  /**\n   * Install the Kubernetes files.\n   */\n  async install(config: BackendSettings, desiredVersion: semver.SemVer, allowSudo: boolean) {\n    await this.progressTracker.action('Installing k3s', 50, async() => {\n      const promises: Promise<void>[] = [];\n      promises.push(this.writeServiceScript(config, desiredVersion, allowSudo));\n\n      promises.push((async() => {\n        // installK3s removes old config and makes sure the directories are recreated;\n        // this means it must be done before adding custom manifests.\n        await this.installK3s(desiredVersion);\n\n        const localPromises: Promise<void>[] = [];\n\n        if (config.experimental?.containerEngine?.webAssembly?.enabled) {\n          localPromises.push(BackendHelper.configureRuntimeClasses(this.vm));\n          if (config.experimental?.kubernetes?.options?.spinkube) {\n            localPromises.push(BackendHelper.configureSpinOperator(this.vm));\n          }\n        }\n        await Promise.all(localPromises);\n      })());\n      await Promise.all(promises);\n    });\n\n    this.activeVersion = desiredVersion;\n  }\n\n  /**\n   * Start Kubernetes.\n   * @returns The Kubernetes endpoint\n   */\n  async start(config_: BackendSettings, kubernetesVersion: semver.SemVer, kubeClient?: () => KubeClient): Promise<void> {\n    const config = this.cfg = clone(config_);\n\n    // Remove flannel config if necessary, before starting k3s\n    if (!config.kubernetes.options.flannel) {\n      await this.vm.execCommand({ root: true }, 'rm', '-f', '/etc/cni/net.d/10-flannel.conflist');\n    }\n\n    await this.progressTracker.action('Starting k3s', 100, async() => {\n      // Run rc-update as we have dynamic dependencies.\n      await this.vm.execCommand({ root: true }, '/sbin/rc-update', '--update');\n      await this.vm.execCommand({ root: true }, '/sbin/rc-service', '--ifnotstarted', 'k3s', 'start');\n    });\n\n    const aborted = await this.progressTracker.action(\n      'Waiting for Kubernetes API',\n      100,\n      async() => {\n        await this.k3sHelper.waitForServerReady(() => Promise.resolve('127.0.0.1'), config.kubernetes.port);\n        while (true) {\n          if (this.vm.currentAction !== Action.STARTING) {\n            // User aborted\n            return true;\n          }\n          try {\n            await this.vm.execCommand({ expectFailure: true }, 'ls', '/etc/rancher/k3s/k3s.yaml');\n            break;\n          } catch (ex) {\n            console.log('Configuration /etc/rancher/k3s/k3s.yaml not present in lima vm; will check again...');\n            await util.promisify(setTimeout)(1_000);\n          }\n        }\n        console.debug('/etc/rancher/k3s/k3s.yaml is ready.');\n\n        return false;\n      },\n    );\n\n    if (aborted) {\n      return;\n    }\n    await this.progressTracker.action(\n      'Updating kubeconfig',\n      50,\n      this.k3sHelper.updateKubeconfig(\n        () => this.vm.execCommand({ capture: true, root: true }, 'cat', '/etc/rancher/k3s/k3s.yaml'),\n      ));\n\n    const client = this.client = kubeClient?.() || new KubeClient();\n\n    await this.progressTracker.action(\n      'Waiting for services',\n      50,\n      async() => {\n        await client.waitForServiceWatcher();\n        client.on('service-changed', (services) => {\n          this.emit('service-changed', services);\n        });\n        client.on('service-error', (service, errorMessage) => {\n          this.emit('service-error', service, errorMessage);\n        });\n      },\n    );\n\n    this.activeVersion = kubernetesVersion;\n    this.currentPort = config.kubernetes.port;\n    this.emit('current-port-changed', this.currentPort);\n\n    // Remove traefik if necessary.\n    if (!this.cfg?.kubernetes?.options.traefik) {\n      await this.progressTracker.action(\n        'Removing Traefik',\n        50,\n        this.k3sHelper.uninstallHelmChart(client, 'traefik'));\n    }\n    if (!this.cfg?.experimental?.kubernetes?.options?.spinkube) {\n      await this.progressTracker.action(\n        'Removing spinkube operator',\n        50,\n        Promise.all([\n          this.k3sHelper.uninstallHelmChart(client, MANIFEST_CERT_MANAGER),\n          this.k3sHelper.uninstallHelmChart(client, MANIFEST_SPIN_OPERATOR),\n        ]));\n    }\n\n    await this.progressTracker.action('Ensuring compatible kubectl', 50,\n      this.k3sHelper.getCompatibleKubectlVersion(this.activeVersion));\n    if (this.cfg?.kubernetes?.options.flannel) {\n      await this.progressTracker.action(\n        'Waiting for nodes',\n        100,\n        client.waitForReadyNodes());\n    } else {\n      await this.progressTracker.action(\n        'Skipping node checks, flannel is disabled',\n        100,\n        async() => {\n          await new Promise(resolve => setTimeout(resolve, 5000));\n        });\n    }\n  }\n\n  async stop() {\n    if (this.cfg?.kubernetes?.enabled) {\n      try {\n        const script = 'if [ -e /etc/init.d/k3s ]; then /sbin/rc-service --ifstarted k3s stop; fi';\n\n        await this.vm.execCommand({ root: true, expectFailure: true }, '/bin/sh', '-c', script);\n      } catch (ex) {\n        console.error('Failed to stop k3s while stopping kube backend: ', ex);\n      }\n    }\n    await this.cleanup();\n  }\n\n  cleanup(): Promise<void> {\n    this.client?.destroy();\n\n    return Promise.resolve();\n  }\n\n  async reset() {\n    await this.k3sHelper.deleteKubeState(this.vm);\n  }\n\n  cfg: BackendSettings | undefined;\n\n  protected readonly arch:  Architecture;\n  protected readonly vm:    LimaBackend;\n  protected activeVersion?: semver.SemVer;\n\n  /** The port Kubernetes is actively listening on. */\n  protected currentPort = 0;\n\n  /** Helper object to manage available K3s versions. */\n  readonly k3sHelper: K3sHelper;\n\n  protected client: KubeClient | null = null;\n\n  protected get progressTracker() {\n    return this.vm.progressTracker;\n  }\n\n  get version(): ShortVersion {\n    return this.activeVersion?.version ?? '';\n  }\n\n  get availableVersions(): Promise<SemanticVersionEntry[]> {\n    return this.k3sHelper.availableVersions;\n  }\n\n  async cachedVersionsOnly(): Promise<boolean> {\n    return await K3sHelper.cachedVersionsOnly();\n  }\n\n  get desiredPort() {\n    return this.cfg?.kubernetes?.port ?? 6443;\n  }\n\n  protected get desiredVersion(): Promise<semver.SemVer | undefined> {\n    return (async() => {\n      let availableVersions: SemanticVersionEntry[];\n      let available = true;\n\n      try {\n        availableVersions = await this.k3sHelper.availableVersions;\n\n        return await BackendHelper.getDesiredVersion(\n          this.cfg!,\n          availableVersions,\n          this.vm.noModalDialogs,\n          this.vm.writeSetting.bind(this.vm));\n      } catch (ex) {\n        // Locked field errors are fatal and will quit the application\n        if (ex instanceof LockedFieldError) {\n          throw ex;\n        }\n        console.error(`Could not get desired version: ${ ex }`);\n        available = false;\n\n        return undefined;\n      } finally {\n        mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available });\n      }\n    })();\n  }\n\n  /**\n   * Install K3s into the VM for execution.\n   * @param version The version to install.\n   */\n  protected async installK3s(version: semver.SemVer) {\n    const k3s = this.arch === 'aarch64' ? 'k3s-arm64' : 'k3s';\n\n    await this.vm.execCommand('mkdir', '-p', 'bin');\n    await this.vm.writeFile('bin/install-k3s', INSTALL_K3S_SCRIPT, 'a+x');\n    await fs.promises.chmod(path.join(paths.cache, 'k3s', version.raw, k3s), 0o755);\n    await this.vm.execCommand({ root: true }, 'bin/install-k3s', version.raw, path.join(paths.cache, 'k3s'));\n  }\n\n  /**\n   * Write the openrc script for k3s.\n   */\n  protected async writeServiceScript(cfg: BackendSettings, desiredVersion: semver.SemVer, allowSudo: boolean) {\n    const allPlatformsThresholdVersion = '1.31.0';\n    const config: Record<string, string> = {\n      PORT:            this.desiredPort.toString(),\n      ENGINE:          cfg.containerEngine.name ?? ContainerEngine.NONE,\n      ADDITIONAL_ARGS: `--node-ip ${ await this.vm.ipAddress }`,\n      LOG_DIR:         paths.logs,\n      USE_CRI_DOCKERD: BackendHelper.requiresCRIDockerd(cfg.containerEngine.name, desiredVersion.version).toString(),\n      ALLPLATFORMS:    semver.lt(desiredVersion, allPlatformsThresholdVersion) ? '--all-platforms' : '',\n    };\n\n    if (os.platform() === 'darwin') {\n      if (cfg.kubernetes.options.flannel) {\n        const { iface, addr } = await this.vm.getListeningInterface(allowSudo);\n\n        config.ADDITIONAL_ARGS += ` --flannel-iface ${ iface }`;\n        if (addr) {\n          config.ADDITIONAL_ARGS += ` --node-external-ip ${ addr }`;\n        }\n      } else {\n        console.log(`Disabling flannel and network policy`);\n        config.ADDITIONAL_ARGS += ' --flannel-backend=none --disable-network-policy';\n      }\n    }\n    if (!cfg.kubernetes.options.traefik) {\n      config.ADDITIONAL_ARGS += ' --disable traefik';\n    }\n    if (cfg.application.debug) {\n      config.ADDITIONAL_ARGS += ' --debug';\n    }\n    await this.vm.writeFile('/etc/init.d/cri-dockerd', SERVICE_CRI_DOCKERD_SCRIPT, 0o755);\n    await this.vm.writeConf('cri-dockerd', {\n      LOG_DIR: paths.logs,\n      ENGINE:  cfg.containerEngine.name ?? ContainerEngine.NONE,\n    });\n    await this.vm.writeFile('/etc/init.d/k3s', SERVICE_K3S_SCRIPT, 0o755);\n    await this.vm.writeConf('k3s', config);\n    await this.vm.writeFile('/etc/logrotate.d/k3s', LOGROTATE_K3S_SCRIPT);\n  }\n\n  async deleteIncompatibleData(desiredVersion: semver.SemVer) {\n    const existingVersion = await K3sHelper.getInstalledK3sVersion(this.vm);\n\n    if (!existingVersion) {\n      return;\n    }\n    if (semver.gt(existingVersion, desiredVersion)) {\n      await this.progressTracker.action(\n        'Deleting incompatible Kubernetes state',\n        100,\n        this.k3sHelper.deleteKubeState(this.vm));\n    }\n  }\n\n  async requiresRestartReasons(currentConfig: BackendSettings, desiredConfig: RecursivePartial<BackendSettings>, extra: ExtraRequiresReasons): Promise<RestartReasons> {\n    // This is a placeholder to force this method to be async\n    await Promise.all([]);\n\n    return this.k3sHelper.requiresRestartReasons(\n      currentConfig,\n      desiredConfig,\n      {\n        'application.adminAccess': undefined,\n      },\n      extra,\n    );\n  }\n\n  listServices(namespace?: string): K8s.ServiceEntry[] {\n    return this.client?.listServices(namespace) || [];\n  }\n\n  async forwardPort(namespace: string, service: string, k8sPort: number | string, hostPort: number): Promise<number | undefined> {\n    return await this.client?.forwardPort(namespace, service, k8sPort, hostPort);\n  }\n\n  async cancelForward(namespace: string, service: string, k8sPort: number | string): Promise<void> {\n    await this.client?.cancelForwardPort(namespace, service, k8sPort);\n  }\n\n  // #region Events\n  eventNames(): (keyof K8s.KubernetesBackendEvents)[] {\n    return super.eventNames() as (keyof K8s.KubernetesBackendEvents)[];\n  }\n\n  listeners<eventName extends keyof K8s.KubernetesBackendEvents>(\n    event: eventName,\n  ): K8s.KubernetesBackendEvents[eventName][] {\n    return super.listeners(event) as K8s.KubernetesBackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof K8s.KubernetesBackendEvents>(\n    event: eventName,\n  ): K8s.KubernetesBackendEvents[eventName][] {\n    return super.rawListeners(event) as K8s.KubernetesBackendEvents[eventName][];\n  }\n  // #endregion\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/kube/wsl.ts",
    "content": "import events from 'events';\nimport path from 'path';\nimport timers from 'timers';\nimport util from 'util';\n\nimport semver from 'semver';\n\nimport { KubeClient } from './client';\nimport K3sHelper, { ExtraRequiresReasons, NoCachedK3sVersionsError, ShortVersion } from '../k3sHelper';\nimport WSLBackend, { Action } from '../wsl';\n\nimport INSTALL_K3S_SCRIPT from '@pkg/assets/scripts/install-k3s';\nimport { BackendSettings, RestartReasons } from '@pkg/backend/backend';\nimport BackendHelper, { MANIFEST_CERT_MANAGER, MANIFEST_SPIN_OPERATOR } from '@pkg/backend/backendHelper';\nimport * as K8s from '@pkg/backend/k8s';\nimport { LockedFieldError } from '@pkg/config/commandLineOptions';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { checkConnectivity } from '@pkg/main/networking';\nimport { SemanticVersionEntry } from '@pkg/utils/kubeVersions';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\nimport { showMessageBox } from '@pkg/window';\n\nconst console = Logging.kube;\n\nexport default class WSLKubernetesBackend extends events.EventEmitter implements K8s.KubernetesBackend {\n  constructor(vm: WSLBackend) {\n    super();\n    this.vm = vm;\n\n    this.k3sHelper.on('versions-updated', () => this.emit('versions-updated'));\n    this.k3sHelper.initialize().catch((err) => {\n      console.log('k3sHelper.initialize failed: ', err);\n      // If we fail to initialize, we still need to continue (with no versions).\n      this.emit('versions-updated');\n    });\n    mainEvents.on('network-ready', () => this.k3sHelper.networkReady());\n  }\n\n  protected cfg:    BackendSettings | undefined;\n  protected vm:     WSLBackend;\n  /** Helper object to manage available K3s versions. */\n  readonly k3sHelper = new K3sHelper('x86_64');\n  protected client: KubeClient | null = null;\n\n  /** The version of Kubernetes currently running. */\n  protected activeVersion: semver.SemVer | undefined;\n\n  /** The port the Kubernetes server is listening on (default 6443) */\n  protected currentPort = 0;\n\n  get progressTracker() {\n    return this.vm.progressTracker;\n  }\n\n  protected get downloadURL() {\n    return 'https://github.com/k3s-io/k3s/releases/download';\n  }\n\n  get version(): ShortVersion {\n    return this.activeVersion?.version ?? '';\n  }\n\n  get port(): number {\n    return this.currentPort;\n  }\n\n  get availableVersions(): Promise<SemanticVersionEntry[]> {\n    return this.k3sHelper.availableVersions;\n  }\n\n  async cachedVersionsOnly(): Promise<boolean> {\n    return await K3sHelper.cachedVersionsOnly();\n  }\n\n  protected get desiredVersion(): Promise<semver.SemVer | undefined> {\n    return (async() => {\n      let availableVersions: SemanticVersionEntry[];\n      let available = true;\n\n      try {\n        availableVersions = await this.k3sHelper.availableVersions;\n\n        return await BackendHelper.getDesiredVersion(\n          this.cfg!,\n          availableVersions,\n          this.vm.noModalDialogs,\n          this.vm.writeSetting.bind(this.vm));\n      } catch (ex) {\n        // Locked field errors are fatal and will quit the application\n        if (ex instanceof LockedFieldError) {\n          throw ex;\n        }\n        console.error(`Could not get desired version: ${ ex }`);\n        available = false;\n\n        return undefined;\n      } finally {\n        mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available });\n      }\n    })();\n  }\n\n  async deleteIncompatibleData(desiredVersion: semver.SemVer) {\n    const existingVersion = await K3sHelper.getInstalledK3sVersion(this.vm);\n\n    if (!existingVersion) {\n      return;\n    }\n    if (semver.gt(existingVersion, desiredVersion)) {\n      console.log(`Deleting incompatible Kubernetes state due to downgrade from ${ existingVersion } to ${ desiredVersion }...`);\n      await this.vm.progressTracker.action(\n        'Deleting incompatible Kubernetes state',\n        100,\n        this.k3sHelper.deleteKubeState(this.vm));\n    }\n  }\n\n  get desiredPort() {\n    return this.cfg?.kubernetes?.port ?? 6443;\n  }\n\n  /**\n   * Download K3s images.  This will also calculate the version to download.\n   * @returns The version of K3s images downloaded, and whether this is a\n   * downgrade.\n   */\n  async download(cfg: BackendSettings): Promise<[semver.SemVer | undefined, boolean]> {\n    this.cfg = cfg;\n    const interval = timers.setInterval(() => {\n      const statuses = [\n        this.k3sHelper.progress.checksum,\n        this.k3sHelper.progress.exe,\n        this.k3sHelper.progress.images,\n      ];\n      const sum = (key: 'current' | 'max') => {\n        return statuses.reduce((v, c) => v + c[key], 0);\n      };\n\n      const current = sum('current');\n      const max = sum('max');\n\n      this.progressTracker.numeric('Downloading Kubernetes components', current, max);\n    });\n\n    try {\n      const desiredVersion = await this.desiredVersion;\n\n      if (desiredVersion === undefined) {\n        return [undefined, false];\n      }\n\n      try {\n        await this.progressTracker.action('Checking k3s images', 100, this.k3sHelper.ensureK3sImages(desiredVersion));\n\n        return [desiredVersion, false];\n      } catch (ex) {\n        if (!await checkConnectivity('github.com')) {\n          throw ex;\n        }\n\n        try {\n          const newVersion = await K3sHelper.selectClosestImage(desiredVersion);\n          const isDowngrade = semver.lt(newVersion, desiredVersion);\n\n          if (isDowngrade) {\n            const options: Electron.MessageBoxOptions = {\n              message:   `Downgrading from ${ desiredVersion.raw } to ${ newVersion.raw } will lose existing Kubernetes workloads. Delete the data?`,\n              type:      'question',\n              buttons:   ['Delete Workloads', 'Cancel'],\n              defaultId: 1,\n              title:     'Confirming migration',\n              cancelId:  1,\n            };\n            const result = await showMessageBox(options, true);\n\n            if (result.response !== 0) {\n              return [undefined, true];\n            }\n          }\n          console.log(`Going with alternative version ${ newVersion.raw }`);\n\n          return [newVersion, isDowngrade];\n        } catch (ex: any) {\n          if (ex instanceof NoCachedK3sVersionsError) {\n            throw new K8s.KubernetesError('No version available', 'The k3s cache is empty and there is no network connection.');\n          }\n          throw ex;\n        }\n      }\n    } finally {\n      timers.clearInterval(interval);\n    }\n  }\n\n  async install(config: BackendSettings, version: semver.SemVer, allowSudo: boolean) {\n    await this.vm.runInstallScript(INSTALL_K3S_SCRIPT,\n      'install-k3s', version.raw, await this.vm.wslify(path.join(paths.cache, 'k3s')));\n\n    if (config.experimental?.containerEngine?.webAssembly?.enabled) {\n      const promises: Promise<void>[] = [];\n\n      promises.push(BackendHelper.configureRuntimeClasses(this.vm));\n      if (config.experimental?.kubernetes?.options?.spinkube) {\n        promises.push(BackendHelper.configureSpinOperator(this.vm));\n      }\n      await Promise.all(promises);\n    }\n  }\n\n  async start(config: BackendSettings, activeVersion: semver.SemVer, kubeClient?: () => KubeClient): Promise<void> {\n    if (!config) {\n      throw new Error('no config!');\n    }\n    this.cfg = config;\n\n    // Clean up kubernetes cgroups before we start, as Kubernetes 1.31.0+ fails\n    // to start if these are left over.  We need to remove all cgroups named\n    // \"kubepods\" as well as their descendants (which are expected to all be\n    // empty).\n    await this.progressTracker.action('Removing stale state', 50,\n      this.vm.execCommand('busybox', 'find', '/sys/fs/cgroup', '-name', 'kubepods', '-exec',\n        'busybox', 'find', '{}', '-type', 'd', '-delete', ';', '-prune'));\n\n    const executable = config.containerEngine.name === ContainerEngine.MOBY ? 'docker' : 'nerdctl';\n\n    await this.vm.verifyReady(executable, 'images');\n\n    // Remove flannel config if necessary, before starting k3s\n    if (!config.kubernetes.options.flannel) {\n      await this.vm.execCommand('busybox', 'rm', '-f', '/etc/cni/net.d/10-flannel.conflist');\n    }\n    await this.progressTracker.action('Starting k3s', 100, this.vm.startService('k3s'));\n\n    if (this.vm.currentAction !== Action.STARTING) {\n      // User aborted\n      return;\n    }\n\n    await this.progressTracker.action(\n      'Updating kubeconfig',\n      100,\n      async() => {\n        // Wait for the file to exist first, for slow machines.\n        const command = 'if test -r /etc/rancher/k3s/k3s.yaml; then echo yes; else echo no; fi';\n\n        while (true) {\n          const result = await this.vm.execCommand({ capture: true }, '/bin/sh', '-c', command);\n\n          if (result.includes('yes')) {\n            break;\n          }\n          await util.promisify(timers.setTimeout)(1_000);\n        }\n\n        await this.k3sHelper.updateKubeconfig(\n          async() => await this.vm.execCommand({ capture: true }, await this.vm.getWSLHelperPath(), 'k3s', 'kubeconfig'));\n      });\n\n    await this.progressTracker.action(\n      'Waiting for Kubernetes API',\n      100,\n      this.k3sHelper.waitForServerReady(() => this.vm.ipAddress, config.kubernetes?.port));\n\n    const client = this.client = kubeClient?.() || new KubeClient();\n\n    await this.progressTracker.action(\n      'Waiting for services',\n      50,\n      async() => {\n        await client.waitForServiceWatcher();\n        client.on('service-changed', (services) => {\n          this.emit('service-changed', services);\n        });\n        client.on('service-error', (service, errorMessage) => {\n          this.emit('service-error', service, errorMessage);\n        });\n      });\n\n    this.activeVersion = activeVersion;\n    this.currentPort = config.kubernetes.port;\n    this.emit('current-port-changed', this.currentPort);\n\n    const tasks: Promise<unknown>[] = [\n      this.k3sHelper.getCompatibleKubectlVersion(this.activeVersion),\n    ];\n\n    // Remove traefik if necessary.\n    if (!config.kubernetes.options.traefik) {\n      tasks.push(this.progressTracker.action(\n        'Removing Traefik',\n        50,\n        this.k3sHelper.uninstallHelmChart(client, 'traefik')));\n    }\n    if (!this.cfg?.experimental?.kubernetes?.options?.spinkube) {\n      tasks.push(this.progressTracker.action(\n        'Removing spinkube operator',\n        50,\n        Promise.all([\n          this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_CERT_MANAGER),\n          this.k3sHelper.uninstallHelmChart(this.client, MANIFEST_SPIN_OPERATOR),\n        ])));\n    }\n\n    if (config.kubernetes.options.flannel) {\n      tasks.push(this.progressTracker.action(\n        'Waiting for nodes',\n        100,\n        client.waitForReadyNodes()));\n    }\n\n    await Promise.all(tasks);\n  }\n\n  async stop() {\n    await this.cleanup();\n    // No need to actually stop the service; the whole distro will shut down.\n  }\n\n  cleanup() {\n    this.client?.destroy();\n\n    return Promise.resolve();\n  }\n\n  async reset() {\n    await this.k3sHelper.deleteKubeState(this.vm);\n  }\n\n  requiresRestartReasons(oldConfig: BackendSettings, newConfig: RecursivePartial<BackendSettings>, extras: ExtraRequiresReasons = {}): Promise<RestartReasons> {\n    return Promise.resolve(this.k3sHelper.requiresRestartReasons(\n      oldConfig,\n      newConfig,\n      {\n        'kubernetes.ingress.localhostOnly': undefined,\n        'WSL.integrations':                 undefined,\n      },\n      extras,\n    ));\n  }\n\n  listServices(namespace?: string): K8s.ServiceEntry[] {\n    return this.client?.listServices(namespace) || [];\n  }\n\n  async forwardPort(namespace: string, service: string, k8sPort: number | string, hostPort: number): Promise<number | undefined> {\n    return await this.client?.forwardPort(namespace, service, k8sPort, hostPort);\n  }\n\n  async cancelForward(namespace: string, service: string, k8sPort: number | string): Promise<void> {\n    await this.client?.cancelForwardPort(namespace, service, k8sPort);\n  }\n\n  // #region Events\n  eventNames(): (keyof K8s.KubernetesBackendEvents)[] {\n    return super.eventNames() as (keyof K8s.KubernetesBackendEvents)[];\n  }\n\n  listeners<eventName extends keyof K8s.KubernetesBackendEvents>(\n    event: eventName,\n  ): K8s.KubernetesBackendEvents[eventName][] {\n    return super.listeners(event) as K8s.KubernetesBackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof K8s.KubernetesBackendEvents>(\n    event: eventName,\n  ): K8s.KubernetesBackendEvents[eventName][] {\n    return super.rawListeners(event) as K8s.KubernetesBackendEvents[eventName][];\n  }\n  // #endregion\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/kubeconfig.ts",
    "content": "// kubernetes-client/javascript doesn't have support for the `proxy-url` cluster field.\n// We are providing variants of loadFromString() and exportConfig() that do.\n\nimport childProcess, { spawn } from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { findHomeDir, KubeConfig } from '@kubernetes/client-node';\nimport {\n  ActionOnInvalid,\n  ConfigOptions,\n  exportContext,\n  exportUser,\n  newContexts,\n  newUsers,\n} from '@kubernetes/client-node/dist/config_types';\nimport _ from 'lodash';\nimport yaml from 'yaml';\n\nimport { executable } from '@pkg/utils/resources';\n\ninterface Cluster {\n  readonly name:           string;\n  readonly caData?:        string;\n  caFile?:                 string;\n  readonly server:         string;\n  readonly skipTLSVerify:  boolean;\n  readonly tlsServerName?: string;\n  readonly proxyUrl?:      string;\n}\n\nexport function loadFromString(kubeConfig : KubeConfig, config: string, opts?: Partial<ConfigOptions>): void {\n  const obj = yaml.parse(config);\n\n  kubeConfig.clusters = newClusters(obj.clusters, opts);\n  kubeConfig.contexts = newContexts(obj.contexts, opts);\n  kubeConfig.users = newUsers(obj.users, opts);\n  kubeConfig.currentContext = obj['current-context'];\n}\n\nfunction newClusters(a: any, opts?: Partial<ConfigOptions>): Cluster[] {\n  const options = Object.assign({ onInvalidEntry: ActionOnInvalid.THROW }, opts || {});\n\n  return _.compact(_.map(a, clusterIterator(options.onInvalidEntry)));\n}\n\nfunction exportCluster(cluster: Cluster): any {\n  return {\n    name:    cluster.name,\n    cluster: {\n      server:                       cluster.server,\n      'certificate-authority-data': cluster.caData,\n      'certificate-authority':      cluster.caFile,\n      'insecure-skip-tls-verify':   cluster.skipTLSVerify,\n      'proxy-url':                  cluster.proxyUrl,\n      'tls-server-name':            cluster.tlsServerName,\n    },\n  };\n}\n\nfunction clusterIterator(onInvalidEntry: ActionOnInvalid): _.ListIterator<any, Cluster | null> {\n  return (elt: any, i: number, list: _.List<any>): Cluster | null => {\n    try {\n      if (!elt.name) {\n        throw new Error(`clusters[${ i }].name is missing`);\n      }\n      if (!elt.cluster) {\n        throw new Error(`clusters[${ i }].cluster is missing`);\n      }\n      if (!elt.cluster.server) {\n        throw new Error(`clusters[${ i }].cluster.server is missing`);\n      }\n\n      return {\n        caData:        elt.cluster['certificate-authority-data'],\n        caFile:        elt.cluster['certificate-authority'],\n        name:          elt.name,\n        proxyUrl:      elt.cluster['proxy-url'],\n        server:        elt.cluster.server.replace(/\\/$/, ''),\n        skipTLSVerify: elt.cluster['insecure-skip-tls-verify'] === true,\n        tlsServerName: elt.cluster['tls-server-name'],\n      };\n    } catch (err) {\n      switch (onInvalidEntry) {\n      case ActionOnInvalid.FILTER:\n        return null;\n      case ActionOnInvalid.THROW:\n      default:\n        throw err;\n      }\n    }\n  };\n}\n\nexport function exportConfig(config : KubeConfig): string {\n  const configObj = {\n    apiVersion:        'v1',\n    kind:              'Config',\n    clusters:          config.clusters.map(exportCluster),\n    users:             config.users.map(exportUser),\n    contexts:          config.contexts.map(exportContext),\n    preferences:       {},\n    'current-context': config.getCurrentContext(),\n  };\n\n  return JSON.stringify(configObj);\n}\n\n/**\n * Get the paths to the kubernetes client config path.\n * This is mainly useful for watching configuration changes.\n */\nexport async function getKubeConfigPaths(): Promise<string[]> {\n  async function hasAccess(filePath: string): Promise<boolean> {\n    try {\n      await fs.promises.access(filePath, fs.constants.R_OK);\n    } catch {\n      return false;\n    }\n\n    return true;\n  }\n\n  if (process.env.KUBECONFIG && process.env.KUBECONFIG.length > 0) {\n    const results: string[] = [];\n\n    for (const filePath of process.env.KUBECONFIG.split(path.delimiter)) {\n      if (await hasAccess(filePath)) {\n        results.push(filePath);\n      }\n    }\n\n    return results;\n  }\n\n  // We do not support locating kubeconfig files inside WSL distros, nor\n  // in-cluster configs, so we only need to check the one path.\n  return [path.join(findHomeDir() ?? os.homedir(), '.kube', 'config')];\n}\n\n// The K8s JS library will get the current context but does not have the ability\n// to save the context. The current version of the package targets k8s 1.18 and\n// there are new config file features (e.g., proxy) that may be lost by outputting\n// the config with the library. So, we drop down to kubectl for this.\nexport function setCurrentContext(ctx: string, exitfunc: (code: number | null, signal: NodeJS.Signals | null) => void) {\n  const opts: childProcess.SpawnOptions = {};\n\n  opts.env = { ...process.env };\n\n  const bat = spawn(executable('kubectl'), ['config', 'use-context', ctx], opts);\n\n  // TODO: For data toggle this based on a debug mode\n  bat.stdout?.on('data', (data) => {\n    console.log(data.toString());\n  });\n\n  bat.stderr?.on('data', (data) => {\n    console.error(data.toString());\n  });\n\n  bat.on('exit', exitfunc);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/lima.ts",
    "content": "// Kubernetes backend for macOS, based on Lima.\n\nimport { ChildProcess, spawn as spawnWithSignal } from 'child_process';\nimport crypto from 'crypto';\nimport events from 'events';\nimport fs from 'fs';\nimport net from 'net';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\nimport util from 'util';\n\nimport Electron from 'electron';\nimport merge from 'lodash/merge';\nimport omit from 'lodash/omit';\nimport semver from 'semver';\nimport tar from 'tar-stream';\nimport yaml from 'yaml';\n\nimport {\n  Architecture,\n  BackendError,\n  BackendEvents,\n  BackendProgress,\n  BackendSettings,\n  execOptions,\n  FailureDetails,\n  RestartReasons,\n  State,\n  VMBackend,\n  VMExecutor,\n} from './backend';\nimport BackendHelper from './backendHelper';\nimport { ContainerEngineClient, MobyClient, NerdctlClient } from './containerClient';\nimport * as K8s from './k8s';\nimport ProgressTracker, { getProgressErrorDescription } from './progressTracker';\n\nimport DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml';\nimport DEFAULT_CONFIG from '@pkg/assets/lima-config.yaml';\nimport NETWORKS_CONFIG from '@pkg/assets/networks-config.yaml';\nimport FLANNEL_CONFLIST from '@pkg/assets/scripts/10-flannel.conflist';\nimport SERVICE_BUILDKITD_CONF from '@pkg/assets/scripts/buildkit.confd';\nimport SERVICE_BUILDKITD_INIT from '@pkg/assets/scripts/buildkit.initd';\nimport DOCKER_CREDENTIAL_SCRIPT from '@pkg/assets/scripts/docker-credential-rancher-desktop';\nimport LOGROTATE_LIMA_GUESTAGENT_SCRIPT from '@pkg/assets/scripts/logrotate-lima-guestagent';\nimport LOGROTATE_OPENRESTY_SCRIPT from '@pkg/assets/scripts/logrotate-openresty';\nimport NERDCTL from '@pkg/assets/scripts/nerdctl';\nimport NGINX_CONF from '@pkg/assets/scripts/nginx.conf';\nimport { ContainerEngine, MountType, VMType } from '@pkg/config/settings';\nimport { getServerCredentialsPath, ServerState } from '@pkg/main/credentialServer/httpCredentialHelperServer';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { exec as sudo } from '@pkg/sudo-prompt';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport clone from '@pkg/utils/clone';\nimport dockerDirManager from '@pkg/utils/dockerDirManager';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { defined, RecursivePartial } from '@pkg/utils/typeUtils';\nimport { openSudoPrompt } from '@pkg/window';\n\n/* eslint @typescript-eslint/switch-exhaustiveness-check: \"error\" */\n\n/**\n * Enumeration for tracking what operation the backend is undergoing.\n */\nexport enum Action {\n  NONE = 'idle',\n  STARTING = 'starting',\n  STOPPING = 'stopping',\n}\n\n/**\n * Symbolic names for various SLIRP IP addresses.\n */\nenum SLIRP {\n  HOST_GATEWAY = '192.168.5.2',\n  DNS = '192.168.5.3',\n  GUEST_IP_ADDRESS = '192.168.5.15',\n}\n\n/**\n * Lima mount\n */\nexport interface LimaMount {\n  location:  string;\n  writable?: boolean;\n  '9p'?: {\n    securityModel:   string;\n    protocolVersion: string;\n    msize:           string;\n    cache:           string;\n  }\n}\n\n/**\n * Lima configuration\n */\nexport interface LimaConfiguration {\n  vmType?:  'qemu' | 'vz';\n  rosetta?: {\n    enabled?: boolean;\n    binfmt?:  boolean;\n  },\n  arch?:  'x86_64' | 'aarch64';\n  images: {\n    location: string;\n    arch?:    'x86_64' | 'aarch64';\n    digest?:  string;\n  }[];\n  cpus?:     number;\n  memory?:   number;\n  disk?:     string;\n  mounts?:   LimaMount[];\n  mountType: 'reverse-sshfs' | '9p' | 'virtiofs';\n  ssh: {\n    localPort:          number;\n    loadDotSSHPubKeys?: boolean;\n  }\n  firmware?: {\n    legacyBIOS?: boolean;\n  }\n  video?: {\n    display?: string;\n  }\n  provision?: {\n    mode:   'system' | 'user';\n    script: string;\n  }[]\n  containerd?: {\n    system?: boolean;\n    user?:   boolean;\n  }\n  probes?: {\n    mode:        'readiness';\n    description: string;\n    script:      string;\n    hint:        string;\n  }[];\n  hostResolver?: {\n    hosts?: Record<string, string>;\n  }\n  portForwards?: Record<string, any>[];\n  networks?:     Record<string, string | boolean>[];\n  env?:          Record<string, string>;\n}\n\n/**\n * QEMU Image formats\n */\nenum ImageFormat {\n  QCOW2 = 'qcow2',\n  RAW = 'raw',\n}\n\n/**\n * QEMU Image Information as returned by `qemu-img info --output=json ...`\n */\ninterface QEMUImageInfo {\n  format: string;\n}\n\n/**\n * Options passed to spawnWithCapture method\n */\ninterface SpawnOptions {\n  expectFailure?: boolean,\n  stderr?:        boolean,\n  env?:           NodeJS.ProcessEnv,\n}\n\n/**\n * Lima networking configuration.\n * @see https://github.com/lima-vm/lima/blob/v0.8.0/pkg/networks/networks.go\n */\ninterface LimaNetworkConfiguration {\n  paths: {\n    socketVMNet: string;\n    varRun:      string;\n    sudoers?:    string;\n  }\n  group?:   string;\n  networks: Record<string, {\n    mode:    'host' | 'shared';\n    gateway: string;\n    dhcpEnd: string;\n    netmask: string;\n  } | {\n    mode:      'bridged';\n    interface: string;\n  }>;\n}\n\n/**\n * One entry from `limactl list --json`\n */\ninterface LimaListResult {\n  name:          string;\n  status:        'Broken' | 'Stopped' | 'Running';\n  dir:           string;\n  arch:          'x86_64' | 'aarch64';\n  vmType?:       'qemu' | 'vz';\n  sshLocalPort?: number;\n  hostAgentPID?: number;\n  qemuPID?:      number;\n  errors?:       string[];\n}\n\n/** SPNetworkDataType is output from /usr/sbin/system_profiler on darwin. */\ninterface SPNetworkDataType {\n  _name:     string;\n  interface: string;\n  dhcp?:     unknown;\n  IPv4?: {\n    Addresses?: string[];\n  };\n}\n\ntype SudoReason = 'networking' | 'docker-socket';\n\n/**\n * SudoCommand describes an operation that will be run under sudo.  This is\n * returned from various methods that need to determine what commands we need to\n * run under sudo to have all functionality.\n */\ninterface SudoCommand {\n  /** Reason why we want sudo access, */\n  reason:   SudoReason;\n  /** Commands that will need to be executed. */\n  commands: string[];\n  /** Paths that will be affected by this command. */\n  paths:    string[];\n}\n\nconst console = Logging.lima;\nconst DEFAULT_DOCKER_SOCK_LOCATION = '/var/run/docker.sock';\n\nexport const MACHINE_NAME = '0';\nconst IMAGE_VERSION = DEPENDENCY_VERSIONS.alpineLimaISO.isoVersion;\nconst ALPINE_EDITION = 'rd';\nconst ALPINE_VERSION = DEPENDENCY_VERSIONS.alpineLimaISO.alpineVersion;\n\nconst ETC_RANCHER_DESKTOP_DIR = '/etc/rancher/desktop';\nconst CREDENTIAL_FORWARDER_SETTINGS_PATH = path.join(ETC_RANCHER_DESKTOP_DIR, 'credfwd');\nconst DOCKER_CREDENTIAL_PATH = '/usr/local/bin/docker-credential-rancher-desktop';\nconst ROOT_DOCKER_CONFIG_DIR = '/root/.docker';\nconst ROOT_DOCKER_CONFIG_PATH = path.join(ROOT_DOCKER_CONFIG_DIR, 'config.json');\n\n/** The following files, and their parents up to /, must only be writable by root,\n *  and none of them are allowed to be symlinks (lima-vm requirements).\n */\nconst VMNET_DIR = '/opt/rancher-desktop';\n\n// Make this file the last one to be loaded by `sudoers` so others don't override needed settings.\n// Details at https://github.com/rancher-sandbox/rancher-desktop/issues/1444\n// This path introduced in version 1.0.1\nconst LIMA_SUDOERS_LOCATION = '/private/etc/sudoers.d/zzzzz-rancher-desktop-lima';\n// Filename used in versions 1.0.0 and earlier:\nconst PREVIOUS_LIMA_SUDOERS_LOCATION = '/private/etc/sudoers.d/rancher-desktop-lima';\n\n/**\n * LimaBackend implements all the Lima-specific functionality for Rancher\n * Desktop.  This is used on macOS and Linux.\n */\n// Implementation note: some of the methods of this class do not need to modify\n// the instance; these have an explicit this parameter [1] to narrow their view\n// of the class instance.  Typically, they use Readonly<LimaBackend> to prevent\n// writing to the instance; however, as that drops all non-public fields [2] we\n// sometimes have to use Readonly<LimaBackend> & LimaBackend to pick them up\n// (though this loses the type guarantees around it not modifying the instance).\n// [1]: https://www.typescriptlang.org/docs/handbook/2/classes.html#this-parameters\n// [2]: https://github.com/microsoft/TypeScript/issues/46802\nexport default class LimaBackend extends events.EventEmitter implements VMBackend, VMExecutor {\n  constructor(arch: Architecture, kubeFactory: (backend: LimaBackend) => K8s.KubernetesBackend) {\n    super();\n    this.arch = arch;\n    this.kubeBackend = kubeFactory(this);\n\n    this.progressTracker = new ProgressTracker((progress) => {\n      this.progress = progress;\n      this.emit('progress');\n    }, console);\n\n    if (!(process.env.RD_TEST ?? '').includes('e2e')) {\n      process.on('exit', async() => {\n        // Attempt to shut down any stray qemu processes.\n        await this.lima('stop', '--force', MACHINE_NAME);\n      });\n    }\n  }\n\n  readonly kubeBackend:   K8s.KubernetesBackend;\n  readonly executor = this;\n  #containerEngineClient: ContainerEngineClient | undefined;\n\n  get containerEngineClient() {\n    if (this.#containerEngineClient) {\n      return this.#containerEngineClient;\n    }\n\n    throw new Error('Invalid state, no container engine client available.');\n  }\n\n  protected readonly CONFIG_PATH = path.join(paths.lima, '_config', `${ MACHINE_NAME }.yaml`);\n\n  /** The current config state. */\n  protected cfg: BackendSettings | undefined;\n\n  /** The current architecture. */\n  protected readonly arch: Architecture;\n\n  /** The version of Kubernetes currently running. */\n  protected activeVersion: semver.SemVer | null = null;\n\n  /** Whether we can prompt the user for administrative access - this setting persists in the config. */\n  #adminAccess = true;\n\n  /** A transient property that prevents prompting via modal UI elements. */\n  #noModalDialogs = false;\n\n  get noModalDialogs() {\n    return this.#noModalDialogs;\n  }\n\n  set noModalDialogs(value: boolean) {\n    this.#noModalDialogs = value;\n  }\n\n  /** Helper object to manage progress notifications. */\n  progressTracker;\n\n  /**\n   * The current operation underway; used to avoid responding to state changes\n   * when we're in the process of doing a different one.\n   */\n  currentAction: Action = Action.NONE;\n\n  writeSetting(changed: RecursivePartial<BackendSettings>) {\n    if (changed) {\n      mainEvents.emit('settings-write', changed);\n    }\n    this.cfg = merge({}, this.cfg, changed);\n  }\n\n  protected internalState: State = State.STOPPED;\n  get state() {\n    return this.internalState;\n  }\n\n  protected async setState(state: State) {\n    this.internalState = state;\n    this.emit('state-changed', this.state);\n    switch (this.state) {\n    case State.STOPPING:\n    case State.STOPPED:\n    case State.ERROR:\n    case State.DISABLED:\n      await this.kubeBackend.cleanup();\n      break;\n    case State.STARTING:\n    case State.STARTED:\n      /* nothing */\n    }\n  }\n\n  progress: BackendProgress = { current: 0, max: 0 };\n\n  debug = false;\n\n  emit: VMBackend['emit'] = events.EventEmitter.prototype.emit;\n\n  get backend(): 'lima' {\n    return 'lima';\n  }\n\n  get cpus(): Promise<number> {\n    return (async() => {\n      return (await this.getLimaConfig())?.cpus || 0;\n    })();\n  }\n\n  get memory(): Promise<number> {\n    return (async() => {\n      return Math.round(((await this.getLimaConfig())?.memory || 0) / 1024 / 1024 / 1024);\n    })();\n  }\n\n  protected ensureArchitectureMatch() {\n    if (os.platform().startsWith('darwin')) {\n      // Since we now use native Electron, the only case this might be an issue\n      // is the user is running under Rosetta. Flag that.\n      if (Electron.app.runningUnderARM64Translation) {\n        // Using 'aarch64' and 'x86_64' in the error because that's what we use\n        // for the DMG suffix, e.g. \"Rancher Desktop.aarch64.dmg\"\n        throw new BackendError('Fatal Error', `Rancher Desktop for x86_64 does not work on aarch64.`, true);\n      }\n    }\n  }\n\n  protected async ensureVirtualizationSupported() {\n    if (os.platform().startsWith('linux')) {\n      const cpuInfo = await fs.promises.readFile('/proc/cpuinfo', 'utf-8');\n\n      if (this.arch === 'x86_64') {\n        if (!/flags.*(vmx|svm)/g.test(cpuInfo)) {\n          console.log(`Virtualization support error: got ${ cpuInfo }`);\n          throw new Error('Virtualization does not appear to be supported on your machine.');\n        }\n      }\n    } else if (os.platform().startsWith('darwin')) {\n      const { stdout } = await childProcess.spawnFile(\n        'sysctl', ['kern.hv_support'],\n        { stdio: ['inherit', 'pipe', console] });\n\n      if (!/:\\s*1$/.test(stdout.trim())) {\n        console.log(`Virtualization support error: got ${ stdout.trim() }`);\n        throw new Error('Virtualization does not appear to be supported on your machine.');\n      }\n    }\n  }\n\n  /**\n   * Get the IPv4 address of the VM. This address should be routable from within the VM itself.\n   * In Lima the SLIRP guest IP address is hard-coded.\n   */\n  get ipAddress(): Promise<string | undefined> {\n    return Promise.resolve(SLIRP.GUEST_IP_ADDRESS);\n  }\n\n  getBackendInvalidReason(): Promise<BackendError | null> {\n    return Promise.resolve(null);\n  }\n\n  /**\n   * Check if the base (alpine) disk image is out of date; if yes, update it\n   * without removing existing data.  This is only ever called from updateConfig\n   * to ensure that the passed-in lima configuration is the one before we\n   * overwrote it.\n   *\n   * This will stop the VM if necessary.\n   */\n  protected async updateBaseDisk(currentConfig: LimaConfiguration) {\n    // Lima does not have natively have any support for this; we'll need to\n    // reach into the configuration and:\n    // 1) Figure out what the old base disk version is.\n    // 2) Confirm that it's out of date.\n    // 3) Change out the base disk as necessary.\n    // Unfortunately, we don't have a version string anywhere _in_ the image, so\n    // we will have to rely on the path in lima.yml instead.\n\n    const images = currentConfig.images.map(i => path.basename(i.location));\n    // We had a typo in the name of the image; it was \"alpline\" instead of \"alpine\".\n    // Image version may have a '+rd1' (or '.rd1') suffix after the upstream semver version.\n    const versionMatch = images.map(i => /^alpl?ine-lima-v([0-9.]+)(?:[+.]rd(\\d+))?-/.exec(i)).find(defined);\n    const existingVersion = semver.coerce(versionMatch?.[1]);\n    const existingRDVersion = versionMatch?.[2];\n\n    if (!existingVersion) {\n      console.log(`Could not find base image version from ${ images }; skipping update of base images.`);\n\n      return;\n    }\n\n    let versionComparison = semver.coerce(IMAGE_VERSION)?.compare(existingVersion);\n\n    // Compare RD patch versions if upstream semver are matching\n    if (versionComparison === 0) {\n      const rdVersionMatch = IMAGE_VERSION.match(/[+.]rd(\\d+)/);\n\n      if (rdVersionMatch) {\n        if (existingRDVersion) {\n          if (parseInt(existingRDVersion) < parseInt(rdVersionMatch[1])) {\n            versionComparison = 1;\n          }\n        } else {\n          // If the new image has an RD patch version, but the old one doesn't, then the new version is newer.\n          versionComparison = 1;\n        }\n      } else if (existingRDVersion) {\n        // If the old image has an RD patch version, but the new one doesn't, then the new version is older.\n        versionComparison = -1;\n      }\n    }\n\n    switch (versionComparison) {\n    case undefined:\n      // Could not parse desired image version\n      console.log(`Error parsing desired image version ${ IMAGE_VERSION }`);\n\n      return;\n    case -1: {\n      // existing version is newer\n      const message = `\n          This Rancher Desktop installation appears to be older than the version\n          that created your existing Kubernetes cluster.  Please either update\n          Rancher Desktop or reset Kubernetes and container images.`;\n\n      console.log(`Base disk is ${ existingVersion }, newer than ${ IMAGE_VERSION } - aborting.`);\n      throw new BackendError('Rancher Desktop Update Required', message.replace(/\\s+/g, ' ').trim());\n    }\n    case 0:\n      // The image is the same version as what we have\n      return;\n    case 1:\n      // Need to update the image.\n      break;\n    default: {\n      // Should never reach this.\n      const message = `\n        There was an error determining if your existing Rancher Desktop cluster\n        needs to be updated.  Please reset Kubernetes and container images, or\n        file an issue with your Rancher Desktop logs attached.`;\n\n      console.log(`Invalid valid comparing ${ existingVersion } to desired ${ IMAGE_VERSION }: ${ JSON.stringify(versionComparison) }`);\n\n      throw new BackendError('Fatal Error', message.replace(/\\s+/g, ' ').trim());\n    }\n    }\n\n    console.log(`Attempting to update base image from ${ existingVersion } to ${ IMAGE_VERSION }...`);\n\n    if ((await this.status)?.status === 'Running') {\n      // This shouldn't be possible (it should only be running if we started it\n      // in the same Rancher Desktop instance); but just in case, we still stop\n      // the VM anyway.\n      await this.lima('stop', MACHINE_NAME);\n    }\n\n    const diskPath = path.join(paths.lima, MACHINE_NAME, 'basedisk');\n\n    await fs.promises.copyFile(this.baseDiskImage, diskPath);\n    // The config file will be updated in updateConfig() instead; no need to do it here.\n    console.log(`Base image successfully updated.`);\n  }\n\n  protected get baseDiskImage() {\n    const imageName = `alpine-lima-v${ IMAGE_VERSION }-${ ALPINE_EDITION }-${ ALPINE_VERSION }.iso`;\n\n    return path.join(paths.resources, os.platform(), imageName);\n  }\n\n  #sshPort = 0;\n  get sshPort(): Promise<number> {\n    return (async() => {\n      if (this.#sshPort === 0) {\n        if ((await this.status)?.status === 'Running') {\n          // if the machine is already running, we can't change the port.\n          const existingPort = (await this.getLimaConfig())?.ssh.localPort;\n\n          if (existingPort) {\n            this.#sshPort = existingPort;\n\n            return existingPort;\n          }\n        }\n\n        const server = net.createServer();\n\n        await new Promise((resolve) => {\n          server.once('listening', resolve);\n          server.listen(0, '127.0.0.1');\n        });\n        this.#sshPort = (server.address() as net.AddressInfo).port;\n        server.close();\n      }\n\n      return this.#sshPort;\n    })();\n  }\n\n  protected getMounts(): LimaMount[] {\n    const mounts: LimaMount[] = [];\n    const locations = ['~', '/tmp/rancher-desktop'];\n    const homeDir = `${ os.homedir() }/`;\n    const extraDirs = [paths.cache, paths.logs, paths.resources];\n\n    if (os.platform() === 'darwin') {\n      // /var and /tmp are symlinks to /private/var and /private/tmp\n      locations.push('/Volumes', '/var/folders', '/private/tmp', '/private/var/folders');\n    }\n    for (const extraDir of extraDirs) {\n      const found = locations.some((loc) => {\n        loc = loc === '~' ? homeDir : path.normalize(loc);\n\n        return !path.relative(loc, path.normalize(extraDir)).startsWith('../');\n      });\n\n      if (!found) {\n        locations.push(extraDir);\n      }\n    }\n\n    for (const location of locations) {\n      const mount: LimaMount = { location, writable: true };\n\n      if (this.cfg?.virtualMachine.mount.type === MountType.NINEP) {\n        const nineP = this.cfg.experimental.virtualMachine.mount['9p'];\n\n        mount['9p'] = {\n          securityModel:   nineP.securityModel,\n          protocolVersion: nineP.protocolVersion,\n          msize:           `${ nineP.msizeInKib }KiB`,\n          cache:           nineP.cacheMode,\n        };\n      }\n      mounts.push(mount);\n    }\n\n    return mounts;\n  }\n\n  /**\n   * Update the Lima configuration.  This may stop the VM if the base disk image\n   * needs to be changed.\n   */\n  protected async updateConfig(allowRoot = true) {\n    const currentConfig = await this.getLimaConfig();\n    const baseConfig: Partial<LimaConfiguration> = currentConfig || {};\n    // We use {} as the first argument because merge() modifies\n    // it, and it would be less safe to modify baseConfig.\n    const config: LimaConfiguration = merge({}, baseConfig, DEFAULT_CONFIG as LimaConfiguration, {\n      vmType:  this.cfg?.virtualMachine.type,\n      rosetta: {\n        enabled: this.cfg?.virtualMachine.useRosetta,\n        binfmt:  this.cfg?.virtualMachine.useRosetta,\n      },\n      images: [{\n        location: this.baseDiskImage,\n        arch:     this.arch,\n      }],\n      cpus:         this.cfg?.virtualMachine.numberCPUs || 4,\n      memory:       (this.cfg?.virtualMachine.memoryInGB || 4) * 1024 * 1024 * 1024,\n      disk:         this.cfg?.experimental.virtualMachine.diskSize ?? '100GiB',\n      mounts:       this.getMounts(),\n      mountType:    this.cfg?.virtualMachine.mount.type,\n      ssh:          { localPort: await this.sshPort },\n      hostResolver: {\n        hosts: {\n          // As far as lima is concerned, the instance name is 'lima-0'.\n          // We change the hostname in a provisioning script.\n          'lima-rancher-desktop':          'lima-0',\n          'host.rancher-desktop.internal': 'host.lima.internal',\n          'host.docker.internal':          'host.lima.internal',\n        },\n      },\n    });\n\n    // Alpine can boot via UEFI now\n    if (config.firmware) {\n      config.firmware.legacyBIOS = false;\n    }\n\n    // RD used to store additional keys in lima.yaml that are not supported by lima (and no longer used by RD).\n    // They must be removed because lima intends to switch to strict YAML parsing, so typos can be detected.\n    delete (config as unknown as Record<string, unknown>).k3s;\n    delete (config as unknown as Record<string, unknown>).paths;\n\n    if (os.platform() === 'darwin') {\n      if (allowRoot) {\n        const hostNetwork = (await this.getDarwinHostNetworks()).find((n) => {\n          return n.dhcp && n.IPv4?.Addresses?.some(addr => addr);\n        });\n\n        // Always add a shared network interface in case the bridged interface doesn't get an IP address.\n        config.networks = [{\n          lima:      'rancher-desktop-shared',\n          interface: 'rd1',\n        }];\n        if (hostNetwork) {\n          config.networks.push({\n            lima:      `rancher-desktop-bridged_${ hostNetwork.interface }`,\n            interface: 'rd0',\n          });\n        } else {\n          console.log('Could not find any acceptable host networks for bridging.');\n        }\n      } else if (this.cfg?.virtualMachine.type === VMType.VZ) {\n        console.log('Using vzNAT networking stack');\n        config.networks = [{\n          interface: 'vznat',\n          vzNAT:     true,\n        }];\n      } else {\n        console.log('Administrator access disallowed, not using socket_vmnet.');\n        delete config.networks;\n      }\n    }\n\n    this.updateConfigPortForwards(config);\n    if (currentConfig) {\n      // update existing configuration\n      const configPath = path.join(paths.lima, MACHINE_NAME, 'lima.yaml');\n\n      await this.progressTracker.action(\n        'Updating outdated virtual machine',\n        100,\n        this.updateBaseDisk(currentConfig),\n      );\n      await fs.promises.writeFile(configPath, yaml.stringify(config, { lineWidth: 0 }), 'utf-8');\n    } else {\n      // new configuration\n      await fs.promises.mkdir(path.dirname(this.CONFIG_PATH), { recursive: true });\n      await fs.promises.writeFile(this.CONFIG_PATH, yaml.stringify(config, { lineWidth: 0 }));\n      if (os.platform().startsWith('darwin')) {\n        try {\n          await childProcess.spawnFile('tmutil', ['addexclusion', paths.lima]);\n        } catch (ex) {\n          console.log('Failed to add exclusion to TimeMachine', ex);\n        }\n      }\n    }\n  }\n\n  protected updateConfigPortForwards(config: LimaConfiguration) {\n    let allPortForwards: Record<string, any>[] | undefined = config.portForwards;\n\n    if (!allPortForwards) {\n      // This shouldn't happen, but fix it anyway\n      config.portForwards = allPortForwards = DEFAULT_CONFIG.portForwards ?? [];\n    }\n    const hostSocket = path.join(paths.altAppHome, 'docker.sock');\n    const dockerPortForwards = allPortForwards?.find(entry => Object.keys(entry).length === 2 &&\n      entry.guestSocket === '/var/run/docker.sock' &&\n      ('hostSocket' in entry));\n\n    if (!dockerPortForwards) {\n      config.portForwards?.push({\n        guestSocket: '/var/run/docker.sock',\n        hostSocket,\n      });\n    } else {\n      dockerPortForwards.hostSocket = hostSocket;\n    }\n  }\n\n  protected async getLimaConfig(): Promise<LimaConfiguration | undefined> {\n    try {\n      const configPath = path.join(paths.lima, MACHINE_NAME, 'lima.yaml');\n      const configRaw = await fs.promises.readFile(configPath, 'utf-8');\n\n      return yaml.parse(configRaw) as LimaConfiguration;\n    } catch (ex) {\n      if ((ex as NodeJS.ErrnoException).code === 'ENOENT') {\n        return undefined;\n      }\n    }\n  }\n\n  protected static get limactl() {\n    return path.join(paths.resources, os.platform(), 'lima', 'bin', 'limactl');\n  }\n\n  protected static get qemuImg() {\n    return path.join(paths.resources, os.platform(), 'lima', 'bin', 'qemu-img');\n  }\n\n  protected static get limaEnv() {\n    const binDir = path.join(paths.resources, os.platform(), 'lima', 'bin');\n    const libDir = path.join(paths.resources, os.platform(), 'lima', 'lib');\n    const VMNETDir = path.join(VMNET_DIR, 'bin');\n    const pathList = (process.env.PATH || '').split(path.delimiter);\n    const newPath = [binDir, VMNETDir].concat(...pathList).filter(x => x);\n    const env = structuredClone(process.env);\n\n    env.LIMA_HOME = paths.lima;\n    env.PATH = newPath.join(path.delimiter);\n\n    // Override LD_LIBRARY_PATH to pick up the QEMU libraries.\n    // - on macOS, this is not used. The macOS dynamic linker uses DYLD_ prefixed variables.\n    // - on packaged (rpm/deb) builds, we do not ship this directory, so it does nothing.\n    // - for AppImage this has no effect because the libs are moved to a dir that is already on LD_LIBRARY_PATH\n    // - this only has an effect on builds extracted from a Linux zip file (which includes a bundled\n    //   QEMU) to make sure QEMU dependencies are loaded from the bundled lib directory.\n    if (env.LD_LIBRARY_PATH) {\n      env.LD_LIBRARY_PATH = libDir + path.delimiter + env.LD_LIBRARY_PATH;\n    } else {\n      env.LD_LIBRARY_PATH = libDir;\n    }\n\n    return env;\n  }\n\n  protected static get qemuImgEnv() {\n    return { ...process.env, LD_LIBRARY_PATH: path.join(paths.resources, os.platform(), 'lima', 'lib') };\n  }\n\n  /**\n   * Run `limactl` with the given arguments.\n   */\n  async lima(this: Readonly<this>, env: NodeJS.ProcessEnv, ...args: string[]): Promise<void>;\n  async lima(this: Readonly<this>, ...args: string[]): Promise<void>;\n  async lima(this: Readonly<this>, envOrArg: NodeJS.ProcessEnv | string, ...args: string[]): Promise<void> {\n    const env = LimaBackend.limaEnv;\n\n    if (typeof envOrArg === 'string') {\n      args = [envOrArg].concat(args);\n    } else {\n      Object.assign(env, envOrArg);\n    }\n    args = this.debug ? ['--debug'].concat(args) : args;\n    try {\n      const { stdout, stderr } = await childProcess.spawnFile(LimaBackend.limactl, args,\n        { env, stdio: ['ignore', 'pipe', 'pipe'] });\n      const formatBreak = stderr || stdout ? '\\n' : '';\n\n      console.log(`> limactl ${ args.join(' ') }${ formatBreak }${ stderr }${ stdout }`);\n    } catch (ex) {\n      console.error(`> limactl ${ args.join(' ') }\\n$`, ex);\n      throw ex;\n    }\n  }\n\n  /**\n   * Run `limactl` with the given arguments, and return stdout.\n   */\n  protected async limaWithCapture(this: Readonly<this>, ...args: string[]): Promise<{ stdout: string, stderr: string }>;\n  protected async limaWithCapture(this: Readonly<this>, options: SpawnOptions, ...args: string[]): Promise<{ stdout: string, stderr: string }>;\n  protected async limaWithCapture(this: Readonly<this>, argOrOptions: string | SpawnOptions, ...args: string[]): Promise<{ stdout: string, stderr: string }> {\n    let options: SpawnOptions = {};\n\n    if (typeof argOrOptions === 'string') {\n      args.unshift(argOrOptions);\n    } else {\n      options = clone(argOrOptions);\n    }\n    if (this.debug) {\n      args.unshift('--debug');\n    }\n    options['env'] = LimaBackend.limaEnv;\n\n    return await this.spawnWithCapture(LimaBackend.limactl, options, ...args);\n  }\n\n  async spawnWithCapture(this: Readonly<this>, cmd: string, argOrOptions: string | SpawnOptions = {}, ...args: string[]): Promise<{ stdout: string, stderr: string }> {\n    let options: SpawnOptions = {};\n\n    if (typeof argOrOptions === 'string') {\n      args.unshift(argOrOptions);\n    } else {\n      options = clone(argOrOptions);\n    }\n    options.env ??= process.env;\n\n    try {\n      const { stdout, stderr } = await childProcess.spawnFile(cmd, args, { env: options.env, stdio: ['ignore', 'pipe', 'pipe'] });\n      const formatBreak = stderr || stdout ? '\\n' : '';\n\n      console.log(`> ${ cmd } ${ args.join(' ') }${ formatBreak }${ stderr }${ stdout }`);\n\n      return { stdout, stderr };\n    } catch (ex: any) {\n      if (!options?.expectFailure) {\n        console.error(`> ${ cmd } ${ args.join(' ') }\\n$`, ex);\n        if (this.debug && 'stdout' in ex) {\n          console.error(ex.stdout);\n        }\n        if (this.debug && 'stderr' in ex) {\n          console.error(ex.stderr);\n        }\n      }\n      throw ex;\n    }\n  }\n\n  /**\n   * Run the given command within the VM.\n   */\n  limaSpawn(options: execOptions, args: string[]): ChildProcess {\n    const workDir = options.cwd ?? '.';\n\n    if (options.root) {\n      args = ['sudo'].concat(args);\n    }\n    args = ['shell', `--workdir=${ workDir }`, MACHINE_NAME].concat(args);\n\n    if (this.debug) {\n      console.log(`> limactl ${ args.join(' ') }`);\n      args.unshift('--debug');\n    }\n\n    return spawnWithSignal(\n      LimaBackend.limactl,\n      args,\n      { ...omit(options, 'cwd'), env: { ...LimaBackend.limaEnv, ...options.env ?? {} } });\n  }\n\n  async execCommand(...command: string[]): Promise<void>;\n  async execCommand(options: execOptions, ...command: string[]): Promise<void>;\n  async execCommand(options: execOptions & { capture: true }, ...command: string[]): Promise<string>;\n  async execCommand(optionsOrArg: execOptions | string, ...command: string[]): Promise<void | string> {\n    let options: execOptions & { capture?: boolean } = {};\n\n    if (typeof optionsOrArg === 'string') {\n      command = [optionsOrArg].concat(command);\n    } else {\n      options = optionsOrArg;\n    }\n    if (options.root) {\n      command = ['sudo'].concat(command);\n    }\n\n    const expectFailure = options.expectFailure ?? false;\n    const workDir = options.cwd ?? '.';\n\n    try {\n      // Print a slightly different message if execution fails.\n      const { stdout } = await this.limaWithCapture({ expectFailure: true }, 'shell', `--workdir=${ workDir }`, MACHINE_NAME, ...command);\n\n      if (options.capture) {\n        return stdout;\n      }\n    } catch (ex: any) {\n      if (!expectFailure) {\n        console.log(`Lima: executing: ${ command.join(' ') }: ${ ex }`);\n        if (this.debug && 'stdout' in ex) {\n          console.error('stdout:', ex.stdout);\n        }\n        if (this.debug && 'stderr' in ex) {\n          console.error('stderr:', ex.stderr);\n        }\n      }\n      throw ex;\n    }\n  }\n\n  spawn(...command: string[]): childProcess.ChildProcess;\n  spawn(options: execOptions, ...command: string[]): childProcess.ChildProcess;\n  spawn(optionsOrCommand: string | execOptions, ...command: string[]): ChildProcess {\n    let options: execOptions = {};\n    const args = command.concat();\n\n    if (typeof optionsOrCommand === 'string') {\n      args.unshift(optionsOrCommand);\n    } else {\n      options = optionsOrCommand;\n    }\n\n    return this.limaSpawn(options, args);\n  }\n\n  /**\n   * Get the current Lima VM status, or undefined if there was an error\n   * (e.g. the machine is not registered).\n   */\n  protected get status(): Promise<LimaListResult | undefined> {\n    return (async() => {\n      try {\n        const { stdout } = await this.limaWithCapture('list', '--json');\n        const lines = stdout.split(/\\r?\\n/).filter(x => x.trim());\n        const entries = lines.map(line => JSON.parse(line) as LimaListResult);\n\n        return entries.find(entry => entry.name === MACHINE_NAME);\n      } catch (ex) {\n        console.error('Could not parse lima status, assuming machine is unavailable.');\n\n        return undefined;\n      }\n    })();\n  }\n\n  protected async imageInfo(fileName: string): Promise<QEMUImageInfo> {\n    try {\n      const { stdout } = await this.spawnWithCapture(LimaBackend.qemuImg, { env: LimaBackend.qemuImgEnv },\n        'info', '--output=json', '--force-share', fileName);\n\n      return JSON.parse(stdout) as QEMUImageInfo;\n    } catch {\n      return { format: 'unknown' } as QEMUImageInfo;\n    }\n  }\n\n  protected async convertToRaw(fileName: string): Promise<void> {\n    const rawFileName = `${ fileName }.raw`;\n\n    await this.spawnWithCapture(LimaBackend.qemuImg, { env: LimaBackend.qemuImgEnv },\n      'convert', fileName, rawFileName);\n    await fs.promises.unlink(fileName);\n    await fs.promises.rename(rawFileName, fileName);\n  }\n\n  protected get isRegistered(): Promise<boolean> {\n    return this.status.then(defined);\n  }\n\n  private static calcRandomTag(desiredLength: number) {\n    // quicker to use Math.random() than pull in all the dependencies utils/string:randomStr wants\n    return Math.random().toString().substring(2, desiredLength + 2);\n  }\n\n  /**\n   * Show the dialog box describing why sudo is required.\n   *\n   * @param explanations Map of why we want sudo, and what files are affected.\n   * @return Whether the user wants to allow the prompt.\n   */\n  protected async showSudoReason(this: Readonly<this> & this, explanations: Record<string, string[]>): Promise<boolean> {\n    if (this.noModalDialogs || !this.cfg?.application.adminAccess) {\n      return false;\n    }\n    const neverAgain = await openSudoPrompt(explanations);\n\n    if (neverAgain && this.cfg) {\n      this.writeSetting({ application: { adminAccess: false } });\n\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * Run the various commands that require privileged access after prompting the\n   * user about the details.\n   *\n   * @returns Whether privileged access was successful; this will also be true\n   *          if no privileged access was required.\n   * @note This may request the root password.\n   */\n  protected async installToolsWithSudo(): Promise<boolean> {\n    const randomTag = LimaBackend.calcRandomTag(8);\n    const commands: string[] = [];\n    const explanations: Partial<Record<SudoReason, string[]>> = {};\n\n    const processCommand = (cmd: SudoCommand | undefined) => {\n      if (cmd) {\n        commands.push(...cmd.commands);\n        explanations[cmd.reason] = (explanations[cmd.reason] ?? []).concat(...cmd.paths);\n      }\n    };\n\n    if (os.platform() === 'darwin') {\n      await this.progressTracker.action('Setting up virtual ethernet', 10, async() => {\n        processCommand(await this.installVMNETTools());\n      });\n      await this.progressTracker.action('Setting Lima permissions', 10, async() => {\n        processCommand(await this.ensureRunLimaLocation());\n        processCommand(await this.createLimaSudoersFile(randomTag));\n      });\n    }\n    await this.progressTracker.action('Setting up Docker socket', 10, async() => {\n      processCommand(await this.configureDockerSocket());\n    });\n\n    if (commands.length === 0) {\n      return true;\n    }\n\n    const requirePassword = await this.sudoRequiresPassword();\n    let allowed = true;\n\n    if (requirePassword) {\n      allowed = await this.progressTracker.action(\n        'Expecting user permission to continue',\n        10,\n        this.showSudoReason(explanations));\n    }\n    if (!allowed) {\n      this.#adminAccess = false;\n\n      return false;\n    }\n\n    const singleCommand = commands.join('; ');\n\n    if (singleCommand.includes(\"'\")) {\n      throw new Error(`Can't execute commands ${ singleCommand } because there's a single-quote in them.`);\n    }\n    try {\n      if (requirePassword) {\n        await this.sudoExec(`/bin/sh -xec '${ singleCommand }'`);\n      } else {\n        await childProcess.spawnFile('sudo', ['--non-interactive', '/bin/sh', '-xec', singleCommand],\n          { stdio: ['ignore', 'pipe', 'pipe'] });\n      }\n    } catch (err) {\n      if (err instanceof Error && err.message === 'User did not grant permission.') {\n        this.#adminAccess = false;\n        console.error('Failed to execute sudo, falling back to unprivileged operation', err);\n\n        return false;\n      }\n      throw err;\n    }\n\n    return true;\n  }\n\n  /**\n   * Determine the commands required to install vmnet-related tools.\n   */\n  protected async installVMNETTools(this: unknown): Promise<SudoCommand | undefined> {\n    const sourcePath = path.join(paths.resources, os.platform(), 'lima', 'socket_vmnet');\n    const installedPath = VMNET_DIR;\n    const walk = async(dir: string): Promise<[string[], string[]]> => {\n      const fullPath = path.resolve(sourcePath, dir);\n      const entries = await fs.promises.readdir(fullPath, { withFileTypes: true });\n      const directories: string[] = [];\n      const files: string[] = [];\n\n      for (const entry of entries) {\n        if (entry.isDirectory()) {\n          const [childDirs, childFiles] = await walk(path.join(dir, entry.name));\n\n          directories.push(path.join(dir, entry.name), ...childDirs);\n          files.push(...childFiles);\n        } else if (entry.isFile() || entry.isSymbolicLink()) {\n          files.push(path.join(dir, entry.name));\n        } else {\n          const childPath = path.join(fullPath, entry.name);\n\n          console.error(`vmnet: Skipping unexpected file ${ childPath }`);\n        }\n      }\n\n      return [directories, files];\n    };\n    const [directories, files] = await walk('.');\n    const hashesMatch = await Promise.all(files.map(async(relPath) => {\n      const hashFile = async(fullPath: string) => {\n        const hash = crypto.createHash('sha256');\n\n        await new Promise((resolve) => {\n          const readStream = fs.createReadStream(fullPath);\n\n          // On error, resolve to anything that won't match the expected hash;\n          // this will trigger a copy. Using the full path is good enough here.\n          hash.on('finish', resolve);\n          hash.on('error', () => resolve(fullPath));\n          readStream.on('error', () => resolve(fullPath));\n          readStream.pipe(hash);\n        });\n\n        return hash.digest('hex');\n      };\n      const sourceFile = path.normalize(path.join(sourcePath, relPath));\n      const installedFile = path.normalize(path.join(installedPath, relPath));\n      const [sourceHash, installedHash] = await Promise.all([\n        hashFile(sourceFile), hashFile(installedFile),\n      ]);\n\n      return sourceHash === installedHash;\n    }));\n\n    if (hashesMatch.every(matched => matched)) {\n      return;\n    }\n\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-vmnet-install-'));\n    const tarPath = path.join(workdir, 'vmnet.tar');\n    const commands: string[] = [];\n\n    try {\n      // Actually create the tar file using all the files, not just the\n      // outdated ones, since we're going to need a prompt anyway.\n      const tarStream = fs.createWriteStream(tarPath);\n      const archive = tar.pack();\n      const archiveFinished = util.promisify(stream.finished)(archive as any);\n      const newEntry = util.promisify(archive.entry.bind(archive));\n      const baseHeader: Partial<tar.Headers> = {\n        mode:  0o755,\n        uid:   0,\n        uname: 'root',\n        gname: 'wheel',\n        type:  'directory',\n      };\n\n      archive.pipe(tarStream);\n\n      await newEntry({\n        ...baseHeader,\n        name: path.basename(installedPath),\n      });\n      for (const relPath of directories) {\n        const info = await fs.promises.lstat(path.join(sourcePath, relPath));\n\n        await newEntry({\n          ...baseHeader,\n          name:  path.normalize(path.join(path.basename(installedPath), relPath)),\n          mtime: info.mtime,\n        });\n      }\n      for (const relPath of files) {\n        const source = path.join(sourcePath, relPath);\n        const info = await fs.promises.lstat(source);\n        const header: tar.Headers = {\n          ...baseHeader,\n          name:  path.normalize(path.join(path.basename(installedPath), relPath)),\n          mode:  info.mode,\n          mtime: info.mtime,\n        };\n\n        if (info.isSymbolicLink()) {\n          header.type = 'symlink';\n          header.linkname = await fs.promises.readlink(source);\n          await newEntry(header);\n        } else {\n          header.type = 'file';\n          header.size = info.size;\n          const entry = archive.entry(header);\n          const readStream = fs.createReadStream(source);\n          const entryFinished = util.promisify(stream.finished)(entry);\n\n          readStream.pipe(entry);\n          await entryFinished;\n        }\n      }\n\n      archive.finalize();\n      await archiveFinished;\n      const command = `tar -xf \"${ tarPath }\" -C \"${ path.dirname(installedPath) }\"`;\n\n      console.log(`VMNET tools install required: ${ command }`);\n      commands.push(command);\n    } finally {\n      commands.push(`rm -fr ${ workdir }`);\n    }\n\n    return {\n      reason: 'networking',\n      commands,\n      paths:  [VMNET_DIR],\n    };\n  }\n\n  /**\n   * Create a sudoers file that has to be byte-for-byte identical to what `limactl sudoers` would create.\n   * We can't use `limactl sudoers` because it will fail when socket_vmnet has not yet been installed at\n   * the secure path. We don't want to ask the user twice for a password: once to install socket_vmnet,\n   * and once more to update the sudoers file. So we try to predict what `limactl sudoers` would write.\n   */\n  protected sudoersFile(config: LimaNetworkConfiguration): string {\n    const host = config.networks['host'];\n    const shared = config.networks['rancher-desktop-shared'];\n\n    if (host.mode !== 'host') {\n      throw new Error('host network has wrong type');\n    }\n    if (shared.mode !== 'shared') {\n      throw new Error('shared network has wrong type');\n    }\n\n    let name = 'host';\n    let sudoers = `%everyone ALL=(root:wheel) NOPASSWD:NOSETENV: /bin/mkdir -m 775 -p /private/var/run\n\n# Manage \"${ name }\" network daemons\n\n%everyone ALL=(root:wheel) NOPASSWD:NOSETENV: \\\\\n    /opt/rancher-desktop/bin/socket_vmnet --pidfile=/private/var/run/${ name }_socket_vmnet.pid --socket-group=everyone --vmnet-mode=host --vmnet-gateway=${ host.gateway } --vmnet-dhcp-end=${ host.dhcpEnd } --vmnet-mask=${ host.netmask } /private/var/run/socket_vmnet.${ name }, \\\\\n    /usr/bin/pkill -F /private/var/run/${ name }_socket_vmnet.pid\n\n`;\n\n    const networks = Object.keys(config.networks).sort();\n\n    for (const name of networks) {\n      const prefix = 'rancher-desktop-bridged_';\n\n      if (!name.startsWith(prefix)) {\n        continue;\n      }\n      sudoers += `# Manage \"${ name }\" network daemons\n\n%everyone ALL=(root:wheel) NOPASSWD:NOSETENV: \\\\\n    /opt/rancher-desktop/bin/socket_vmnet --pidfile=/private/var/run/${ name }_socket_vmnet.pid --socket-group=everyone --vmnet-mode=bridged --vmnet-interface=${ name.slice(prefix.length) } /private/var/run/socket_vmnet.${ name }, \\\\\n    /usr/bin/pkill -F /private/var/run/${ name }_socket_vmnet.pid\n\n`;\n    }\n\n    name = 'rancher-desktop-shared';\n    sudoers += `# Manage \"${ name }\" network daemons\n\n%everyone ALL=(root:wheel) NOPASSWD:NOSETENV: \\\\\n    /opt/rancher-desktop/bin/socket_vmnet --pidfile=/private/var/run/${ name }_socket_vmnet.pid --socket-group=everyone --vmnet-mode=shared --vmnet-gateway=${ shared.gateway } --vmnet-dhcp-end=${ shared.dhcpEnd } --vmnet-mask=${ shared.netmask } /private/var/run/socket_vmnet.${ name }, \\\\\n    /usr/bin/pkill -F /private/var/run/${ name }_socket_vmnet.pid\n`;\n\n    return sudoers;\n  }\n\n  protected async createLimaSudoersFile(this: Readonly<this> & this, randomTag: string): Promise<SudoCommand | undefined> {\n    const paths: string[] = [];\n    const commands: string[] = [];\n\n    try {\n      await fs.promises.access(PREVIOUS_LIMA_SUDOERS_LOCATION);\n      commands.push(`rm -f ${ PREVIOUS_LIMA_SUDOERS_LOCATION }`);\n      paths.push(PREVIOUS_LIMA_SUDOERS_LOCATION);\n      console.debug(`Previous sudoers file ${ PREVIOUS_LIMA_SUDOERS_LOCATION } exists, will delete.`);\n    } catch (err: any) {\n      if (err?.code !== 'ENOENT') {\n        console.error(`Error checking ${ PREVIOUS_LIMA_SUDOERS_LOCATION }: ${ err }; ignoring.`);\n      }\n    }\n\n    const networkConfig = await this.installCustomLimaNetworkConfig(true);\n    const sudoers = this.sudoersFile(networkConfig);\n    let updateSudoers = false;\n\n    try {\n      const existingSudoers = await fs.promises.readFile(LIMA_SUDOERS_LOCATION, { encoding: 'utf-8' });\n\n      if (sudoers !== existingSudoers) {\n        updateSudoers = true;\n      }\n    } catch (ex: any) {\n      if (ex?.code !== 'ENOENT') {\n        throw ex;\n      }\n      updateSudoers = true;\n      console.debug(`Sudoers file ${ LIMA_SUDOERS_LOCATION } does not exist, creating.`);\n    }\n\n    if (updateSudoers) {\n      const tmpFile = path.join(os.tmpdir(), `rd-sudoers${ randomTag }.txt`);\n\n      await fs.promises.writeFile(tmpFile, sudoers, { mode: 0o644 });\n      commands.push(`mkdir -p \"${ path.dirname(LIMA_SUDOERS_LOCATION) }\" && cp \"${ tmpFile }\" ${ LIMA_SUDOERS_LOCATION } && rm -f \"${ tmpFile }\"`);\n      paths.push(LIMA_SUDOERS_LOCATION);\n      console.debug(`Sudoers file ${ LIMA_SUDOERS_LOCATION } needs to be updated.`);\n    }\n\n    if (commands.length > 0) {\n      return {\n        reason: 'networking', commands, paths,\n      };\n    }\n  }\n\n  protected async ensureRunLimaLocation(this: unknown): Promise<SudoCommand | undefined> {\n    const limaRunLocation: string = NETWORKS_CONFIG.paths.varRun;\n    const commands: string[] = [];\n    let dirInfo: fs.Stats | null;\n\n    try {\n      dirInfo = await fs.promises.stat(limaRunLocation);\n\n      // If it's owned by root and not readable by others, it's fine\n      if (dirInfo.uid === 0 && (dirInfo.mode & fs.constants.S_IWOTH) === 0) {\n        return;\n      }\n    } catch (err) {\n      dirInfo = null;\n      if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n        console.log(`Unexpected situation with ${ limaRunLocation }, stat => error ${ err }`, err);\n        throw err;\n      }\n    }\n    if (!dirInfo) {\n      commands.push(`mkdir -p ${ limaRunLocation }`);\n      commands.push(`chmod 755 ${ limaRunLocation }`);\n    }\n    commands.push(`chown -R root:daemon ${ limaRunLocation }`);\n    commands.push(`chmod -R o-w ${ limaRunLocation }`);\n\n    return {\n      reason: 'networking',\n      commands,\n      paths:  [limaRunLocation],\n    };\n  }\n\n  protected async configureDockerSocket(this: Readonly<this> & this): Promise<SudoCommand | undefined> {\n    if (this.cfg?.containerEngine.name !== ContainerEngine.MOBY) {\n      return;\n    }\n    const realPath = await this.evalSymlink(DEFAULT_DOCKER_SOCK_LOCATION);\n    const targetPath = path.join(paths.altAppHome, 'docker.sock');\n\n    if (realPath === targetPath) {\n      return;\n    }\n\n    return {\n      reason:   'docker-socket',\n      commands: [`ln -sf \"${ targetPath }\" \"${ DEFAULT_DOCKER_SOCK_LOCATION }\"`],\n      paths:    [DEFAULT_DOCKER_SOCK_LOCATION],\n    };\n  }\n\n  protected async evalSymlink(this: Readonly<this>, path: string): Promise<string> {\n    // Use lstat.isSymbolicLink && readlink(path) to walk symlinks,\n    // instead of fs.readlink(file) to show both where a symlink is\n    // supposed to point, whether or not the referent exists right now.\n    // Do this because the lima docker.sock (the referent) is deleted when lima shuts down.\n    // Most of the time /var/run/docker.sock points directly to the lima socket, but\n    // this code allows intermediate symlinks.\n    try {\n      while ((await fs.promises.lstat(path)).isSymbolicLink()) {\n        path = await fs.promises.readlink(path);\n      }\n    } catch (err) {\n      if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n        console.log(`Error trying to resolve symbolic link ${ path }:`, err);\n      }\n    }\n\n    return path;\n  }\n\n  protected async sudoRequiresPassword() {\n    try {\n      // Check if we can run /usr/bin/true (or /bin/true) without requiring a password\n      await childProcess.spawnFile('sudo', ['--non-interactive', '--reset-timestamp', 'true'],\n        { stdio: ['ignore', 'pipe', 'pipe'] });\n      console.debug(\"sudo --non-interactive didn't throw an error, so assume we can do passwordless sudo\");\n\n      return false;\n    } catch (err: any) {\n      console.debug(`sudo --non-interactive threw an error, so assume it needs a password: ${ JSON.stringify(err) }`);\n\n      return true;\n    }\n  }\n\n  /**\n   * Use the sudo-prompt library to run the script as root\n   * @param command: Path to an executable file\n   */\n  protected async sudoExec(this: unknown, command: string) {\n    await new Promise<void>((resolve, reject) => {\n      sudo(command, { name: 'Rancher Desktop' }, (error, stdout, stderr) => {\n        if (stdout) {\n          console.log(`Prompt for sudo: stdout: ${ stdout }`);\n        }\n        if (stderr) {\n          console.log(`Prompt for sudo: stderr: ${ stderr }`);\n        }\n        if (error) {\n          reject(error);\n        } else {\n          resolve();\n        }\n      });\n    });\n  }\n\n  /**\n   * Provide a default network config file with rancher-desktop specific settings.\n   *\n   * If there's an existing file, replace it if it doesn't contain a\n   * paths.varRun setting for rancher-desktop\n   */\n  protected async installCustomLimaNetworkConfig(allowRoot = true): Promise<LimaNetworkConfiguration> {\n    const networkPath = path.join(paths.lima, '_config', 'networks.yaml');\n\n    let config: LimaNetworkConfiguration;\n\n    try {\n      config = yaml.parse(await fs.promises.readFile(networkPath, 'utf8'));\n      if (config?.paths?.varRun !== NETWORKS_CONFIG.paths.varRun) {\n        const backupName = networkPath.replace(/\\.yaml$/, '.orig.yaml');\n\n        await fs.promises.rename(networkPath, backupName);\n        console.log(`Lima network configuration has unexpected contents; existing file renamed as ${ backupName }.`);\n        config = clone(NETWORKS_CONFIG);\n      }\n    } catch (err) {\n      if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n        console.log(`Existing networks.yaml file ${ networkPath } not yaml-parsable, got error ${ err }. It will be replaced.`);\n      }\n      config = clone(NETWORKS_CONFIG);\n    }\n\n    config.paths['socketVMNet'] = '/opt/rancher-desktop/bin/socket_vmnet';\n\n    // Clean up deprecated keys in config.paths, if present.\n    // These keys are no longer used and may cause errors\n    // during rdctl shutdown when upgrading to version\n    // 1.20.x or later.\n    delete (config.paths as any)['vdeSwitch'];\n    delete (config.paths as any)['vdeVMNet'];\n\n    if (config.group === 'staff') {\n      config.group = 'everyone';\n    }\n\n    for (const key of Object.keys(config.networks)) {\n      if (key.startsWith('rancher-desktop-bridged_')) {\n        delete config.networks[key];\n      }\n    }\n\n    if (allowRoot) {\n      for (const hostNetwork of await this.getDarwinHostNetworks()) {\n        // Indiscriminately add all host networks, whether they _currently_ have\n        // DHCP / IPv4 addresses.\n        if (hostNetwork.interface) {\n          config.networks[`rancher-desktop-bridged_${ hostNetwork.interface }`] = {\n            mode:      'bridged',\n            interface: hostNetwork.interface,\n          };\n        }\n      }\n      const sudoersPath = config.paths.sudoers;\n\n      // Explanation of this rename at definition of PREVIOUS_LIMA_SUDOERS_LOCATION\n      if (!sudoersPath || sudoersPath === PREVIOUS_LIMA_SUDOERS_LOCATION) {\n        config.paths.sudoers = LIMA_SUDOERS_LOCATION;\n      }\n    } else {\n      delete config.paths.sudoers;\n    }\n\n    await fs.promises.writeFile(networkPath, yaml.stringify(config), { encoding: 'utf-8' });\n\n    return config;\n  }\n\n  /**\n   * Get host networking information on a darwin system.\n   */\n  protected async getDarwinHostNetworks(): Promise<SPNetworkDataType[]> {\n    const { stdout } = await childProcess.spawnFile('/usr/sbin/system_profiler',\n      ['SPNetworkDataType', '-json', '-detailLevel', 'basic'],\n      { stdio: ['ignore', 'pipe', console] });\n\n    return JSON.parse(stdout).SPNetworkDataType;\n  }\n\n  protected async configureContainerEngine(): Promise<void> {\n    try {\n      const configureWASM = !!this.cfg?.experimental?.containerEngine?.webAssembly?.enabled;\n      const mobyStorageDriver = this.cfg?.containerEngine?.mobyStorageDriver ?? 'auto';\n\n      await this.writeFile('/usr/local/bin/nerdctl', NERDCTL, 0o755);\n\n      await this.execCommand({ root: true }, 'mkdir', '-p', '/etc/cni/net.d');\n      if (this.cfg?.kubernetes.options.flannel) {\n        await this.writeFile('/etc/cni/net.d/10-flannel.conflist', FLANNEL_CONFLIST);\n      }\n\n      const promises: Promise<unknown>[] = [];\n\n      promises.push(BackendHelper.configureContainerEngine(this, configureWASM, mobyStorageDriver));\n      if (configureWASM) {\n        const version = semver.parse(DEPENDENCY_VERSIONS.spinCLI);\n        const env = {\n          ...process.env,\n          KUBE_PLUGIN_VERSION: DEPENDENCY_VERSIONS.spinKubePlugin,\n          SPIN_TEMPLATES_TAG:  (version ? `spin/templates/v${ version.major }.${ version.minor }` : 'unknown'),\n        };\n\n        promises.push(this.spawnWithCapture(executable('setup-spin'), { env }));\n      }\n      await Promise.all(promises);\n    } catch (err) {\n      console.log(`Error trying to start/update containerd: ${ err }: `, err);\n    }\n  }\n\n  protected async configureLogrotate(): Promise<void> {\n    await this.writeFile('/etc/logrotate.d/lima-guestagent', LOGROTATE_LIMA_GUESTAGENT_SCRIPT, 0o644);\n  }\n\n  async readFile(filePath: string, options?: { encoding?: BufferEncoding }): Promise<string> {\n    const encoding = options?.encoding ?? 'utf-8';\n    const stdout: Buffer[] = [];\n    const stderr: Buffer[] = [];\n\n    try {\n      // Use limaSpawn to avoid logging file contents (too verbose).\n      const proc = this.limaSpawn({ root: true }, ['/bin/cat', filePath]);\n\n      await new Promise<void>((resolve, reject) => {\n        proc.stdout?.on('data', (chunk: Buffer | string) => {\n          stdout.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n        });\n        proc.stderr?.on('data', (chunk: Buffer | string) => {\n          stderr.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n        });\n        proc.on('error', reject);\n        proc.on('exit', (code, signal) => {\n          if (code || signal) {\n            return reject(new Error(`Failed to read ${ filePath }: /bin/cat exited with ${ code || signal }`));\n          }\n          resolve();\n        });\n      });\n\n      return Buffer.concat(stdout).toString(encoding);\n    } catch (ex: any) {\n      console.error(`Failed to read file ${ filePath }:`, ex);\n      if (stderr.length) {\n        console.error(Buffer.concat(stderr).toString('utf-8'));\n      }\n      if (stdout.length) {\n        console.error(Buffer.concat(stdout).toString('utf-8'));\n      }\n      throw ex;\n    }\n  }\n\n  /**\n   * Write the given contents to a given file name in the VM.\n   * The file will be owned by root.\n   * @param filePath The destination file path, in the VM.\n   * @param fileContents The contents of the file.\n   * @param permissions The file permissions.\n   */\n  async writeFile(filePath: string, fileContents: string, permissions: fs.Mode = 0o644) {\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `rd-${ path.basename(filePath) }-`));\n    const tempPath = `/tmp/${ path.basename(workdir) }.${ path.basename(filePath) }`;\n\n    try {\n      const scriptPath = path.join(workdir, path.basename(filePath));\n\n      await fs.promises.writeFile(scriptPath, fileContents, 'utf-8');\n      await this.lima('copy', scriptPath, `${ MACHINE_NAME }:${ tempPath }`);\n      await this.execCommand('chmod', permissions.toString(8), tempPath);\n      await this.execCommand({ root: true }, 'mv', tempPath, filePath);\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true });\n      await this.execCommand({ root: true }, 'rm', '-f', tempPath);\n    }\n  }\n\n  async copyFileIn(hostPath: string, vmPath: string): Promise<void> {\n    // TODO This logic is copied from writeFile() above and should be simplified.\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `rd-${ path.basename(hostPath) }-`));\n    const tempPath = `/tmp/${ path.basename(workdir) }.${ path.basename(hostPath) }`;\n\n    try {\n      await this.lima('copy', hostPath, `${ MACHINE_NAME }:${ tempPath }`);\n      await this.execCommand('chmod', '644', tempPath);\n      await this.execCommand({ root: true }, 'mv', tempPath, vmPath);\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true });\n      await this.execCommand({ root: true }, 'rm', '-f', tempPath);\n    }\n  }\n\n  copyFileOut(vmPath: string, hostPath: string): Promise<void> {\n    return this.lima('copy', `${ MACHINE_NAME }:${ vmPath }`, hostPath);\n  }\n\n  /**\n   * Get IPv4 address for specified interface.\n   */\n  async getInterfaceAddr(iface: string) {\n    try {\n      const ipAddr = await this.execCommand({ capture: true },\n        'ip', '--family', 'inet', 'addr', 'show', iface);\n      const match = / inet ([0-9.]+)/.exec(ipAddr);\n\n      return match ? match[1] : '';\n    } catch (ex: any) {\n      console.error(`Could not get address for ${ iface }: ${ ex?.stderr || ex }`);\n\n      return '';\n    }\n  }\n\n  /**\n   * Get the network interface name and address to listen on for services;\n   * used for flannel configuration.\n   */\n  async getListeningInterface(allowSudo: boolean) {\n    if (allowSudo) {\n      const bridgedIP = await this.getInterfaceAddr('rd0');\n\n      if (bridgedIP) {\n        console.log(`Using ${ bridgedIP } on bridged network rd0`);\n\n        return { iface: 'rd0', addr: bridgedIP };\n      }\n      const sharedIP = await this.getInterfaceAddr('rd1');\n\n      if (this.cfg?.application.adminAccess) {\n        await this.noBridgedNetworkDialog(sharedIP);\n      }\n      if (sharedIP) {\n        console.log(`Using ${ sharedIP } on shared network rd1`);\n\n        return { iface: 'rd1', addr: sharedIP };\n      }\n      console.log(`Neither bridged network rd0 nor shared network rd1 have an IPv4 address`);\n    }\n    if (this.cfg?.virtualMachine.type === VMType.VZ) {\n      const vznatIP = await this.getInterfaceAddr('vznat');\n\n      if (vznatIP) {\n        console.log(`Using ${ vznatIP } on vznat network`);\n\n        return { iface: 'vznat', addr: vznatIP };\n      }\n      console.log(`vznat interface does not have an IPv4 address`);\n    }\n\n    return { iface: 'eth0', addr: await this.ipAddress };\n  }\n\n  /**\n   * Display dialog to explain that bridged networking is not available.\n   */\n  protected noBridgedNetworkDialog(sharedIP: string) {\n    const options: Electron.NotificationConstructorOptions = {\n      title: 'Bridged network did not get an IP address.',\n      body:  `Using shared network address ${ sharedIP }`,\n      icon:  'info',\n    };\n\n    if (!sharedIP) {\n      options.body = \"Shared network isn't available either. Only network access is via port forwarding to the host.\";\n    }\n\n    this.emit('show-notification', options);\n\n    return Promise.resolve();\n  }\n\n  protected async writeBuildkitScripts() {\n    await this.writeFile(`/etc/init.d/buildkitd`, SERVICE_BUILDKITD_INIT, 0o755);\n    await this.writeFile(`/etc/conf.d/buildkitd`, SERVICE_BUILDKITD_CONF, 0o644);\n  }\n\n  protected async getResolver() {\n    try {\n      const limaEnv = await this.execCommand({ capture: true, root: true },\n        'grep', 'LIMA_CIDATA_SLIRP_DNS', '/mnt/lima-cidata/lima.env');\n      const match = /LIMA_CIDATA_SLIRP_DNS=([0-9.]+)/.exec(limaEnv);\n\n      return match ? match[1] : SLIRP.DNS;\n    } catch (ex: any) {\n      console.error(`Could not get resolver address: ${ ex?.stderr || ex }`);\n\n      // This will be wrong for VZ emulation\n      return SLIRP.DNS;\n    }\n  }\n\n  protected async configureOpenResty(config: BackendSettings) {\n    const allowedImagesConf = '/usr/local/openresty/nginx/conf/allowed-images.conf';\n    const resolver = `resolver ${ await this.getResolver() } ipv6=off;\\n`;\n\n    await this.writeFile(`/usr/local/openresty/nginx/conf/nginx.conf`, NGINX_CONF, 0o644);\n    await this.writeFile(`/usr/local/openresty/nginx/conf/resolver.conf`, resolver, 0o644);\n    await this.writeFile('/etc/logrotate.d/openresty', LOGROTATE_OPENRESTY_SCRIPT, 0o644);\n    if (config.containerEngine.allowedImages.enabled) {\n      const patterns = BackendHelper.createAllowedImageListConf(config.containerEngine.allowedImages);\n\n      await this.writeFile(allowedImagesConf, patterns, 0o644);\n    } else {\n      await this.execCommand({ root: true }, 'rm', '-f', allowedImagesConf);\n    }\n    const obsoleteImageAllowListConf = path.join(path.dirname(allowedImagesConf), 'image-allow-list.conf');\n\n    await this.execCommand({ root: true }, 'rm', '-f', obsoleteImageAllowListConf);\n  }\n\n  /**\n   * Write a configuration file for an OpenRC service.\n   * @param service The name of the OpenRC service to configure.\n   * @param settings A mapping of configuration values.  This should be shell escaped.\n   */\n  async writeConf(service: string, settings: Record<string, string>) {\n    const contents = Object.entries(settings).map(([key, value]) => `${ key }=\"${ value }\"\\n`).join('');\n\n    await this.writeFile(`/etc/conf.d/${ service }`, contents);\n  }\n\n  protected async installTrivy() {\n    const trivyPath = path.join(paths.resources, 'linux', 'internal', 'trivy');\n\n    await this.lima('copy', trivyPath, `${ MACHINE_NAME }:./trivy`);\n    await this.execCommand({ root: true }, 'mv', './trivy', '/usr/local/bin/trivy');\n  }\n\n  /**\n   * Start the VM.  If the machine is already started, this does nothing.\n   * Note that this does not start k3s.\n   * @precondition The VM configuration is correct.\n   */\n  protected async startVM() {\n    let allowRoot = this.#adminAccess;\n\n    // We need both the lima config + the lima network config to correctly check if we need sudo\n    // access; but if it's denied, we need to regenerate both again to account for the change.\n    allowRoot &&= await this.progressTracker.action('Asking for permission to run tasks as administrator', 100, this.installToolsWithSudo());\n\n    if (!allowRoot) {\n      // sudo access was denied; re-generate the config.\n      await this.progressTracker.action('Regenerating configuration to account for lack of permissions', 100, Promise.all([\n        this.updateConfig(false),\n        this.installCustomLimaNetworkConfig(false),\n      ]));\n    }\n\n    await this.progressTracker.action('Starting virtual machine', 100, async() => {\n      try {\n        const env: NodeJS.ProcessEnv = {};\n\n        env.LIMA_SSH_PORT_FORWARDER = this.cfg?.experimental.virtualMachine.sshPortForwarder ? 'true' : 'false';\n        await this.lima(env, 'start', '--tty=false', await this.isRegistered ? MACHINE_NAME : this.CONFIG_PATH);\n      } finally {\n        // Symlink the logs (especially if start failed) so the users can find them\n        const machineDir = path.join(paths.lima, MACHINE_NAME);\n\n        // Start the process, but ignore the result.\n        fs.promises.readdir(machineDir)\n          .then(filenames => filenames.filter(x => x.endsWith('.log'))\n            .forEach(filename => fs.promises.symlink(\n              path.join(path.relative(paths.logs, machineDir), filename),\n              path.join(paths.logs, `lima.${ filename }`))\n              .catch(() => { })));\n        try {\n          await fs.promises.rm(this.CONFIG_PATH, { force: true });\n        } catch (e) {\n          console.debug(`Failed to delete ${ this.CONFIG_PATH }: ${ e }`);\n        }\n      }\n    });\n  }\n\n  async start(config_: BackendSettings): Promise<void> {\n    const config = this.cfg = clone(config_);\n    let kubernetesVersion: semver.SemVer | undefined;\n    let isDowngrade = false;\n\n    await this.setState(State.STARTING);\n    this.currentAction = Action.STARTING;\n    this.#adminAccess = config_.application.adminAccess ?? true;\n    this.#containerEngineClient = undefined;\n    await this.progressTracker.action('Starting Backend', 10, async() => {\n      try {\n        this.ensureArchitectureMatch();\n        await Promise.all([\n          this.progressTracker.action('Ensuring virtualization is supported', 50, this.ensureVirtualizationSupported()),\n          this.progressTracker.action('Updating cluster configuration', 50, this.updateConfig(this.#adminAccess)),\n        ]);\n\n        if (this.currentAction !== Action.STARTING) {\n          // User aborted before we finished\n          return;\n        }\n\n        const vmStatus = await this.status;\n        let isVMAlreadyRunning = vmStatus?.status === 'Running';\n\n        // Virtualization Framework only supports RAW disks\n        if (vmStatus && config.virtualMachine.type === VMType.VZ) {\n          const diffdisk = path.join(paths.lima, MACHINE_NAME, 'diffdisk');\n          const { format } = await this.imageInfo(diffdisk);\n\n          if (format === ImageFormat.QCOW2) {\n            if (isVMAlreadyRunning) {\n              await this.lima('stop', MACHINE_NAME);\n              isVMAlreadyRunning = false;\n            }\n            await this.convertToRaw(diffdisk);\n          }\n        }\n        // Start the VM; if it's already running, this does nothing.\n        await this.startVM();\n\n        // Clear the diagnostic about not having Kubernetes versions\n        mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: true });\n\n        if (config.kubernetes.enabled) {\n          [kubernetesVersion, isDowngrade] = await this.kubeBackend.download(config);\n\n          if (kubernetesVersion === undefined) {\n            if (isDowngrade) {\n              // The desired version was unavailable, and the user declined a downgrade.\n              await this.setState(State.ERROR);\n\n              return;\n            }\n            // The desired version was unavailable, and we couldn't find a fallback.\n            // Notify the user, and turn off Kubernetes.\n            mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: false });\n            this.writeSetting({ kubernetes: { enabled: false } });\n          }\n        }\n\n        if (this.currentAction !== Action.STARTING) {\n          // User aborted before we finished\n          return;\n        }\n\n        if ((await this.status)?.status === 'Running') {\n          await this.progressTracker.action('Stopping existing instance', 100, async() => {\n            await this.kubeBackend.stop();\n            if (isDowngrade && isVMAlreadyRunning) {\n              // If we're downgrading, stop the VM (and start it again immediately),\n              // to ensure there are no containers running (so we can delete files).\n              await this.lima('stop', MACHINE_NAME);\n              await this.startVM();\n            }\n          });\n        }\n\n        if (this.currentAction !== Action.STARTING) {\n          // User aborted before we finished\n          return;\n        }\n\n        if (kubernetesVersion) {\n          await this.kubeBackend.deleteIncompatibleData(kubernetesVersion);\n        }\n\n        await Promise.all([\n          this.progressTracker.action('Installing CA certificates', 50, this.installCACerts()),\n          this.progressTracker.action('Configuring image proxy', 50, this.configureOpenResty(config)),\n          this.progressTracker.action('Configuring container engine', 50, this.configureContainerEngine()),\n          this.progressTracker.action('Configuring logrotate', 50, this.configureLogrotate()),\n        ]);\n\n        if (config.containerEngine.allowedImages.enabled) {\n          await this.startService('rd-openresty');\n        }\n        switch (config.containerEngine.name) {\n        case ContainerEngine.CONTAINERD:\n          await this.startService('containerd');\n          try {\n            await this.execCommand({\n              root:          true,\n              expectFailure: true,\n            },\n            'ctr', '--address', '/run/k3s/containerd/containerd.sock', 'namespaces', 'create', 'default');\n          } catch {\n            // expecting failure because the namespace may already exist\n          }\n          break;\n        case ContainerEngine.MOBY:\n          // The string is for shell expansion, not a JS template string.\n          // eslint-disable-next-line no-template-curly-in-string\n          await this.writeConf('docker', { DOCKER_OPTS: '--host=unix:///var/run/docker.sock --host=unix:///var/run/docker.sock.raw ${DOCKER_OPTS:-}' });\n          await this.startService('docker');\n          break;\n        case ContainerEngine.NONE:\n          throw new Error('No container engine is set');\n        }\n\n        const tasks = [\n          this.progressTracker.action('Installing Buildkit', 50, this.writeBuildkitScripts()),\n          this.progressTracker.action('Installing image scanner', 50, this.installTrivy()),\n          this.progressTracker.action('Installing credential helper', 50, this.installCredentialHelper()),\n        ];\n        if (kubernetesVersion) {\n          tasks.push(this.kubeBackend.install(config, kubernetesVersion, this.#adminAccess));\n        }\n\n        await Promise.all(tasks);\n\n        if (this.currentAction !== Action.STARTING) {\n          // User aborted\n          return;\n        }\n\n        switch (config.containerEngine.name) {\n        case ContainerEngine.MOBY:\n          this.#containerEngineClient = new MobyClient(this, `unix://${ path.join(paths.altAppHome, 'docker.sock') }`);\n          await this.progressTracker.action('Setting docker context', 50,\n            dockerDirManager.ensureDockerContextConfigured(\n              this.#adminAccess,\n              path.join(paths.altAppHome, 'docker.sock')));\n          break;\n        case ContainerEngine.CONTAINERD:\n          await this.execCommand({ root: true }, '/sbin/rc-service', '--ifnotstarted', 'buildkitd', 'start');\n          this.#containerEngineClient = new NerdctlClient(this);\n          break;\n        }\n\n        const actions = [\n          this.progressTracker.action('Waiting for container engine client to be ready', 50,\n            this.#containerEngineClient.waitForReady()),\n        ];\n\n        if (kubernetesVersion) {\n          actions.push(this.kubeBackend.start(config, kubernetesVersion));\n        }\n\n        await Promise.all(actions);\n\n        await this.setState(config.kubernetes.enabled ? State.STARTED : State.DISABLED);\n      } catch (err) {\n        console.error('Error starting lima:', err);\n        await this.setState(State.ERROR);\n        if (err instanceof BackendError) {\n          if (!err.fatal) {\n            return;\n          }\n        }\n        throw err;\n      } finally {\n        this.currentAction = Action.NONE;\n      }\n    });\n  }\n\n  protected async startService(serviceName: string) {\n    await this.progressTracker.action(`Starting ${ serviceName }`, 50, async() => {\n      await this.execCommand({ root: true }, '/sbin/rc-service', '--ifnotstarted', serviceName, 'start');\n    });\n  }\n\n  protected async installCACerts(): Promise<void> {\n    const certs = await this.progressTracker.action('fetching certificates', 56,\n      new Promise<(string | Buffer)[]>((resolve) => {\n        mainEvents.once('cert-ca-certificates', resolve);\n        mainEvents.emit('cert-get-ca-certificates');\n      }));\n\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-ca-'));\n\n    try {\n      await this.progressTracker.action('removing existing certificates', 50,\n        this.execCommand({ root: true }, '/bin/sh', '-c', 'rm -f /usr/local/share/ca-certificates/rd-*.crt'));\n\n      if (certs && certs.length > 0) {\n        await this.progressTracker.action('bundling certificates', 50, async function() {\n          const writeStream = fs.createWriteStream(path.join(workdir, 'certs.tar'));\n          const archive = tar.pack();\n          const archiveFinished = util.promisify(stream.finished)(archive as any);\n\n          archive.pipe(writeStream);\n\n          for (const [index, cert] of certs.entries()) {\n            const curried = archive.entry.bind(archive, {\n              name: `rd-${ index }.crt`,\n              mode: 0o600,\n            }, cert);\n\n            await util.promisify(curried)();\n          }\n          archive.finalize();\n          await archiveFinished;\n        });\n\n        await this.progressTracker.action('copying certificates', 50,\n          this.lima('copy', path.join(workdir, 'certs.tar'), `${ MACHINE_NAME }:/tmp/certs.tar`));\n        await this.progressTracker.action('extracting certificates', 50,\n          this.execCommand({ root: true }, 'tar', 'xf', '/tmp/certs.tar', '-C', '/usr/local/share/ca-certificates/'));\n      }\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true, force: true });\n    }\n    await this.progressTracker.action('Running update-ca-certificates', 50,\n      this.execCommand({ root: true }, 'update-ca-certificates'));\n  }\n\n  protected async installCredentialHelper() {\n    const credsPath = getServerCredentialsPath();\n\n    try {\n      const stateInfo: ServerState = JSON.parse(await fs.promises.readFile(credsPath, { encoding: 'utf-8' }));\n      const escapedPassword = stateInfo.password.replace(/\\\\/g, '\\\\\\\\')\n        .replace(/'/g, \"\\\\'\");\n      // leading `$` is needed to escape single-quotes, as : $'abc\\'xyz'\n      const leadingDollarSign = stateInfo.password.includes(\"'\") ? '$' : '';\n      const fileContents = `CREDFWD_AUTH=${ leadingDollarSign }'${ stateInfo.user }:${ escapedPassword }'\nCREDFWD_URL='http://${ SLIRP.HOST_GATEWAY }:${ stateInfo.port }'\n`;\n      const defaultConfig = { credsStore: 'rancher-desktop' };\n      let existingConfig: Record<string, any>;\n\n      await this.execCommand({ root: true }, 'mkdir', '-p', ETC_RANCHER_DESKTOP_DIR);\n      await this.writeFile(CREDENTIAL_FORWARDER_SETTINGS_PATH, fileContents, 0o644);\n      await this.writeFile(DOCKER_CREDENTIAL_PATH, DOCKER_CREDENTIAL_SCRIPT, 0o755);\n      try {\n        existingConfig = JSON.parse(await this.execCommand({ capture: true, root: true }, 'cat', ROOT_DOCKER_CONFIG_PATH));\n      } catch (err: any) {\n        await this.execCommand({ root: true }, 'mkdir', '-p', ROOT_DOCKER_CONFIG_DIR);\n        existingConfig = {};\n      }\n      merge(existingConfig, defaultConfig);\n      if (this.cfg?.containerEngine.name === ContainerEngine.CONTAINERD) {\n        existingConfig = BackendHelper.ensureDockerAuth(existingConfig);\n      }\n      await this.writeFile(ROOT_DOCKER_CONFIG_PATH, jsonStringifyWithWhiteSpace(existingConfig), 0o644);\n    } catch (err: any) {\n      console.log('Error trying to create/update docker credential files:', err);\n    }\n  }\n\n  async stop(): Promise<void> {\n    // When we manually call stop, the subprocess will terminate, which will\n    // cause stop to get called again.  Prevent the reentrancy.\n    // If we're in the middle of starting, also ignore the call to stop (from\n    // the process terminating), as we do not want to shut down the VM in that\n    // case.\n\n    if (this.currentAction !== Action.NONE) {\n      return;\n    }\n    this.currentAction = Action.STOPPING;\n    this.#containerEngineClient = undefined;\n\n    await this.progressTracker.action('Stopping services', 10, async() => {\n      try {\n        await this.setState(State.STOPPING);\n\n        const status = await this.status;\n\n        if (defined(status) && status.status === 'Running') {\n          if (this.cfg?.kubernetes.enabled) {\n            try {\n              await this.execCommand({ root: true, expectFailure: true }, '/sbin/rc-service', '--ifstarted', 'k3s', 'stop');\n            } catch (ex) {\n              console.error('Failed to stop k3s while stopping services: ', ex);\n            }\n          }\n          await this.execCommand({ root: true }, '/sbin/rc-service', '--ifstarted', 'buildkitd', 'stop');\n          await this.execCommand({ root: true }, '/sbin/rc-service', '--ifstarted', 'docker', 'stop');\n          await this.execCommand({ root: true }, '/sbin/rc-service', '--ifstarted', 'containerd', 'stop');\n          await this.execCommand({ root: true }, '/sbin/rc-service', '--ifstarted', 'rd-openresty', 'stop');\n          await this.execCommand({ root: true }, '/sbin/fstrim', '/mnt/data');\n\n          // TODO: Remove try/catch once https://github.com/lima-vm/lima/issues/1381 is fixed\n          // The first time a new VM running on VZ is stopped, it dies with a \"panic\".\n          try {\n            await this.lima('stop', MACHINE_NAME);\n          } catch (ex) {\n            if (status.vmType === VMType.VZ) {\n              console.log(`limactl stop failed with ${ ex }`);\n            } else {\n              throw ex;\n            }\n          }\n          await dockerDirManager.clearDockerContext();\n        }\n        await this.setState(State.STOPPED);\n      } catch (ex) {\n        await this.setState(State.ERROR);\n        throw ex;\n      } finally {\n        this.currentAction = Action.NONE;\n      }\n    });\n  }\n\n  async del(): Promise<void> {\n    try {\n      if (await this.isRegistered) {\n        await this.progressTracker.action(\n          'Deleting virtual machine',\n          10,\n          this.lima('delete', '--force', MACHINE_NAME));\n      }\n    } catch (ex) {\n      await this.setState(State.ERROR);\n      throw ex;\n    }\n\n    this.cfg = undefined;\n  }\n\n  async reset(config: BackendSettings): Promise<void> {\n    await this.progressTracker.action('Resetting Kubernetes', 5, async() => {\n      await this.stop();\n      // Start the VM, so that we can delete files.\n      await this.startVM();\n      await this.kubeBackend.reset();\n      await this.start(config);\n    });\n  }\n\n  async handleSettingsUpdate(_: BackendSettings): Promise<void> {}\n\n  async requiresRestartReasons(cfg: RecursivePartial<BackendSettings>): Promise<RestartReasons> {\n    const GiB = 1024 * 1024 * 1024;\n    const limaConfig = await this.getLimaConfig();\n    const reasons: RestartReasons = {};\n\n    if (!this.cfg) {\n      return reasons; // No need to restart if nothing exists\n    }\n    Object.assign(reasons, this.kubeBackend.k3sHelper.requiresRestartReasons(this.cfg, cfg, {\n      'experimental.virtualMachine.mount.9p.cacheMode':       undefined,\n      'experimental.virtualMachine.mount.9p.msizeInKib':      undefined,\n      'experimental.virtualMachine.mount.9p.protocolVersion': undefined,\n      'experimental.virtualMachine.mount.9p.securityModel':   undefined,\n      'experimental.virtualMachine.sshPortForwarder':         undefined,\n      'virtualMachine.mount.type':                            undefined,\n      'virtualMachine.type':                                  undefined,\n      'virtualMachine.useRosetta':                            undefined,\n    }));\n    if (limaConfig) {\n      Object.assign(reasons, await this.kubeBackend.requiresRestartReasons(this.cfg, cfg, {\n        'experimental.virtualMachine.diskSize': { current: limaConfig.disk ?? '100GiB' },\n        'virtualMachine.memoryInGB':            { current: (limaConfig.memory ?? 4 * GiB) / GiB },\n        'virtualMachine.numberCPUs':            { current: limaConfig.cpus ?? 2 },\n      }));\n    }\n\n    return reasons;\n  }\n\n  async getFailureDetails(exception: any): Promise<FailureDetails> {\n    const logfile = console.path;\n    const logLines = (await fs.promises.readFile(logfile, 'utf-8')).split('\\n').slice(-10);\n\n    return {\n      lastCommand:        exception[childProcess.ErrorCommand],\n      lastCommandComment: getProgressErrorDescription(exception) ?? 'Unknown',\n      lastLogLines:       logLines,\n    };\n  }\n\n  // #region Events\n  eventNames(): (keyof BackendEvents)[] {\n    return super.eventNames() as (keyof BackendEvents)[];\n  }\n\n  listeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.listeners(event) as BackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.rawListeners(event) as BackendEvents[eventName][];\n  }\n  // #endregion\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/mock.ts",
    "content": "import events from 'events';\nimport fs from 'fs';\nimport os from 'os';\nimport util from 'util';\n\nimport semver from 'semver';\n\nimport {\n  BackendEvents, BackendSettings, execOptions, RestartReasons, State, VMExecutor,\n} from './backend';\nimport {\n  ContainerBasicOptions,\n  ContainerComposeExecOptions,\n  ContainerComposeOptions,\n  ContainerComposePortOptions,\n  ContainerEngineClient,\n  ContainerRunClientOptions,\n  ContainerRunOptions,\n  ContainerStopOptions,\n  ReadableProcess,\n  WritableReadableProcess,\n} from './containerClient';\nimport { KubernetesBackend, KubernetesBackendEvents, KubernetesError } from './k8s';\nimport ProgressTracker from './progressTracker';\n\nimport K3sHelper from '@pkg/backend/k3sHelper';\nimport { Settings } from '@pkg/config/settings';\nimport { ChildProcess } from '@pkg/utils/childProcess';\nimport Logging, { Log } from '@pkg/utils/logging';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst console = Logging.mock;\n\nexport default class MockBackend extends events.EventEmitter implements VMExecutor {\n  readonly kubeBackend: KubernetesBackend = new MockKubernetesBackend();\n  readonly executor = this;\n  readonly backend = 'mock';\n  cfg:                  BackendSettings | undefined;\n  state:                State = State.STOPPED;\n  readonly cpus = Promise.resolve(1);\n  readonly memory = Promise.resolve(1);\n  progress = { current: 0, max: 0 };\n  readonly progressTracker = new ProgressTracker((progress) => {\n    this.progress = progress;\n    this.emit('progress');\n  });\n\n  debug = false;\n\n  containerEngineClient = new MockContainerEngineClient();\n\n  getBackendInvalidReason(): Promise<KubernetesError | null> {\n    return Promise.resolve(null);\n  }\n\n  protected setState(state: State) {\n    this.state = state;\n    this.emit('state-changed', state);\n  }\n\n  async start(config: Settings): Promise<void> {\n    if ([State.DISABLED, State.STARTING, State.STARTED].includes(this.state)) {\n      await this.stop();\n    }\n    console.log('Starting mock backend...');\n    this.setState(State.STARTING);\n    this.cfg = config;\n    for (let i = 0; i < 10; i++) {\n      this.progressTracker.numeric('Starting mock backend', i, 10);\n      await util.promisify(setTimeout)(1_000);\n    }\n    this.progressTracker.numeric('Starting mock backend', 10, 10);\n    await this.kubeBackend.start(config, new semver.SemVer('1.0.0'));\n    this.setState(State.STARTED);\n    console.log('Mock backend started');\n  }\n\n  async stop(): Promise<void> {\n    console.log('Stopping mock backend...');\n    this.setState(State.STOPPING);\n    await this.progressTracker.action('Stopping mock backend', 0,\n      util.promisify(setTimeout)(1_000));\n    this.setState(State.STOPPED);\n    console.log('Mock backend stopped.');\n  }\n\n  async del(): Promise<void> {\n    console.log('Deleting mock backend...');\n    await this.stop();\n  }\n\n  reset(config: Settings): Promise<void> {\n    return Promise.resolve();\n  }\n\n  ipAddress = Promise.resolve('192.0.2.1');\n\n  getFailureDetails() {\n    return Promise.resolve({\n      lastCommandComment: 'Not implemented',\n      lastLogLines:       [],\n    });\n  }\n\n  lastCommandComment = '';\n\n  noModalDialogs = true;\n\n  async handleSettingsUpdate(_: BackendSettings): Promise<void> {}\n\n  requiresRestartReasons(config: RecursivePartial<BackendSettings>): Promise<RestartReasons> {\n    if (!this.cfg) {\n      return Promise.resolve({});\n    }\n\n    return this.kubeBackend.requiresRestartReasons(this.cfg, config);\n  }\n\n  listIntegrations(): Promise<Record<string, string | boolean>> {\n    if (os.platform() !== 'win32') {\n      throw new Error('This is only expected on Windows');\n    }\n\n    return Promise.resolve({\n      alpha: true,\n      beta:  false,\n      gamma: 'some error',\n    });\n  }\n\n  // #region VMExecutor\n  execCommand(...command: string[]): Promise<void>;\n  execCommand(options: execOptions, ...command: string[]): Promise<void>;\n  execCommand(options: execOptions & { capture: true }, ...command: string[]): Promise<string>;\n  execCommand(optionsOrArg: execOptions | string, ...command: string[]): Promise<void | string> {\n    const options: execOptions & { capture?: boolean } = typeof (optionsOrArg) === 'string' ? {} : optionsOrArg;\n    const args = (typeof (optionsOrArg) === 'string' ? [optionsOrArg] : []).concat(command);\n\n    if (options.capture) {\n      return Promise.resolve(`Mock not executing ${ args.join(' ') }`);\n    }\n\n    return Promise.resolve();\n  }\n\n  spawn(...command: string[]): ChildProcess;\n  spawn(options: execOptions, ...command: string[]): ChildProcess;\n  spawn(optionsOrCommand: string | execOptions, ...command: string[]): ChildProcess {\n    return null as unknown as ChildProcess;\n  }\n\n  readFile(filePath: string, options: { encoding?: BufferEncoding } = {}): Promise<string> {\n    return Promise.reject('MockBackend#readFile() not implemented');\n  }\n\n  writeFile(filePath: string, fileContents: string, permissions: fs.Mode = 0o644): Promise<void> {\n    return Promise.resolve();\n  }\n\n  copyFileIn(hostPath: string, vmPath: string): Promise<void> {\n    return Promise.reject('MockBackend#copyFileIn() not implemented');\n  }\n\n  copyFileOut(vmPath: string, hostPath: string): Promise<void> {\n    return Promise.reject('MockBackend#copyFileOut() not implemented');\n  }\n\n  // #endregion\n\n  // #region Events\n  eventNames(): (keyof BackendEvents)[] {\n    return super.eventNames() as (keyof BackendEvents)[];\n  }\n\n  listeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.listeners(event) as BackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.rawListeners(event) as BackendEvents[eventName][];\n  }\n  // #endregion\n}\n\nclass MockKubernetesBackend extends events.EventEmitter implements KubernetesBackend {\n  readonly availableVersions = Promise.resolve([]);\n  version = '';\n  desiredPort = 9443;\n\n  readonly k3sHelper = new K3sHelper('x86_64');\n\n  cachedVersionsOnly(): Promise<boolean> {\n    return Promise.resolve(false);\n  }\n\n  listServices() {\n    return [];\n  }\n\n  forwardPort(namespace: string, service: string, k8sPort: number | string, hostPort: number): Promise<number | undefined> {\n    return Promise.resolve(12345);\n  }\n\n  cancelForward(namespace: string, service: string, k8sPort: number | string): Promise<void> {\n    return Promise.resolve();\n  }\n\n  download() {\n    return Promise.resolve([undefined, false] as const);\n  }\n\n  deleteIncompatibleData() {\n    return Promise.resolve();\n  }\n\n  install() {\n    return Promise.resolve();\n  }\n\n  start() {\n    return Promise.resolve();\n  }\n\n  stop() {\n    return Promise.resolve();\n  }\n\n  cleanup() {\n    return Promise.resolve();\n  }\n\n  reset() {\n    return Promise.resolve();\n  }\n\n  requiresRestartReasons() {\n    return Promise.resolve({});\n  }\n\n  // #region Events\n  eventNames(): (keyof KubernetesBackendEvents)[] {\n    return super.eventNames() as (keyof KubernetesBackendEvents)[];\n  }\n\n  listeners<eventName extends keyof KubernetesBackendEvents>(\n    event: eventName,\n  ): KubernetesBackendEvents[eventName][] {\n    return super.listeners(event) as KubernetesBackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof KubernetesBackendEvents>(\n    event: eventName,\n  ): KubernetesBackendEvents[eventName][] {\n    return super.rawListeners(event) as KubernetesBackendEvents[eventName][];\n  }\n  // #endregion\n}\n\nclass MockContainerEngineClient implements ContainerEngineClient {\n  waitForReady(): Promise<void> {\n    return Promise.resolve();\n  }\n\n  readFile(imageID: string, filePath: string, options?: { encoding?: BufferEncoding; namespace?: string; }): Promise<string> {\n    throw new Error('Method not implemented.');\n  }\n\n  copyFile(imageID: string, sourcePath: string, destinationDir: string, options?: { namespace?: string; }): Promise<void> {\n    throw new Error('Method not implemented.');\n  }\n\n  getTags(imageName: string, options?: ContainerBasicOptions): Promise<Set<string>> {\n    throw new Error('Method not implemented.');\n  }\n\n  run(imageID: string, options?: ContainerRunOptions): Promise<string> {\n    throw new Error('Method not implemented.');\n  }\n\n  stop(container: string, options?: ContainerStopOptions): Promise<void> {\n    throw new Error('Method not implemented.');\n  }\n\n  composeUp(options: ContainerComposeOptions): Promise<void> {\n    throw new Error('Method not implemented.');\n  }\n\n  composeDown(options?: ContainerComposeOptions): Promise<void> {\n    throw new Error('Method not implemented.');\n  }\n\n  composeExec(options: ContainerComposeExecOptions): Promise<ReadableProcess> {\n    throw new Error('Method not implemented.');\n  }\n\n  composePort(options: ContainerComposePortOptions): Promise<string> {\n    throw new Error('Method not implemented.');\n  }\n\n  runClient(args: string[], stdio?: 'ignore', options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: Log, options?: ContainerRunClientOptions): Promise<Record<string, never>>;\n  runClient(args: string[], stdio: 'pipe', options?: ContainerRunClientOptions): Promise<{ stdout: string; stderr: string; }>;\n  runClient(args: string[], stdio: 'stream', options?: ContainerRunClientOptions): ReadableProcess;\n  runClient(args: string[], stdio: 'interactive', options?: ContainerRunClientOptions): WritableReadableProcess;\n  runClient(args: string[], stdio?: unknown, options?: ContainerRunClientOptions): unknown {\n    return Promise.resolve({ stdout: '', stderr: '' });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/mock_screenshots.ts",
    "content": "import semver from 'semver';\n\nimport { BackendSettings } from '@pkg/backend/backend';\nimport { KubeClient, ServiceEntry } from '@pkg/backend/kube/client';\nimport LimaKubernetesBackend from '@pkg/backend/kube/lima';\nimport WSLKubernetesBackend from '@pkg/backend/kube/wsl';\n\nexport class LimaKubernetesBackendMock extends LimaKubernetesBackend {\n  start(config_: BackendSettings, kubernetesVersion: semver.SemVer): Promise<void> {\n    return super.start(config_, kubernetesVersion, () => new KubeClientMock());\n  }\n}\n\nexport class WSLKubernetesBackendMock extends WSLKubernetesBackend {\n  start(config_: BackendSettings, kubernetesVersion: semver.SemVer): Promise<void> {\n    return super.start(config_, kubernetesVersion, () => new KubeClientMock());\n  }\n}\n\nclass KubeClientMock extends KubeClient {\n  listServices(namespace: string | undefined = undefined): ServiceEntry[] {\n    return [{\n      namespace:  'default',\n      name:       'nginx',\n      portName:   'http',\n      port:       8080,\n      listenPort: 30001,\n    }, {\n      namespace: 'default',\n      name:      'wordpress',\n      portName:  'http',\n      port:      8080,\n    }, {\n      namespace: 'default',\n      name:      'wordpress',\n      portName:  'https',\n      port:      443,\n    }];\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/progressTracker.ts",
    "content": "import { BackendProgress } from './backend';\n\nimport { Log } from '@pkg/utils/logging';\n\nconst ErrorDescription = Symbol('progressTracker.description');\n\nexport function getProgressErrorDescription(e: any) {\n  return e[ErrorDescription] as string | undefined;\n}\n\n/**\n * ProgressTracker is used to track the progress of multiple parallel actions.\n * It invokes a callback that takes a progress object as input when one of those\n * actions comes to a close. An \"action\" is effectively a Promise with some\n * associated metadata.\n *\n * Additionally, a \"numeric\" progress object can be set on ProgressTracker.\n * This takes precedence over any other progress object that may correspond\n * to an action. This can be useful for things like summarizing the overall\n * progress of all actions configured on the ProgressTracker.\n */\nexport default class ProgressTracker {\n  /**\n   * @param notify The callback to invoke on progress change.\n   */\n  constructor(notify: (progress: BackendProgress) => void, log?: Log) {\n    this.notify = notify;\n    this.log = log;\n  }\n\n  /**\n   * A function that will be called when there is any change in the\n   * state of progress.\n   */\n  protected notify: (progress: BackendProgress) => void;\n\n  /**\n   * Optional logger to track state changes.   We will only emit debug output.\n   */\n  protected log?: Log;\n\n  /**\n   * A progress object that is preferred over progress objects that\n   * correspond to actions when passing one to .notify. Can be thought\n   * of as an action without any associated Promise and with infinitely\n   * high priority.\n   */\n  protected numericProgress?: BackendProgress;\n\n  /**\n   * A list of pending actions. The currently running action with\n   * the highest priority will be passed to this.notify.\n   */\n  protected actionProgress: { priority: number, id: number, progress: BackendProgress }[] = [];\n\n  /**\n   * Provides the ID of the next action.\n   */\n  protected nextActionID = 0;\n\n  /**\n   * Set the progress to a numeric value.  Numeric progress is always shown in\n   * preference to other progress.  There may only be one active numeric\n   * progress at a time.\n   * @param description Descriptive text.\n   * @param current The current numeric progress\n   * @param max Maximum possible numeric prog\n   */\n  numeric(description: string, current: number, max: number) {\n    if (current < max) {\n      this.numericProgress = {\n        current, max, description, transitionTime: new Date(),\n      };\n    } else {\n      this.numericProgress = undefined;\n    }\n    this.update();\n  }\n\n  /**\n   * Register an action.\n   * @param description Descriptive text for the action, to be shown to the user.\n   * @param priority Only the action with the largest priority will be shown among concurrent actions.\n   * @returns A promise that will be resolved when the passed-in promise resolves.\n   */\n  action<T>(description: string, priority: number, promise: Promise<T>): Promise<T>;\n  action<T>(description: string, priority: number, fn: () => Promise<T>): Promise<T>;\n  action<T>(description: string, priority: number, v: Promise<T> | (() => Promise<T>)) {\n    const id = this.nextActionID;\n\n    this.nextActionID++;\n    this.actionProgress.push({\n      priority,\n      id,\n      progress: {\n        current: 0, max: -1, description, transitionTime: new Date(),\n      },\n    });\n    this.update();\n    this.log?.debug(`Progress: started ${ description }`);\n\n    const promise = (v instanceof Promise) ? v : v();\n\n    return new Promise<T>((resolve, reject) => {\n      promise.then((val) => {\n        this.actionProgress = this.actionProgress.filter(p => p.id !== id);\n        this.update();\n        this.log?.debug(`Progress: finished ${ description }`);\n        resolve(val);\n      }).catch((ex) => {\n        this.actionProgress = this.actionProgress.filter(p => p.id !== id);\n        this.update();\n        this.log?.debug(`Progress: errored ${ description }: ${ ex?.ErrorDescription ?? ex }`);\n        if (!(ErrorDescription in ex)) {\n          Object.defineProperty(\n            ex,\n            ErrorDescription,\n            {\n              enumerable: false,\n              value:      description,\n            });\n        }\n        reject(ex);\n      });\n    });\n  }\n\n  /**\n   * Invoke this.notify with the highest-priority progress object.\n   */\n  protected update() {\n    if (this.numericProgress) {\n      this.notify(this.numericProgress);\n\n      return;\n    }\n    if (this.actionProgress.length < 1) {\n      // No action progress either; no active progress at all.\n      this.notify({ current: 1, max: 1 });\n\n      return;\n    }\n\n    const { progress } = this.actionProgress.reduce((a, b) => a.priority > b.priority ? a : b);\n\n    this.notify(progress);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/steve.ts",
    "content": "import { ChildProcess, spawn } from 'child_process';\nimport net from 'net';\nimport os from 'os';\nimport path from 'path';\nimport { setTimeout } from 'timers/promises';\n\nimport K3sHelper from '@pkg/backend/k3sHelper';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst STEVE_PORT = 9443;\n\nconst console = Logging.steve;\n\n/**\n * @description Singleton that manages the lifecycle of the Steve API\n */\nexport class Steve {\n  private static instance: Steve;\n  private process!:        ChildProcess;\n\n  private isRunning: boolean;\n\n  private constructor() {\n    this.isRunning = false;\n  }\n\n  /**\n   * @description Checks for an existing instance of Steve. If one does not\n   * exist, instantiate a new one.\n   */\n  public static getInstance(): Steve {\n    if (!Steve.instance) {\n      Steve.instance = new Steve();\n    }\n\n    return Steve.instance;\n  }\n\n  /**\n   * @description Starts the Steve API if one is not already running.\n   * Returns only after Steve is ready to accept connections.\n   */\n  public async start() {\n    const { pid } = this.process || { };\n\n    if (this.isRunning && pid) {\n      console.debug(`Steve is already running with pid: ${ pid }`);\n\n      return;\n    }\n\n    const osSpecificName = /^win/i.test(os.platform()) ? 'steve.exe' : 'steve';\n    const stevePath = path.join(paths.resources, os.platform(), 'internal', osSpecificName);\n    const env = Object.assign({}, process.env);\n\n    try {\n      env.KUBECONFIG = await K3sHelper.findKubeConfigToUpdate('rancher-desktop');\n    } catch {\n      // do nothing\n    }\n    this.process = spawn(\n      stevePath,\n      [\n        '--context',\n        'rancher-desktop',\n        '--ui-path',\n        path.join(paths.resources, 'rancher-dashboard'),\n        '--offline',\n        'true',\n      ],\n      { env },\n    );\n\n    const { stdout, stderr } = this.process;\n\n    if (!stdout || !stderr) {\n      console.error('Unable to get child process...');\n\n      return;\n    }\n\n    stdout.on('data', (data: any) => {\n      console.log(`stdout: ${ data }`);\n    });\n\n    stderr.on('data', (data: any) => {\n      console.error(`stderr: ${ data }`);\n    });\n\n    this.process.on('close', (code: any) => {\n      console.log(`child process exited with code ${ code }`);\n      this.isRunning = false;\n    });\n\n    try {\n      await new Promise<void>((resolve, reject) => {\n        this.process.once('spawn', () => {\n          this.isRunning = true;\n          console.debug(`Spawned child pid: ${ this.process.pid }`);\n          resolve();\n        });\n        this.process.once('error', (err) => {\n          reject(new Error(`Failed to spawn Steve: ${ err.message }`, { cause: err }));\n        });\n      });\n\n      await this.waitForReady();\n    } catch (ex) {\n      console.error(ex);\n    }\n  }\n\n  /**\n   * Wait for Steve to be ready to accept connections.\n   */\n  private async waitForReady(): Promise<void> {\n    const maxAttempts = 60;\n    const delayMs = 500;\n\n    for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n      if (!this.isRunning) {\n        throw new Error('Steve process exited before becoming ready');\n      }\n\n      if (await this.isPortReady()) {\n        console.debug(`Steve is ready after ${ attempt } attempt(s)`);\n\n        return;\n      }\n\n      await setTimeout(delayMs);\n    }\n\n    throw new Error(`Steve did not become ready after ${ maxAttempts * delayMs / 1000 } seconds`);\n  }\n\n  /**\n   * Check if Steve is accepting connections on its port.\n   */\n  private isPortReady(): Promise<boolean> {\n    return new Promise((resolve) => {\n      const socket = new net.Socket();\n\n      socket.setTimeout(1000);\n      socket.once('connect', () => {\n        socket.destroy();\n        resolve(true);\n      });\n      socket.once('error', () => {\n        socket.destroy();\n        resolve(false);\n      });\n      socket.once('timeout', () => {\n        socket.destroy();\n        resolve(false);\n      });\n      socket.connect(STEVE_PORT, '127.0.0.1');\n    });\n  }\n\n  /**\n   * Stops the Steve API.\n   */\n  public stop() {\n    if (!this.isRunning) {\n      return;\n    }\n\n    this.process.kill('SIGINT');\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/backend/wsl.ts",
    "content": "// Kubernetes backend for Windows, based on WSL2 + k3s\n\nimport events from 'events';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\nimport util from 'util';\n\nimport _ from 'lodash';\nimport * as reg from 'native-reg';\nimport semver from 'semver';\nimport tar from 'tar-stream';\n\nimport {\n  BackendError,\n  BackendEvents,\n  BackendProgress,\n  BackendSettings,\n  execOptions,\n  FailureDetails,\n  RestartReasons,\n  State,\n  VMBackend,\n  VMExecutor,\n} from './backend';\nimport BackendHelper from './backendHelper';\nimport { ContainerEngineClient, MobyClient, NerdctlClient } from './containerClient';\nimport ProgressTracker, { getProgressErrorDescription } from './progressTracker';\n\nimport DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml';\nimport FLANNEL_CONFLIST from '@pkg/assets/scripts/10-flannel.conflist';\nimport SERVICE_BUILDKITD_CONF from '@pkg/assets/scripts/buildkit.confd';\nimport SERVICE_BUILDKITD_INIT from '@pkg/assets/scripts/buildkit.initd';\nimport CONFIGURE_IMAGE_ALLOW_LIST from '@pkg/assets/scripts/configure-allowed-images';\nimport DOCKER_CREDENTIAL_SCRIPT from '@pkg/assets/scripts/docker-credential-rancher-desktop';\nimport INSTALL_WSL_HELPERS_SCRIPT from '@pkg/assets/scripts/install-wsl-helpers';\nimport LOGROTATE_K3S_SCRIPT from '@pkg/assets/scripts/logrotate-k3s';\nimport LOGROTATE_OPENRESTY_SCRIPT from '@pkg/assets/scripts/logrotate-openresty';\nimport SERVICE_SCRIPT_MOPROXY from '@pkg/assets/scripts/moproxy.initd';\nimport NERDCTL from '@pkg/assets/scripts/nerdctl';\nimport NGINX_CONF from '@pkg/assets/scripts/nginx.conf';\nimport SERVICE_GUEST_AGENT_INIT from '@pkg/assets/scripts/rancher-desktop-guestagent.initd';\nimport SERVICE_SCRIPT_CRI_DOCKERD from '@pkg/assets/scripts/service-cri-dockerd.initd';\nimport SERVICE_SCRIPT_K3S from '@pkg/assets/scripts/service-k3s.initd';\nimport SERVICE_SCRIPT_DOCKERD from '@pkg/assets/scripts/service-wsl-dockerd.initd';\nimport SCRIPT_DATA_WSL_CONF from '@pkg/assets/scripts/wsl-data.conf';\nimport WSL_EXEC from '@pkg/assets/scripts/wsl-exec';\nimport WSL_INIT_SCRIPT from '@pkg/assets/scripts/wsl-init';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport { getServerCredentialsPath, ServerState } from '@pkg/main/credentialServer/httpCredentialHelperServer';\nimport mainEvents from '@pkg/main/mainEvents';\nimport BackgroundProcess from '@pkg/utils/backgroundProcess';\nimport * as childProcess from '@pkg/utils/childProcess';\nimport clone from '@pkg/utils/clone';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { defined, RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { KubernetesBackend } from './k8s';\n\n/* eslint @typescript-eslint/switch-exhaustiveness-check: \"error\" */\n\nconst console = Logging.wsl;\nconst INSTANCE_NAME = 'rancher-desktop';\nconst DATA_INSTANCE_NAME = 'rancher-desktop-data';\n\nconst ETC_RANCHER_DESKTOP_DIR = '/etc/rancher/desktop';\nconst CREDENTIAL_FORWARDER_SETTINGS_PATH = `${ ETC_RANCHER_DESKTOP_DIR }/credfwd`;\nconst DOCKER_CREDENTIAL_PATH = '/usr/local/bin/docker-credential-rancher-desktop';\nconst ROOT_DOCKER_CONFIG_DIR = '/root/.docker';\nconst ROOT_DOCKER_CONFIG_PATH = `${ ROOT_DOCKER_CONFIG_DIR }/config.json`;\n/** Number of times to retry converting a path between WSL & Windows. */\nconst WSL_PATH_CONVERT_RETRIES = 10;\n\n/**\n * Enumeration for tracking what operation the backend is undergoing.\n */\nexport enum Action {\n  NONE = 'idle',\n  STARTING = 'starting',\n  STOPPING = 'stopping',\n}\n\n/** The version of the WSL distro we expect. */\nconst DISTRO_VERSION = DEPENDENCY_VERSIONS.WSLDistro;\n\n/**\n * The list of directories that are in the data distribution (persisted across\n * version upgrades).\n */\nconst DISTRO_DATA_DIRS = [\n  '/etc/rancher',\n  '/var/lib',\n];\n\ntype wslExecOptions = execOptions & {\n  /** Output encoding; defaults to utf16le. */\n  encoding?: BufferEncoding;\n  /** The distribution to execute within. */\n  distro?:   string;\n};\n\nexport default class WSLBackend extends events.EventEmitter implements VMBackend, VMExecutor {\n  constructor(kubeFactory: (backend: WSLBackend) => KubernetesBackend) {\n    super();\n    this.progressTracker = new ProgressTracker((progress) => {\n      this.progress = progress;\n      this.emit('progress');\n    }, console);\n\n    this.hostSwitchProcess = new BackgroundProcess('host-switch.exe', {\n      spawn: async() => {\n        const exe = path.join(paths.resources, 'win32', 'internal', 'host-switch.exe');\n        const stream = await Logging['host-switch'].fdStream;\n        const args: string[] = [];\n\n        if (this.cfg?.kubernetes.enabled) {\n          const k8sPort = 6443;\n          const eth0IP = '192.168.127.2';\n          const k8sPortForwarding = `127.0.0.1:${ k8sPort }=${ eth0IP }:${ k8sPort }`;\n\n          args.push('--port-forward', k8sPortForwarding);\n        }\n\n        return childProcess.spawn(exe, args, {\n          stdio:       ['ignore', stream, stream],\n          windowsHide: true,\n        });\n      },\n      shouldRun: () => Promise.resolve([State.STARTING, State.STARTED, State.DISABLED].includes(this.state)),\n    });\n\n    this.kubeBackend = kubeFactory(this);\n  }\n\n  protected get distroFile() {\n    return path.join(paths.resources, os.platform(), `distro-${ DISTRO_VERSION }.tar`);\n  }\n\n  /** The current config state. */\n  protected cfg: BackendSettings | undefined;\n\n  /** Indicates whether the current installation is an Admin Install. */\n  #isAdminInstall: Promise<boolean> | undefined;\n\n  protected getIsAdminInstall(): Promise<boolean> {\n    this.#isAdminInstall ??= new Promise((resolve) => {\n      let key;\n\n      try {\n        key = reg.openKey(reg.HKLM, 'SOFTWARE', reg.Access.READ);\n\n        if (key) {\n          const parsedValue = reg.getValue(key, 'SUSE\\\\RancherDesktop', 'AdminInstall');\n          const isAdmin = parsedValue !== null;\n\n          return resolve(isAdmin);\n        } else {\n          console.debug('Failed to open registry key: HKEY_LOCAL_MACHINE\\SOFTWARE');\n        }\n      } catch (error) {\n        console.error(`Error accessing registry: ${ error }`);\n      } finally {\n        reg.closeKey(key);\n      }\n\n      return resolve(false);\n    });\n\n    return this.#isAdminInstall;\n  }\n\n  /**\n   * Reference to the _init_ process in WSL.  All other processes should be\n   * children of this one.  Note that this is busybox init, running in a custom\n   * mount & pid namespace.\n   */\n  protected process: childProcess.ChildProcess | null = null;\n\n  /**\n   * Windows-side process for the Rancher Desktop Networking,\n   * it is used to provide DNS, DHCP and Port Forwarding\n   * to the vm-switch that is running in the WSL VM.\n   */\n  protected hostSwitchProcess: BackgroundProcess;\n\n  readonly kubeBackend:   KubernetesBackend;\n  readonly executor = this;\n  #containerEngineClient: ContainerEngineClient | undefined;\n\n  get containerEngineClient() {\n    if (this.#containerEngineClient) {\n      return this.#containerEngineClient;\n    }\n\n    throw new Error('Invalid state, no container engine client available.');\n  }\n\n  /** A transient property that prevents prompting via modal UI elements. */\n  #noModalDialogs = false;\n\n  get noModalDialogs() {\n    return this.#noModalDialogs;\n  }\n\n  set noModalDialogs(value: boolean) {\n    this.#noModalDialogs = value;\n  }\n\n  /**\n   * The current operation underway; used to avoid responding to state changes\n   * when we're in the process of doing a different one.\n   */\n  currentAction: Action = Action.NONE;\n\n  /** Whether debug mode is enabled */\n  debug = false;\n\n  get backend(): 'wsl' {\n    return 'wsl';\n  }\n\n  writeSetting(changed: RecursivePartial<BackendSettings>) {\n    if (changed) {\n      mainEvents.emit('settings-write', changed);\n    }\n    this.cfg = _.merge({}, this.cfg, changed);\n  }\n\n  /** The current user-visible state of the backend. */\n  protected internalState: State = State.STOPPED;\n  get state() {\n    return this.internalState;\n  }\n\n  protected async setState(state: State) {\n    this.internalState = state;\n    this.emit('state-changed', this.state);\n    switch (this.state) {\n    case State.STOPPING:\n    case State.STOPPED:\n    case State.ERROR:\n    case State.DISABLED:\n      await this.kubeBackend.stop();\n      break;\n    case State.STARTING:\n    case State.STARTED:\n      /* nothing */\n    }\n  }\n\n  progressTracker: ProgressTracker;\n\n  progress: BackendProgress = { current: 0, max: 0 };\n\n  get cpus(): Promise<number> {\n    // This doesn't make sense for WSL2, since that's a global configuration.\n    return Promise.resolve(0);\n  }\n\n  get memory(): Promise<number> {\n    // This doesn't make sense for WSL2, since that's a global configuration.\n    return Promise.resolve(0);\n  }\n\n  /**\n   * List the registered WSL2 distributions.\n   */\n  protected async registeredDistros({ runningOnly = false } = {}): Promise<string[]> {\n    const args = ['--list', '--quiet', runningOnly ? '--running' : undefined];\n    const distros = (await this.execWSL({ capture: true }, ...args.filter(defined)))\n      .split(/\\r?\\n/g)\n      .map(x => x.trim())\n      .filter(x => x);\n\n    if (distros.length < 1) {\n      // Return early if we find no distributions in this list; listing again\n      // with verbose will fail if there are no distributions.\n      return [];\n    }\n\n    const stdout = await this.execWSL({ capture: true }, '--list', '--verbose');\n    // As wsl.exe may be localized, don't check state here.\n    const parser = /^[\\s*]+(?<name>.*?)\\s+\\w+\\s+(?<version>\\d+)\\s*$/;\n\n    const result = stdout.trim()\n      .split(/[\\r\\n]+/)\n      .slice(1) // drop the title row\n      .map(line => parser.exec(line))\n      .filter(defined)\n      .filter(result => result.groups?.version === '2')\n      .map(result => result.groups?.name)\n      .filter(defined);\n\n    return result.filter(x => distros.includes(x));\n  }\n\n  protected async isDistroRegistered({ distribution = INSTANCE_NAME, runningOnly = false } = {}): Promise<boolean> {\n    const distros = await this.registeredDistros({ runningOnly });\n\n    console.log(`Registered distributions: ${ distros }`);\n\n    return distros.includes(distribution || INSTANCE_NAME);\n  }\n\n  protected async getDistroVersion(): Promise<string> {\n    // ESLint doesn't realize we're doing inline shell scripts.\n    // eslint-disable-next-line no-template-curly-in-string\n    const script = '[ -e /etc/os-release ] && . /etc/os-release ; echo ${VERSION_ID:-0.1}';\n\n    return (await this.captureCommand('/bin/sh', '-c', script)).trim();\n  }\n\n  /**\n   * Ensure that the distribution has been installed into WSL2.\n   * Any upgrades to the distribution should be done immediately after this.\n   */\n  protected async ensureDistroRegistered(): Promise<void> {\n    if (!await this.isDistroRegistered()) {\n      await this.progressTracker.action('Registering WSL distribution', 100, async() => {\n        await fs.promises.mkdir(paths.wslDistro, { recursive: true });\n        try {\n          await this.execWSL({ capture: true },\n            '--import', INSTANCE_NAME, paths.wslDistro, this.distroFile, '--version', '2');\n        } catch (ex: any) {\n          if (!String(ex.stdout ?? '').includes('ensure virtualization is enabled')) {\n            throw ex;\n          }\n          throw new BackendError('Virtualization not supported', ex.stdout, true);\n        }\n      });\n    }\n\n    if (!await this.isDistroRegistered()) {\n      throw new Error(`Error registering WSL2 distribution`);\n    }\n\n    await this.initDataDistribution();\n  }\n\n  /**\n   * If the WSL distribution we use to hold the data doesn't exist, create it\n   * and copy the skeleton over from the active one.\n   */\n  protected async initDataDistribution() {\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-distro-'));\n\n    try {\n      if (!await this.isDistroRegistered({ distribution: DATA_INSTANCE_NAME })) {\n        await this.progressTracker.action('Initializing WSL data', 100, async() => {\n          try {\n            // Create a distro archive from the main distro.\n            // WSL seems to require a working /bin/sh for initialization.\n            const OVERRIDE_FILES = { 'etc/wsl.conf': SCRIPT_DATA_WSL_CONF };\n            const REQUIRED_FILES = [\n              '/bin/busybox', // Base tools\n              '/bin/mount', // Required for WSL startup\n              '/bin/sh', // WSL requires a working shell to initialize\n              '/lib', // Dependencies for busybox\n              '/etc/passwd', // So WSL can spawn programs as a user\n            ];\n            const archivePath = path.join(workdir, 'distro.tar');\n\n            console.log('Creating initial data distribution...');\n            // Make sure all the extra data directories exist\n            await Promise.all(DISTRO_DATA_DIRS.map((dir) => {\n              return this.execCommand('/bin/busybox', 'mkdir', '-p', dir);\n            }));\n            // Figure out what required files actually exist in the distro; they\n            // may not exist on various versions.\n            const extraFiles = (await Promise.all(REQUIRED_FILES.map(async(path) => {\n              try {\n                await this.execCommand({ expectFailure: true }, 'busybox', '[', '-e', path, ']');\n\n                return path;\n              } catch (ex) {\n                // Exception expected - the path doesn't exist\n                return undefined;\n              }\n            }))).filter(defined);\n\n            await this.execCommand('tar', '-cf', await this.wslify(archivePath),\n              '-C', '/', ...extraFiles, ...DISTRO_DATA_DIRS);\n\n            // The tar-stream package doesn't handle appends well (needs to\n            // stream to a temporary file), and busybox tar doesn't support\n            // append either.  Luckily Windows ships with a bsdtar that\n            // supports it, though it only supports short options.\n            for (const [relPath, contents] of Object.entries(OVERRIDE_FILES)) {\n              const absPath = path.join(workdir, 'tar', relPath);\n\n              await fs.promises.mkdir(path.dirname(absPath), { recursive: true });\n              await fs.promises.writeFile(absPath, contents);\n            }\n            // msys comes with its own \"tar.exe\"; ensure we use the version\n            // shipped with Windows.\n            const tarExe = path.join(process.env.SystemRoot ?? '', 'system32', 'tar.exe');\n\n            await childProcess.spawnFile(tarExe,\n              ['-r', '-f', archivePath, '-C', path.join(workdir, 'tar'), ...Object.keys(OVERRIDE_FILES)],\n              { stdio: 'pipe' });\n            await this.execCommand('tar', '-tvf', await this.wslify(archivePath));\n            await this.execWSL('--import', DATA_INSTANCE_NAME, paths.wslDistroData, archivePath, '--version', '2');\n          } catch (ex) {\n            console.log(`Error registering data distribution: ${ ex }`);\n            await this.execWSL('--unregister', DATA_INSTANCE_NAME);\n            throw ex;\n          }\n        });\n      } else {\n        console.log('data distro already registered');\n      }\n\n      await this.progressTracker.action('Updating WSL data', 100, async() => {\n        // We may have extra directories (due to upgrades); copy any new ones over.\n        const missingDirs: string[] = [];\n\n        await Promise.all(DISTRO_DATA_DIRS.map(async(dir) => {\n          try {\n            await this.execWSL({ expectFailure: true, encoding: 'utf-8' },\n              '--distribution', DATA_INSTANCE_NAME, '--exec', '/bin/busybox', '[', '!', '-d', dir, ']');\n            missingDirs.push(dir);\n          } catch (ex) {\n            // Directory exists.\n          }\n        }));\n        if (missingDirs.length > 0) {\n          // Copy the new directories into the data distribution.\n          // Note that we're not using compression, since we (kind of) don't have gzip...\n          console.log(`Data distribution missing directories ${ missingDirs }, adding...`);\n          const archivePath = await this.wslify(path.join(workdir, 'data.tar'));\n\n          await this.execCommand('tar', '-cf', archivePath, '-C', '/', ...missingDirs);\n          await this.execWSL('--distribution', DATA_INSTANCE_NAME, '--exec', '/bin/busybox', 'tar', '-xf', archivePath, '-C', '/');\n        }\n      });\n    } catch (ex) {\n      console.log('Error setting up data distribution:', ex);\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true, maxRetries: 3 });\n    }\n  }\n\n  /**\n   * Runs wsl-proxy process in the default namespace. This is to proxy\n   * other distro's traffic from default namespace into the network namespace.\n   */\n\n  protected async runWslProxy() {\n    const debug = this.debug ? 'true' : 'false';\n    const logDir = await this.wslify(paths.logs);\n    const logfile = path.posix.join(logDir, 'wsl-proxy.log');\n\n    try {\n      await this.execCommand('/usr/local/bin/wsl-proxy', `-debug=${ debug }`, `-logfile=${ logfile }`);\n    } catch (err: any) {\n      console.log('Error trying to start wsl-proxy in default namespace:', err);\n    }\n  }\n\n  /**\n   * Write out /etc/hosts in the main distribution, copying the bulk of the\n   * contents from the data distribution.\n   */\n  protected async writeHostsFile(config: BackendSettings) {\n    const virtualNetworkStaticAddr = '192.168.127.254';\n    const virtualNetworkGatewayAddr = '192.168.127.1';\n\n    await this.progressTracker.action('Updating /etc/hosts', 50, async() => {\n      const contents = await fs.promises.readFile(`\\\\\\\\wsl$\\\\${ DATA_INSTANCE_NAME }\\\\etc\\\\hosts`, 'utf-8');\n      const lines = contents.split(/\\r?\\n/g)\n        .filter(line => !line.includes('host.docker.internal'));\n      const hosts = ['host.rancher-desktop.internal', 'host.docker.internal'];\n      const extra = [\n        '# BEGIN Rancher Desktop configuration.',\n        `${ virtualNetworkStaticAddr } ${ hosts.join(' ') }`,\n        `${ virtualNetworkGatewayAddr } gateway.rancher-desktop.internal`,\n        '# END Rancher Desktop configuration.',\n      ].map(l => `${ l }\\n`).join('');\n\n      await fs.promises.writeFile(`\\\\\\\\wsl$\\\\${ INSTANCE_NAME }\\\\etc\\\\hosts`,\n        lines.join('\\n') + extra, 'utf-8');\n    });\n  }\n\n  /**\n   * Mount the data distribution over.\n   *\n   * @returns a process that ensures the mount points stay alive by preventing\n   * the distribution from being terminated due to being idle.  It should be\n   * killed once things are up.\n   */\n  protected async mountData(): Promise<childProcess.ChildProcess> {\n    const mountRoot = '/mnt/wsl/rancher-desktop/run/data';\n\n    await this.execCommand('mkdir', '-p', mountRoot);\n    // Only bind mount the root if it doesn't exist; because this is in the\n    // shared mount (/mnt/wsl/), it can persist even if all of our distribution\n    // instances terminate, as long as the WSL VM is still running.  Once that\n    // happens, it is no longer possible to unmount the bind mount...\n    // However, there's an exception: the underlying device could have gone\n    // missing (!); if that happens, we _can_ unmount it.\n    const mountInfo = await this.execWSL(\n      { capture: true, encoding: 'utf-8' },\n      '--distribution', DATA_INSTANCE_NAME, '--exec', 'busybox', 'cat', '/proc/self/mountinfo');\n    // https://www.kernel.org/doc/html/latest/filesystems/proc.html#proc-pid-mountinfo-information-about-mounts\n    // We want fields 5 \"mount point\" and 10 \"mount source\".\n    const matchRegex = new RegExp(String.raw`\n      (?<mountID>\\S+)\n      (?<parentID>\\S+)\n      (?<majorMinor>\\S+)\n      (?<root>\\S+)\n      (?<mountPoint>\\S+)\n      (?<mountOptions>\\S+)\n      (?<optionalFields>.*?)\n      -\n      (?<fsType>\\S+)\n      (?<mountSource>\\S+)\n      (?<superOptions>\\S+)\n    `.trim().replace(/\\s+/g, String.raw`\\s+`));\n    const mountFields = mountInfo.split(/\\r?\\n/).map(line => matchRegex.exec(line)).filter(defined);\n    let hasValidMount = false;\n\n    for (const mountLine of mountFields) {\n      const { mountPoint, mountSource: device } = mountLine.groups ?? {};\n\n      if (mountPoint !== mountRoot || !device) {\n        continue;\n      }\n      // Some times we can have the mount but the disk is missing.\n      // In that case we need to umount it, and the re-mount.\n      try {\n        await this.execWSL(\n          { expectFailure: true },\n          '--distribution', DATA_INSTANCE_NAME, '--exec', 'busybox', 'test', '-e', device);\n        console.debug(`Found a valid mount with ${ device }: ${ mountLine.input }`);\n        hasValidMount = true;\n      } catch (ex) {\n        // Busybox returned error, the devices doesn't exist.  Unmount.\n        console.log(`Unmounting missing device ${ device }: ${ mountLine.input }`);\n        await this.execWSL(\n          '--distribution', DATA_INSTANCE_NAME, '--exec', 'busybox', 'umount', mountRoot);\n      }\n    }\n\n    if (!hasValidMount) {\n      console.log(`Did not find a valid mount, mounting ${ mountRoot }`);\n      await this.execWSL('--distribution', DATA_INSTANCE_NAME, 'mount', '--bind', '/', mountRoot);\n    }\n    await Promise.all(DISTRO_DATA_DIRS.map(async(dir) => {\n      await this.execCommand('mkdir', '-p', dir);\n      await this.execCommand('mount', '-o', 'bind', `${ mountRoot }/${ dir.replace(/^\\/+/, '') }`, dir);\n    }));\n\n    return childProcess.spawn('wsl.exe',\n      ['--distribution', INSTANCE_NAME, '--exec', 'sh'], { windowsHide: true });\n  }\n\n  /**\n   * Convert a Windows path to a path in the WSL subsystem:\n   * - Changes \\s to /s\n   * - Figures out what the /mnt/DRIVE-LETTER path should be\n   */\n  async wslify(windowsPath: string, distro?: string): Promise<string> {\n    for (let i = 1; i <= WSL_PATH_CONVERT_RETRIES; i++) {\n      const result: string = (await this.captureCommand({ distro }, 'wslpath', '-a', '-u', windowsPath)).trimEnd();\n\n      if (result) {\n        return result;\n      }\n      console.log(`Failed to convert '${ windowsPath }' to a wsl path, retry #${ i }`);\n      await util.promisify(setTimeout)(100);\n    }\n\n    return '';\n  }\n\n  protected async killStaleProcesses() {\n    // Attempting to terminate a terminated distribution is a no-op.\n    await Promise.all([\n      this.execWSL('--terminate', INSTANCE_NAME),\n      this.execWSL('--terminate', DATA_INSTANCE_NAME),\n      this.hostSwitchProcess.stop(),\n    ]);\n  }\n\n  /**\n   * Copy a file from Windows to the WSL distribution.\n   */\n  protected async wslInstall(windowsPath: string, targetDirectory: string, targetBasename = ''): Promise<void> {\n    const wslSourcePath = await this.wslify(windowsPath);\n    const basename = path.basename(windowsPath);\n    // Don't use `path.join` or the backslashes will come back.\n    const targetFile = `${ targetDirectory }/${ targetBasename || basename }`;\n\n    console.log(`Installing ${ windowsPath } as ${ wslSourcePath } into ${ targetFile } ...`);\n    try {\n      const stdout = await this.captureCommand('cp', wslSourcePath, targetFile);\n\n      if (stdout) {\n        console.log(`cp ${ windowsPath } as ${ wslSourcePath } to ${ targetFile }: ${ stdout }`);\n      }\n    } catch (err) {\n      console.log(`Error trying to cp ${ windowsPath } as ${ wslSourcePath } to ${ targetFile }: ${ err }`);\n      throw err;\n    }\n  }\n\n  /**\n   * Read the given file in a WSL distribution\n   * @param [filePath] the path of the file to read.\n   * @param [options] Optional configuration for reading the file.\n   * @param [options.distro=INSTANCE_NAME] The distribution to read from.\n   * @param [options.encoding='utf-8'] The encoding to use for the result.\n   */\n  async readFile(filePath: string, options?: Partial<{\n    distro:   typeof INSTANCE_NAME | typeof DATA_INSTANCE_NAME,\n    encoding: BufferEncoding,\n  }>) {\n    const distro = options?.distro ?? INSTANCE_NAME;\n    const encoding = options?.encoding ?? 'utf-8';\n\n    filePath = (await this.execCommand({ distro, capture: true }, 'busybox', 'readlink', '-f', filePath)).trim();\n\n    // Run wslpath here, to ensure that WSL generates any files we need.\n    for (let i = 1; i <= WSL_PATH_CONVERT_RETRIES; ++i) {\n      const windowsPath = (await this.execCommand({\n        distro, encoding, capture: true,\n      }, '/bin/wslpath', '-w', filePath)).trim();\n\n      if (!windowsPath) {\n        // Failed to convert for some reason; try again.\n        await util.promisify(setTimeout)(100);\n        continue;\n      }\n\n      return await fs.promises.readFile(windowsPath, options?.encoding ?? 'utf-8');\n    }\n\n    throw new Error(`Failed to convert ${ filePath } to a Windows path.`);\n  }\n\n  /**\n   * Write the given contents to a given file name in the given WSL distribution.\n   * @param filePath The destination file path, in the WSL distribution.\n   * @param fileContents The contents of the file.\n   * @param [options] An object with fields .permissions=0o644 (the file permissions); and .distro=INSTANCE_NAME (WSL distribution to write to).\n   */\n  async writeFileWSL(filePath: string, fileContents: string, options?: Partial<{ permissions: fs.Mode, distro: typeof INSTANCE_NAME | typeof DATA_INSTANCE_NAME }>) {\n    const distro = options?.distro ?? INSTANCE_NAME;\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `rd-${ path.basename(filePath) }-`));\n\n    try {\n      const scriptPath = path.join(workdir, path.basename(filePath));\n      const wslScriptPath = await this.wslify(scriptPath, distro);\n\n      await fs.promises.writeFile(scriptPath, fileContents.replace(/\\r/g, ''), 'utf-8');\n      await this.execCommand({ distro }, 'busybox', 'cp', wslScriptPath, filePath);\n      await this.execCommand({ distro }, 'busybox', 'chmod', (options?.permissions ?? 0o644).toString(8), filePath);\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true, maxRetries: 3 });\n    }\n  }\n\n  /**\n   * Write the given contents to a given file name in the VM.\n   * The file will be owned by root.\n   * @param filePath The destination file path, in the VM.\n   * @param fileContents The contents of the file.\n   * @param permissions The file permissions.\n   */\n  async writeFile(filePath: string, fileContents: string, permissions: fs.Mode = 0o644) {\n    await this.writeFileWSL(filePath, fileContents, { permissions });\n  }\n\n  async copyFileIn(hostPath: string, vmPath: string): Promise<void> {\n    // Sometimes WSL has issues copying _from_ the VM.  So we instead do the\n    // copying from inside the VM.\n    await this.execCommand('/bin/cp', '-f', '-T', await this.wslify(hostPath), vmPath);\n  }\n\n  async copyFileOut(vmPath: string, hostPath: string): Promise<void> {\n    // Sometimes WSL has issues copying _from_ the VM.  So we instead do the\n    // copying from inside the VM.\n    await this.execCommand('/bin/cp', '-f', '-T', vmPath, await this.wslify(hostPath));\n  }\n\n  /**\n   * Run the given installation script.\n   * @param scriptContents The installation script contents to run (in WSL).\n   * @param scriptName An identifying label for the script's temporary directory - has no impact on functionality\n   * @param args Arguments for the script.\n   */\n  async runInstallScript(scriptContents: string, scriptName: string, ...args: string[]) {\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), `rd-${ scriptName }-`));\n\n    try {\n      const scriptPath = path.join(workdir, scriptName);\n      const wslScriptPath = await this.wslify(scriptPath);\n\n      await fs.promises.writeFile(scriptPath, scriptContents.replace(/\\r/g, ''), 'utf-8');\n      await this.execCommand('chmod', 'a+x', wslScriptPath);\n      await this.execCommand(wslScriptPath, ...args);\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true, maxRetries: 3 });\n    }\n  }\n\n  /**\n   * Install helper tools for WSL (nerdctl integration).\n   */\n  protected async installWSLHelpers() {\n    const windowsNerdctlPath = path.join(paths.resources, 'linux', 'bin', 'nerdctl-stub');\n    const nerdctlPath = await this.wslify(windowsNerdctlPath);\n\n    await this.runInstallScript(INSTALL_WSL_HELPERS_SCRIPT, 'install-wsl-helpers', nerdctlPath);\n  }\n\n  protected async installCredentialHelper() {\n    const credsPath = getServerCredentialsPath();\n\n    try {\n      const credentialServerAddr = 'host.rancher-desktop.internal:6109';\n      const stateInfo: ServerState = JSON.parse(await fs.promises.readFile(credsPath, { encoding: 'utf-8' }));\n      const escapedPassword = stateInfo.password.replace(/\\\\/g, '\\\\\\\\')\n        .replace(/'/g, \"\\\\'\");\n      // leading `$` is needed to escape single-quotes, as : $'abc\\'xyz'\n      const leadingDollarSign = stateInfo.password.includes(\"'\") ? '$' : '';\n      const fileContents = `CREDFWD_AUTH=${ leadingDollarSign }'${ stateInfo.user }:${ escapedPassword }'\n      CREDFWD_URL='http://${ credentialServerAddr }'\n      `;\n      const defaultConfig = { credsStore: 'rancher-desktop' };\n      let existingConfig: Record<string, any>;\n\n      await this.execCommand('mkdir', '-p', ETC_RANCHER_DESKTOP_DIR);\n      await this.writeFile(CREDENTIAL_FORWARDER_SETTINGS_PATH, fileContents, 0o644);\n      await this.writeFile(DOCKER_CREDENTIAL_PATH, DOCKER_CREDENTIAL_SCRIPT, 0o755);\n      try {\n        existingConfig = JSON.parse(await this.captureCommand('cat', ROOT_DOCKER_CONFIG_PATH));\n      } catch (err: any) {\n        await this.execCommand('mkdir', '-p', ROOT_DOCKER_CONFIG_DIR);\n        existingConfig = {};\n      }\n      _.merge(existingConfig, defaultConfig);\n      if (this.cfg?.containerEngine.name === ContainerEngine.CONTAINERD) {\n        existingConfig = BackendHelper.ensureDockerAuth(existingConfig);\n      }\n      await this.writeFile(ROOT_DOCKER_CONFIG_PATH, jsonStringifyWithWhiteSpace(existingConfig), 0o644);\n    } catch (err: any) {\n      console.log('Error trying to create/update docker credential files:', err);\n    }\n  }\n\n  /**\n   * Return the Linux path to the moproxy executable.\n   */\n  protected getMoproxyPath(): Promise<string> {\n    return this.wslify(path.join(paths.resources, 'linux', 'internal', 'moproxy'));\n  }\n\n  protected async writeProxySettings(proxy: BackendSettings['experimental']['virtualMachine']['proxy']): Promise<void> {\n    if (proxy.address && proxy.port) {\n      // Write to /etc/moproxy/proxy.ini\n      const protocol = proxy.address.startsWith('socks5://') ? 'socks5' : 'http';\n      const address = proxy.address.replace(/(https|http|socks5):\\/\\//g, '');\n      const contents = `[rancher-desktop-proxy]\\naddress=${ address }:${ proxy.port }\\nprotocol=${ protocol }\\n`;\n      const attributePrefix = protocol === 'socks5' ? 'socks' : 'http';\n      const username = proxy.username ? `${ attributePrefix } username=${ proxy.username }\\n` : '';\n      const password = proxy.password ? `${ attributePrefix } password=${ proxy.password }\\n` : '';\n\n      await this.writeFile(`/etc/moproxy/proxy.ini`, `${ contents }${ username }${ password }`);\n    } else {\n      await this.writeFile(`/etc/moproxy/proxy.ini`, '; no proxy defined');\n    }\n\n    await this.modifyConf('moproxy', { MOPROXY_NOPROXY: proxy.noproxy.join(',') });\n  }\n\n  /**\n   * handleUpgrade removes all the left over files that\n   * were renamed in between releases.\n   */\n  protected async handleUpgrade(files: string[]) {\n    for (const file of files) {\n      try {\n        await fs.promises.rm(file, { force: true, maxRetries: 3 });\n      } catch {\n        // ignore the err from exception, since we are\n        // removing renamed files from previous releases\n      }\n    }\n  }\n\n  protected async installGuestAgent(kubeVersion: semver.SemVer | undefined, cfg: BackendSettings | undefined) {\n    const enableKubernetes = !!kubeVersion;\n    const isAdminInstall = await this.getIsAdminInstall();\n\n    const guestAgentConfig: Record<string, string> = {\n      LOG_DIR:                  await this.wslify(paths.logs),\n      GUESTAGENT_ADMIN_INSTALL: isAdminInstall ? 'true' : 'false',\n      GUESTAGENT_KUBERNETES:    enableKubernetes ? 'true' : 'false',\n      GUESTAGENT_CONTAINERD:    cfg?.containerEngine.name === ContainerEngine.CONTAINERD ? 'true' : 'false',\n      GUESTAGENT_DOCKER:        cfg?.containerEngine.name === ContainerEngine.MOBY ? 'true' : 'false',\n      GUESTAGENT_DEBUG:         this.debug ? 'true' : 'false',\n      GUESTAGENT_K8S_SVC_ADDR:  isAdminInstall && !cfg?.kubernetes.ingress.localhostOnly ? '0.0.0.0' : '127.0.0.1',\n    };\n\n    await Promise.all([\n      this.writeFile('/etc/init.d/rancher-desktop-guestagent', SERVICE_GUEST_AGENT_INIT, 0o755),\n      this.writeConf('rancher-desktop-guestagent', guestAgentConfig),\n    ]);\n    await this.execCommand('/sbin/rc-update', 'add', 'rancher-desktop-guestagent', 'default');\n  }\n\n  /**\n   * debugArg returns the given arguments in an array if the debug flag is\n   * set, else an empty array.\n   */\n  protected debugArg(...args: string[]): string[] {\n    return this.debug ? args : [];\n  }\n\n  /**\n   * execWSL runs wsl.exe with the given arguments, redirecting all output to\n   * the log files.\n   */\n  protected async execWSL(...args: string[]): Promise<void>;\n  protected async execWSL(options: wslExecOptions, ...args: string[]): Promise<void>;\n  protected async execWSL(options: wslExecOptions & { capture: true }, ...args: string[]): Promise<string>;\n  protected async execWSL(optionsOrArg: wslExecOptions | string, ...args: string[]): Promise<void | string> {\n    let options: wslExecOptions & { capture?: boolean } = {};\n\n    if (typeof optionsOrArg === 'string') {\n      args = [optionsOrArg].concat(...args);\n    } else {\n      options = optionsOrArg;\n    }\n    try {\n      let stream = options.logStream;\n\n      if (!stream) {\n        const logFile = Logging['wsl-exec'];\n\n        // Write a duplicate log line so we can line up the log files.\n        logFile.log(`Running: wsl.exe ${ args.join(' ') }`);\n        stream = await logFile.fdStream;\n      }\n\n      // We need two separate calls so TypeScript can resolve the return values.\n      if (options.capture) {\n        console.debug(`Capturing output: wsl.exe ${ args.join(' ') }`);\n        const { stdout } = await childProcess.spawnFile('wsl.exe', args, {\n          ...options,\n          encoding: options.encoding ?? 'utf16le',\n          stdio:    ['ignore', 'pipe', stream],\n        });\n\n        return stdout;\n      }\n      console.debug(`Running: wsl.exe ${ args.join(' ') }`);\n      await childProcess.spawnFile('wsl.exe', args, {\n        ...options,\n        encoding: options.encoding ?? 'utf16le',\n        stdio:    ['ignore', stream, stream],\n      });\n    } catch (ex) {\n      if (!options.expectFailure) {\n        console.log(`WSL failed to execute wsl.exe ${ args.join(' ') }: ${ ex }`);\n      }\n      throw ex;\n    }\n  }\n\n  async execCommand(...command: string[]): Promise<void>;\n  async execCommand(options: wslExecOptions, ...command: string[]): Promise<void>;\n  async execCommand(options: wslExecOptions & { capture: true }, ...command: string[]): Promise<string>;\n  async execCommand(optionsOrArg: wslExecOptions | string, ...command: string[]): Promise<void | string> {\n    let options: wslExecOptions = {};\n    const cwdOptions: string[] = [];\n\n    if (typeof optionsOrArg === 'string') {\n      command = [optionsOrArg].concat(command);\n    } else {\n      options = optionsOrArg;\n    }\n\n    if (options.cwd) {\n      cwdOptions.push('--cd', options.cwd.toString());\n      delete options.cwd;\n    }\n\n    const expectFailure = options.expectFailure ?? false;\n\n    try {\n      // Print a slightly different message if execution fails.\n      return await this.execWSL({\n        encoding: 'utf-8', ...options, expectFailure: true,\n      }, '--distribution', options.distro ?? INSTANCE_NAME, ...cwdOptions, '--exec', ...command);\n    } catch (ex) {\n      if (!expectFailure) {\n        console.log(`WSL: executing: ${ command.join(' ') }: ${ ex }`);\n      }\n      throw ex;\n    }\n  }\n\n  spawn(...command: string[]): childProcess.ChildProcess;\n  spawn(options: execOptions, ...command: string[]): childProcess.ChildProcess;\n  spawn(optionsOrCommand: execOptions | string, ...command: string[]): childProcess.ChildProcess {\n    const args = ['--distribution', INSTANCE_NAME, '--exec', '/usr/local/bin/wsl-exec'];\n\n    if (typeof optionsOrCommand === 'string') {\n      args.push(optionsOrCommand);\n    } else {\n      const options: execOptions = optionsOrCommand;\n\n      // runTrivyScan() calls spawn({root: true}, …), which we ignore because we are already running as root\n      if (options.expectFailure || options.logStream || options.env) {\n        throw new TypeError('Not supported yet');\n      }\n    }\n    args.push(...command);\n\n    return childProcess.spawn('wsl.exe', args);\n  }\n\n  /**\n   * captureCommand runs the given command in the K3s WSL environment and returns\n   * the standard output.\n   * @param command The command to execute.\n   * @returns The output of the command.\n   */\n  protected async captureCommand(...command: string[]): Promise<string>;\n  protected async captureCommand(options: wslExecOptions, ...command: string[]): Promise<string>;\n  protected async captureCommand(optionsOrArg: wslExecOptions | string, ...command: string[]): Promise<string> {\n    let result: string;\n    let debugArg: string;\n\n    if (typeof optionsOrArg === 'string') {\n      result = await this.execCommand({ capture: true }, optionsOrArg, ...command);\n      debugArg = optionsOrArg;\n    } else {\n      result = await this.execCommand({ ...optionsOrArg, capture: true }, ...command);\n      debugArg = JSON.stringify(optionsOrArg);\n    }\n    console.debug(`captureCommand:\\ncommand: (${ debugArg } ${ command.map(s => `'${ s }'`).join(' ') })\\noutput: <${ result }>`);\n\n    return result;\n  }\n\n  /** Get the IPv4 address of the VM, assuming it's already up. */\n  get ipAddress(): Promise<string | undefined> {\n    return (async() => {\n      // When using mirrored-mode networking, 127.0.0.1 works just fine\n      // ...also, there may not even be an `eth0` to find the IP of!\n      try {\n        const networkModeString = await this.captureCommand('wslinfo', '-n', '--networking-mode');\n\n        if (networkModeString === 'mirrored') {\n          return '127.0.0.1';\n        }\n      } catch {\n        // wslinfo is missing (wsl < 2.0.4) - fall back to old behavior\n      }\n\n      // We need to locate the _local_ route (netmask) for eth0, and then\n      // look it up in /proc/net/fib_trie to find the local address.\n      const routesString = await this.captureCommand('cat', '/proc/net/route');\n      const routes = routesString.split(/\\r?\\n/).map(line => line.split(/\\s+/));\n      const route = routes.find(route => route[0] === 'eth0' && route[1] !== '00000000');\n\n      if (!route) {\n        return undefined;\n      }\n      const net = Array.from(route[1].matchAll(/../g)).reverse().map(n => parseInt(n.toString(), 16)).join('.');\n      const trie = await this.captureCommand('cat', '/proc/net/fib_trie');\n      const lines = _.takeWhile(trie.split(/\\r?\\n/).slice(1), line => /^\\s/.test(line));\n      const iface = _.dropWhile(lines, line => !line.includes(`${ net }/`));\n      const addr = iface.find((_, i, array) => array[i + 1]?.includes('/32 host LOCAL'));\n\n      return addr?.split(/\\s+/).pop();\n    })();\n  }\n\n  async getBackendInvalidReason(): Promise<BackendError | null> {\n    // Check if wsl.exe is available\n    try {\n      await this.isDistroRegistered();\n    } catch (ex: any) {\n      const stdout = String(ex.stdout || '');\n      const isWSLMissing = (ex as NodeJS.ErrnoException).code === 'ENOENT';\n      const isInvalidUsageError = stdout.includes('Usage: ') && !stdout.includes('--exec');\n\n      if (isWSLMissing || isInvalidUsageError) {\n        console.log('Error launching WSL: it does not appear to be installed.');\n        const message = `\n          Windows Subsystem for Linux does not appear to be installed.\n\n          Please install it manually:\n\n          https://docs.microsoft.com/en-us/windows/wsl/install\n        `.replace(/[ \\t]{2,}/g, '').trim();\n\n        return new BackendError('Error: WSL Not Installed', message, true);\n      }\n      throw ex;\n    }\n\n    return null;\n  }\n\n  /**\n   * Check the WSL distribution version is acceptable; upgrade the distro\n   * version if it is too old.\n   * @precondition The distribution is already registered.\n   */\n  protected async upgradeDistroAsNeeded() {\n    let existingVersion = await this.getDistroVersion();\n\n    if (!semver.valid(existingVersion, true)) {\n      existingVersion += '.0';\n    }\n    let desiredVersion = DISTRO_VERSION;\n\n    if (!semver.valid(desiredVersion, true)) {\n      desiredVersion += '.0';\n    }\n    if (semver.lt(existingVersion, desiredVersion, true)) {\n      // Make sure we copy the data over before we delete the old distro\n      await this.progressTracker.action('Upgrading WSL distribution', 100, async() => {\n        await this.initDataDistribution();\n        await this.execWSL('--unregister', INSTANCE_NAME);\n        await this.ensureDistroRegistered();\n      });\n    }\n  }\n\n  /**\n   * Runs /sbin/init in the Rancher Desktop WSL2 distribution.\n   * This manages {this.process}.\n   */\n  protected async runInit() {\n    const logFile = Logging['wsl-init'];\n    const PID_FILE = '/run/wsl-init.pid';\n    const streamReaders: Promise<void>[] = [];\n\n    // Delete any stale wsl-init PID file\n    try {\n      await this.execCommand('rm', '-f', PID_FILE);\n    } catch {\n    }\n\n    await this.writeFile('/usr/local/bin/wsl-init', WSL_INIT_SCRIPT, 0o755);\n\n    // The process should already be gone by this point, but make sure.\n    this.process?.kill('SIGTERM');\n    const env: Record<string, string> = {\n      ...process.env,\n      WSLENV:           `${ process.env.WSLENV }:DISTRO_DATA_DIRS:LOG_DIR/p:RD_DEBUG`,\n      DISTRO_DATA_DIRS: DISTRO_DATA_DIRS.join(':'),\n      LOG_DIR:          paths.logs,\n    };\n\n    if (this.debug) {\n      env.RD_DEBUG = '1';\n    }\n    this.process = childProcess.spawn('wsl.exe',\n      ['--distribution', INSTANCE_NAME, '--exec', '/usr/local/bin/wsl-init'],\n      {\n        env,\n        stdio:       ['ignore', 'pipe', 'pipe'],\n        windowsHide: true,\n      });\n    for (const readable of [this.process.stdout, this.process.stderr]) {\n      if (readable) {\n        readable.on('data', (chunk: Buffer | string) => {\n          logFile.log(chunk.toString().trimEnd());\n        });\n        streamReaders.push(stream.promises.finished(readable));\n      }\n    }\n    this.process.on('exit', async(status, signal) => {\n      await Promise.allSettled(streamReaders);\n      if ([0, null].includes(status) && ['SIGTERM', null].includes(signal)) {\n        console.log('/sbin/init exited gracefully.');\n        await this.stop();\n      } else {\n        console.log(`/sbin/init exited with status ${ status } signal ${ signal }`);\n        await this.stop();\n        await this.setState(State.ERROR);\n      }\n    });\n\n    // Wait for the PID file\n    const startTime = Date.now();\n    const waitTime = 1_000;\n    const maxWaitTime = 30_000;\n\n    while (true) {\n      try {\n        const stdout = await this.captureCommand({ expectFailure: true }, 'cat', PID_FILE);\n\n        console.debug(`Read wsl-init.pid: ${ stdout.trim() }`);\n        break;\n      } catch (e) {\n        console.debug(`Error testing for wsl-init.pid: ${ e } (will retry)`);\n      }\n      if (Date.now() - startTime > maxWaitTime) {\n        throw new Error(`Timed out after waiting for /run/wsl-init.pid: ${ maxWaitTime / waitTime } secs`);\n      }\n      await util.promisify(setTimeout)(waitTime);\n    }\n  }\n\n  /**\n   * Write a configuration file for an OpenRC service.\n   * @param service The name of the OpenRC service to configure.\n   * @param settings A mapping of configuration values.  This should be shell escaped.\n   */\n  protected async writeConf(service: string, settings: Record<string, string>) {\n    const contents = Object.entries(settings).map(([key, value]) => `${ key }=\"${ value }\"\\n`).join('');\n\n    await this.writeFile(`/etc/conf.d/${ service }`, contents);\n  }\n\n  /**\n   * Read the configuration file for an OpenRC service.\n   * @param service The name of the OpenRC service to read.\n   */\n  protected async readConf(service: string): Promise<Record<string, string>> {\n    // Matches a k/v-pair and groups it into separated key and value, e.g.:\n    // [\"key1:\"value1\"\", \"key1\", \"\"value1\"\"]\n    const confRegex = /(?:^|^)\\s*?([\\w]+)(?:\\s*=\\s*?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*(?:[\\w.-])*|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/;\n    const conf = await this.readFile(`/etc/conf.d/${ service }`);\n\n    const confFields = conf.split(/\\r?\\n/) // Splits config in array of k/v-pairs ([\"key1:\"value1\"\", \"key2:\"value2\"\"])\n      // Maps the array into [[\"key1:\"value1\"\", \"key1\", \"\"value1\"\"], [\"key2:\"value2\"\", \"key2\", \"\"value2\"\"]]\n      .map(line => confRegex.exec(line))\n      .filter(defined);\n\n    return confFields.reduce((res, curr) => {\n      const key = curr[1];\n      const value = curr[2].replace(/^(['\"])([\\s\\S]*)\\1$/mg, '$2'); // Removes redundant quotes from value\n\n      return { ...res, ...{ [key]: value } };\n    }, {} as Record<string, string>);\n  }\n\n  /**\n   * Updates a service config with the given settings.\n   * @param service The name of the OpenRC service to configure.\n   * @param settings A mapping of configuration values.\n   */\n  protected async modifyConf(service: string, settings: Record<string, string>) {\n    const current = await this.readConf(service);\n    const contents = { ...current, ...settings };\n\n    await this.writeConf(service, contents);\n  }\n\n  /**\n   * Execute a command on a given OpenRC service.\n   *\n   * @param service The name of the OpenRC service to execute.\n   * @param action The name of the OpenRC service action to execute.\n   * @param argument Argument to pass to `wsl-service` (`--ifnotstart`, `--ifstarted`)\n   */\n  async execService(service: string, action: string, argument = '') {\n    await this.execCommand('/usr/local/bin/wsl-service', argument, service, action);\n  }\n\n  /**\n   * Start the given OpenRC service.  This should only happen after\n   * provisioning, to ensure that provisioning can modify any configuration.\n   *\n   * @param service The name of the OpenRC service to execute.\n   */\n  async startService(service: string) {\n    await this.execCommand('/sbin/rc-update', '--update');\n    await this.execService(service, 'start', '--ifnotstarted');\n  }\n\n  /**\n   * Stop the given OpenRC service.\n   *\n   * @param service The name of the OpenRC service to stop.\n   */\n  async stopService(service: string) {\n    await this.execService(service, 'stop', '--ifstarted');\n  }\n\n  /**\n   * Verify that the given command runs successfully\n   * @param command\n   */\n  async verifyReady(...command: string[]) {\n    const startTime = Date.now();\n    const maxWaitTime = 60_000;\n    const waitTime = 500;\n\n    while (true) {\n      const currentTime = Date.now();\n\n      if ((currentTime - startTime) > maxWaitTime) {\n        console.log(`Waited more than ${ maxWaitTime / 1000 } secs for ${ command.join(' ') } to succeed. Giving up.`);\n        break;\n      }\n      try {\n        await this.execCommand({ expectFailure: true }, ...command);\n        break;\n      } catch (err) {\n        console.debug(`Command ${ command } failed: `, err);\n      }\n      await util.promisify(setTimeout)(waitTime);\n    }\n  }\n\n  async start(config_: BackendSettings): Promise<void> {\n    const config = this.cfg = _.defaultsDeep(clone(config_),\n      { containerEngine: { name: ContainerEngine.NONE } });\n    let kubernetesVersion: semver.SemVer | undefined;\n    let isDowngrade = false;\n\n    await this.setState(State.STARTING);\n    this.currentAction = Action.STARTING;\n    this.#containerEngineClient = undefined;\n    await this.progressTracker.action('Initializing Rancher Desktop', 10, async() => {\n      try {\n        const prepActions = [(async() => {\n          await this.ensureDistroRegistered();\n          await this.upgradeDistroAsNeeded();\n          await this.writeHostsFile(config);\n        })()];\n\n        if (config.kubernetes.enabled) {\n          prepActions.push((async() => {\n            [kubernetesVersion, isDowngrade] = await this.kubeBackend.download(config);\n          })());\n        }\n\n        // Clear the diagnostic about not having Kubernetes versions\n        mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: true });\n\n        await this.progressTracker.action('Preparing to start', 0, Promise.all(prepActions));\n        if (config.kubernetes.enabled && kubernetesVersion === undefined) {\n          if (isDowngrade) {\n            // The desired version was unavailable, and the user declined a downgrade.\n            this.setState(State.ERROR);\n\n            return;\n          }\n          // The desired version was unavailable, and we couldn't find a fallback.\n          // Notify the user, and turn off Kubernetes.\n          mainEvents.emit('diagnostics-event', { id: 'kube-versions-available', available: false });\n          this.writeSetting({ kubernetes: { enabled: false } });\n        }\n        if (this.currentAction !== Action.STARTING) {\n          // User aborted before we finished\n          return;\n        }\n\n        // If we were previously running, stop it now.\n        await this.progressTracker.action('Stopping existing instance', 100, async() => {\n          try {\n            await this.execCommand({ expectFailure: true }, 'rm', '-f', '/var/log/rc.log');\n          } catch {}\n          this.process?.kill('SIGTERM');\n          await this.killStaleProcesses();\n        });\n\n        const distroLock = await this.progressTracker.action('Mounting WSL data', 100, this.mountData());\n\n        try {\n          await this.progressTracker.action('Installing container engine', 0, Promise.all([\n            this.progressTracker.action('Starting WSL environment', 100, async() => {\n              const rdNetworkingDNS = 'gateway.rancher-desktop.internal';\n              const logPath = await this.wslify(paths.logs);\n              const rotateConf = LOGROTATE_K3S_SCRIPT.replace(/\\r/g, '')\n                .replace('/var/log', logPath);\n              const configureWASM = !!this.cfg?.experimental?.containerEngine?.webAssembly?.enabled;\n              const mobyStorageDriver = this.cfg?.containerEngine?.mobyStorageDriver ?? 'auto';\n\n              await Promise.all([\n                this.progressTracker.action('Installing the docker-credential helper', 10, async() => {\n                  // This must run after /etc/rancher is mounted\n                  await this.installCredentialHelper();\n                }),\n                this.progressTracker.action('DNS configuration', 50, () => {\n                  return new Promise<void>((resolve) => {\n                    console.debug(`setting DNS server to ${ rdNetworkingDNS } for rancher desktop networking`);\n                    try {\n                      this.hostSwitchProcess.start();\n                    } catch (error) {\n                      console.error('Failed to run rancher desktop networking host-switch.exe process:', error);\n                    }\n                    resolve();\n                  });\n                }),\n                this.progressTracker.action('Kubernetes dockerd compatibility', 50, async() => {\n                  await this.writeFile('/etc/init.d/cri-dockerd', SERVICE_SCRIPT_CRI_DOCKERD, 0o755);\n                  await this.writeConf('cri-dockerd', {\n                    ENGINE:  config.containerEngine.name,\n                    LOG_DIR: logPath,\n                  });\n                }),\n                this.progressTracker.action('Kubernetes components', 50, async() => {\n                  await this.writeFile('/etc/init.d/k3s', SERVICE_SCRIPT_K3S, 0o755);\n                  await this.writeFile('/etc/logrotate.d/k3s', rotateConf);\n                  await this.execCommand('mkdir', '-p', '/etc/cni/net.d');\n                  if (config.kubernetes.options.flannel) {\n                    await this.writeFile('/etc/cni/net.d/10-flannel.conflist', FLANNEL_CONFLIST);\n                  }\n                }),\n                this.progressTracker.action('container engine components', 50, async() => {\n                  await BackendHelper.configureContainerEngine(this, configureWASM, mobyStorageDriver);\n                  await this.writeConf('containerd', { log_owner: 'root' });\n                  await this.writeFile('/usr/local/bin/nerdctl', NERDCTL, 0o755);\n                  await this.writeFile('/etc/init.d/docker', SERVICE_SCRIPT_DOCKERD, 0o755);\n                  await this.writeConf('docker', {\n                    WSL_HELPER_BINARY: await this.getWSLHelperPath(),\n                    LOG_DIR:           logPath,\n                  });\n                  await this.writeFile(`/etc/init.d/buildkitd`, SERVICE_BUILDKITD_INIT, 0o755);\n                  await this.writeFile(`/etc/conf.d/buildkitd`,\n                    `${ SERVICE_BUILDKITD_CONF }\\nlog_file=${ logPath }/buildkitd.log\\n`);\n                }),\n                this.progressTracker.action('Proxy Config Setup', 50, async() => {\n                  await this.execCommand('mkdir', '-p', '/etc/moproxy');\n                  await this.writeConf('moproxy', {\n                    MOPROXY_BINARY: await this.getMoproxyPath(),\n                    LOG_DIR:        logPath,\n                  });\n                  await this.writeFile('/etc/init.d/moproxy', SERVICE_SCRIPT_MOPROXY, 0o755);\n                  await this.writeProxySettings(config.experimental.virtualMachine.proxy);\n                }),\n                this.progressTracker.action('Configuring image proxy', 50, async() => {\n                  const allowedImagesConf = '/usr/local/openresty/nginx/conf/allowed-images.conf';\n                  const resolver = `resolver ${ rdNetworkingDNS } ipv6=off;\\n`;\n\n                  await this.writeFile(`/usr/local/openresty/nginx/conf/nginx.conf`, NGINX_CONF, 0o644);\n                  await this.writeFile(`/usr/local/openresty/nginx/conf/resolver.conf`, resolver, 0o644);\n                  await this.writeFile(`/etc/logrotate.d/openresty`, LOGROTATE_OPENRESTY_SCRIPT, 0o644);\n\n                  await this.runInstallScript(CONFIGURE_IMAGE_ALLOW_LIST, 'configure-allowed-images');\n                  if (config.containerEngine.allowedImages.enabled) {\n                    const patterns = BackendHelper.createAllowedImageListConf(config.containerEngine.allowedImages);\n\n                    await this.writeFile(allowedImagesConf, patterns, 0o644);\n                  } else {\n                    await this.execCommand({ root: true }, 'rm', '-f', allowedImagesConf);\n                  }\n                  const obsoleteImageAllowListConf = path.join(path.dirname(allowedImagesConf), 'image-allow-list.conf');\n\n                  await this.execCommand({ root: true }, 'rm', '-f', obsoleteImageAllowListConf);\n                }),\n                this.progressTracker.action('Rancher Desktop guest agent', 50, this.installGuestAgent(kubernetesVersion, this.cfg)),\n                // Remove any residual rc artifacts from previous version\n                this.execCommand({ root: true }, 'rm', '-f',\n                  '/etc/init.d/vtunnel-peer', '/etc/runlevels/default/vtunnel-peer',\n                  '/etc/init.d/host-resolver', '/etc/runlevels/default/host-resolver',\n                  '/etc/init.d/dnsmasq-generate', '/etc/runlevels/default/dnsmasq-generate',\n                  '/etc/init.d/dnsmasq', '/etc/runlevels/default/dnsmasq'),\n              ]);\n\n              await this.writeFile('/usr/local/bin/wsl-exec', WSL_EXEC, 0o755);\n              await this.runInit();\n              if (configureWASM) {\n                try {\n                  const version = semver.parse(DEPENDENCY_VERSIONS.spinCLI);\n                  const env = {\n                    KUBE_PLUGIN_VERSION: DEPENDENCY_VERSIONS.spinKubePlugin,\n                    SPIN_TEMPLATES_TAG:  (version ? `spin/templates/v${ version.major }.${ version.minor }` : 'unknown'),\n                  };\n                  const wslenv = Object.keys(env).join(':');\n\n                  // wsl-exec is needed to correctly resolve DNS names\n                  await this.execCommand({\n                    env: {\n                      ...process.env, ...env, WSLENV: wslenv,\n                    },\n                  }, '/usr/local/bin/wsl-exec', await this.wslify(executable('setup-spin')));\n                } catch {\n                  // just ignore any errors; all the script does is installing spin plugins and templates\n                }\n              }\n              // Do not await on this, as we don't want to wait until the proxy exits.\n              this.runWslProxy().catch(console.error);\n            }),\n            this.progressTracker.action('Installing CA certificates', 100, this.installCACerts()),\n            this.progressTracker.action('Installing helpers', 50, this.installWSLHelpers()),\n          ]));\n\n          if (kubernetesVersion) {\n            const version = kubernetesVersion;\n            const allPlatformsThresholdVersion = '1.31.0';\n\n            // We install containerd-shims as part of the container engine installation (see\n            // BackendHelper#installContainerdShims); and we need that to finish first so that when\n            // we install Kubernetes, we can look up the set of shims in order to create\n            // RuntimeClasses for them.  (See BackendHelper#configureRuntimeClasses.)\n            await this.progressTracker.action('Installing Kubernetes', 0, Promise.all([\n              this.progressTracker.action('Writing K3s configuration', 50, async() => {\n                const k3sConf = {\n                  PORT:                   config.kubernetes.port.toString(),\n                  LOG_DIR:                await this.wslify(paths.logs),\n                  'export IPTABLES_MODE': 'legacy',\n                  ENGINE:                 config.containerEngine.name,\n                  ADDITIONAL_ARGS:        config.kubernetes.options.traefik ? '' : '--disable traefik',\n                  USE_CRI_DOCKERD:        BackendHelper.requiresCRIDockerd(config.containerEngine.name, version).toString(),\n                  ALLPLATFORMS:           semver.lt(version, allPlatformsThresholdVersion) ? '--all-platforms' : '',\n                };\n\n                // Make sure the apiserver can be accessed from WSL through the internal gateway\n                k3sConf.ADDITIONAL_ARGS += ' --tls-san gateway.rancher-desktop.internal';\n\n                // Generate certificates for the statically defined host entries.\n                // This is useful for users connecting to the host via HTTPS.\n                k3sConf.ADDITIONAL_ARGS += ' --tls-san host.rancher-desktop.internal';\n                k3sConf.ADDITIONAL_ARGS += ' --tls-san host.docker.internal';\n\n                // Add the `veth-rd-ns` IP address from inside the namespace\n                k3sConf.ADDITIONAL_ARGS += ' --tls-san 192.168.143.1';\n\n                if (!config.kubernetes.options.flannel) {\n                  console.log(`Disabling flannel and network policy`);\n                  k3sConf.ADDITIONAL_ARGS += ' --flannel-backend=none --disable-network-policy';\n                }\n                if (config.application.debug) {\n                  config.ADDITIONAL_ARGS += ' --debug';\n                }\n\n                await this.writeConf('k3s', k3sConf);\n              }),\n              this.progressTracker.action('Installing k3s', 100, async() => {\n                await this.kubeBackend.deleteIncompatibleData(version);\n                await this.kubeBackend.install(config, version, false);\n              })]));\n          }\n        } finally {\n          distroLock.kill('SIGTERM');\n        }\n\n        await this.progressTracker.action('Running provisioning scripts', 100, this.runProvisioningScripts());\n\n        if (config.experimental.virtualMachine.proxy.enabled && config.experimental.virtualMachine.proxy.address && config.experimental.virtualMachine.proxy.port) {\n          await this.progressTracker.action('Starting proxy', 100, this.startService('moproxy'));\n        }\n        if (config.containerEngine.allowedImages.enabled) {\n          await this.progressTracker.action('Starting image proxy', 100, this.startService('rd-openresty'));\n        }\n        await this.progressTracker.action('Starting container engine', 0, this.startService(config.containerEngine.name === ContainerEngine.MOBY ? 'docker' : 'containerd'));\n\n        switch (config.containerEngine.name) {\n        case ContainerEngine.CONTAINERD:\n          await this.progressTracker.action('Starting buildkit', 0,\n            this.startService('buildkitd'));\n          try {\n            await this.execCommand({\n              root:          true,\n              expectFailure: true,\n            },\n            'ctr', '--address', '/run/k3s/containerd/containerd.sock', 'namespaces', 'create', 'default');\n          } catch {\n            // expecting failure because the namespace may already exist\n          }\n          this.#containerEngineClient = new NerdctlClient(this);\n          break;\n        case ContainerEngine.MOBY:\n          this.#containerEngineClient = new MobyClient(this, 'npipe:////./pipe/docker_engine');\n          break;\n        }\n\n        // Set the kubernetes ingress address to localhost only for\n        // a non-admin installation, if it's not already set.\n        if (!config.kubernetes.ingress.localhostOnly && !await this.getIsAdminInstall()) {\n          this.writeSetting({ kubernetes: { ingress: { localhostOnly: true } } });\n        }\n\n        const tasks = [\n          this.progressTracker.action('Waiting for container engine to be ready', 0, this.containerEngineClient.waitForReady()),\n        ];\n\n        if (kubernetesVersion) {\n          tasks.push(this.progressTracker.action('Starting Kubernetes', 100, this.kubeBackend.start(config, kubernetesVersion)));\n        }\n\n        await Promise.all(tasks);\n\n        await this.setState(config.kubernetes.enabled ? State.STARTED : State.DISABLED);\n      } catch (ex) {\n        await this.setState(State.ERROR);\n        throw ex;\n      } finally {\n        this.currentAction = Action.NONE;\n      }\n    });\n  }\n\n  protected async installCACerts(): Promise<void> {\n    const certs: (string | Buffer)[] = await new Promise((resolve) => {\n      mainEvents.once('cert-ca-certificates', resolve);\n      mainEvents.emit('cert-get-ca-certificates');\n    });\n\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-ca-'));\n\n    try {\n      await this.execCommand('/bin/sh', '-c', 'rm -f /usr/local/share/ca-certificates/rd-*.crt');\n      // Similar to Lima backends, we better use of tar here to improve the performance in case of\n      // many certificates.\n\n      if (certs && certs.length > 0) {\n        const writeStream = fs.createWriteStream(path.join(workdir, 'certs.tar'));\n        const archive = tar.pack();\n        const archiveFinished = util.promisify(stream.finished)(archive as any);\n\n        archive.pipe(writeStream);\n\n        for (const [index, cert] of certs.entries()) {\n          const curried = archive.entry.bind(archive, {\n            name: `rd-${ index }.crt`,\n            mode: 0o600,\n          }, cert);\n\n          await util.promisify(curried)();\n        }\n        archive.finalize();\n        await archiveFinished;\n\n        await this.execCommand(\n          'tar', 'xf', await this.wslify(path.join(workdir, 'certs.tar')),\n          '-C', '/usr/local/share/ca-certificates/');\n      }\n    } finally {\n      await fs.promises.rm(workdir, { recursive: true, maxRetries: 3 });\n    }\n    await this.execCommand('/usr/sbin/update-ca-certificates');\n  }\n\n  /**\n   * Run provisioning scripts; this is done after init is started.\n   */\n  protected async runProvisioningScripts() {\n    const provisioningPath = path.join(paths.config, 'provisioning');\n\n    await fs.promises.mkdir(provisioningPath, { recursive: true });\n    await Promise.all([\n      (async() => {\n        // Write out the readme file.\n        const ReadmePath = path.join(provisioningPath, 'README');\n\n        try {\n          await fs.promises.access(ReadmePath, fs.constants.F_OK);\n        } catch {\n          const contents = `${ `\n            Any files named '*.start' in this directory will be executed\n            sequentially on Rancher Desktop startup, before the main services.\n            Files are processed in lexical order, and startup will be delayed\n            until they have all run to completion. Similarly, any files named\n            '*.stop' will be executed on shutdown, after the main services have\n            exited, and delay shutdown until they have run to completion.\n            Note that the script file names may not include whitespace.\n            `.replace(/\\s*\\n\\s*/g, '\\n').trim() }\\n`;\n\n          await fs.promises.writeFile(ReadmePath, contents, { encoding: 'utf-8' });\n        }\n      })(),\n      (async() => {\n        const linuxPath = await this.wslify(provisioningPath);\n\n        // Stop the service if it's already running for some reason.\n        // This should never be the case (because we tore down init).\n        await this.stopService('local');\n\n        // Clobber /etc/local.d and replace it with a symlink to our desired\n        // path.  This is needed as /etc/init.d/local does not support\n        // overriding the script directory.\n        await this.execCommand('rm', '-r', '-f', '/etc/local.d');\n        await this.execCommand('ln', '-s', '-f', '-T', linuxPath, '/etc/local.d');\n\n        // Ensure all scripts are executable; Windows mounts are unlikely to\n        // have it set by default.\n        await this.execCommand('/usr/bin/find',\n          '/etc/local.d/',\n          '(', '-name', '*.start', '-o', '-name', '*.stop', ')',\n          '-print', '-exec', 'chmod', 'a+x', '{}', ';');\n\n        // Run the script.\n        await this.startService('local');\n      })(),\n    ]);\n  }\n\n  async stop(): Promise<void> {\n    // When we manually call stop, the subprocess will terminate, which will\n    // cause stop to get called again.  Prevent the reentrancy.\n    // If we're in the middle of starting, also ignore the call to stop (from\n    // the process terminating), as we do not want to shut down the VM in that\n    // case.\n    if (this.currentAction !== Action.NONE) {\n      return;\n    }\n    this.currentAction = Action.STOPPING;\n    try {\n      await this.setState(State.STOPPING);\n      await this.kubeBackend.stop();\n      this.#containerEngineClient = undefined;\n\n      await this.progressTracker.action('Shutting Down...', 10, async() => {\n        if (await this.isDistroRegistered({ runningOnly: true })) {\n          const services = ['k3s', 'docker', 'containerd', 'rd-openresty',\n            'rancher-desktop-guestagent', 'buildkitd'];\n\n          for (const service of services) {\n            try {\n              await this.stopService(service);\n            } catch (ex) {\n              // Do not allow errors here to prevent us from stopping.\n              console.error(`Failed to stop service ${ service }:`, ex);\n            }\n          }\n          try {\n            await this.stopService('local');\n          } catch (ex) {\n            // Do not allow errors here to prevent us from stopping.\n            console.error('Failed to run user provisioning scripts on stopping:', ex);\n          }\n        }\n        const initProcess = this.process;\n\n        this.process = null;\n        if (initProcess) {\n          initProcess.kill('SIGTERM');\n          try {\n            await this.execCommand({ expectFailure: true }, '/usr/bin/killall', '/usr/local/bin/network-setup');\n          } catch (ex) {\n            // `killall` returns failure if it fails to kill (e.g. if the\n            // process does not exist); `-q` only suppresses printing any error\n            // messages.\n            console.error('Ignoring error shutting down network-setup:', ex);\n          }\n        }\n        await this.hostSwitchProcess.stop();\n        if (await this.isDistroRegistered({ runningOnly: true })) {\n          await this.execWSL('--terminate', INSTANCE_NAME);\n        }\n      });\n      await this.setState(State.STOPPED);\n    } catch (ex) {\n      await this.setState(State.ERROR);\n      throw ex;\n    } finally {\n      this.currentAction = Action.NONE;\n    }\n  }\n\n  async del(): Promise<void> {\n    await this.progressTracker.action('Deleting Kubernetes', 20, async() => {\n      await this.stop();\n      if (await this.isDistroRegistered()) {\n        await this.execWSL('--unregister', INSTANCE_NAME);\n      }\n      if (await this.isDistroRegistered({ distribution: DATA_INSTANCE_NAME })) {\n        await this.execWSL('--unregister', DATA_INSTANCE_NAME);\n      }\n      this.cfg = undefined;\n    });\n  }\n\n  async reset(config: BackendSettings): Promise<void> {\n    await this.progressTracker.action('Resetting Kubernetes state...', 5, async() => {\n      await this.stop();\n      // Mount the data first so they can be deleted correctly.\n      const distroLock = await this.mountData();\n\n      try {\n        await this.kubeBackend.reset();\n      } finally {\n        distroLock.kill('SIGTERM');\n      }\n      await this.start(config);\n    });\n  }\n\n  async handleSettingsUpdate(newConfig: BackendSettings): Promise<void> {\n    const proxy = newConfig.experimental.virtualMachine.proxy;\n\n    await this.writeProxySettings(proxy);\n    if (this.currentAction === Action.NONE && this.process) {\n      if (proxy.enabled && proxy.address && proxy.port) {\n        await this.execService('moproxy', 'reload', '--ifstarted');\n        await this.startService('moproxy');\n      } else {\n        await this.stopService('moproxy');\n      }\n    }\n  }\n\n  // The WSL implementation of requiresRestartReasons doesn't need to do\n  // anything asynchronously; however, to match the API, we still need to return\n  // a Promise.\n  requiresRestartReasons(cfg: RecursivePartial<BackendSettings>): Promise<RestartReasons> {\n    if (!this.cfg) {\n      // No need to restart if nothing exists\n      return Promise.resolve({});\n    }\n\n    return Promise.resolve(this.kubeBackend.requiresRestartReasons(\n      this.cfg, cfg));\n  }\n\n  /**\n   * Return the Linux path to the WSL helper executable.\n   */\n  getWSLHelperPath(distro?: string): Promise<string> {\n    // We need to get the Linux path to our helper executable; it is easier to\n    // just get WSL to do the transformation for us.\n\n    return this.wslify(executable('wsl-helper-linux'), distro);\n  }\n\n  async getFailureDetails(exception: any): Promise<FailureDetails> {\n    const loglines = (await fs.promises.readFile(console.path, 'utf-8')).split('\\n').slice(-10);\n\n    return {\n      lastCommand:        exception[childProcess.ErrorCommand],\n      lastCommandComment: getProgressErrorDescription(exception) ?? 'Unknown',\n      lastLogLines:       loglines,\n    };\n  }\n\n  // #region Events\n  eventNames(): (keyof BackendEvents)[] {\n    return super.eventNames() as (keyof BackendEvents)[];\n  }\n\n  listeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.listeners(event) as BackendEvents[eventName][];\n  }\n\n  rawListeners<eventName extends keyof BackendEvents>(\n    event: eventName,\n  ): BackendEvents[eventName][] {\n    return super.rawListeners(event) as BackendEvents[eventName][];\n  }\n  // #endregion\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ActionDropdown.vue",
    "content": "<script>\nimport { Dropdown as VDropdown } from 'floating-vue';\n\nexport default {\n  name:       'ActionDropdown',\n  components: { VDropdown },\n\n  props: {\n    size: {\n      type:    String,\n      default: '', // possible values are xs, sm, lg. empty is default .btn\n    },\n    // whether this is a button and dropdown (default) or dropdown that looks like a button/dropdown\n    dualAction: {\n      type:    Boolean,\n      default: true,\n    },\n\n    disableButton: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  computed: {\n    buttonSize() {\n      const { size } = this;\n      let out;\n\n      switch (size) {\n      case '':\n        out = 'btn';\n        break;\n      case 'xs':\n        out = 'btn btn-xs';\n        break;\n      case 'sm':\n        out = 'btn btn-sm';\n        break;\n      case 'lg':\n        out = 'btn btn-lg';\n        break;\n      default:\n      }\n\n      return out;\n    },\n  },\n\n  methods: {\n    hasSlot(name = 'default') {\n      return !!this.$slots[name] || !!this.$slots.name();\n    },\n\n    // allows parent components to programmatically open the dropdown\n    togglePopover() {\n      // this.$refs.popoverButton.click();\n    },\n  },\n};\n</script>\n<template>\n  <div class=\"dropdown-button-group\">\n    <div\n      class=\"dropdown-button bg-primary btn-role-primary\"\n      :class=\"{ 'one-action': !dualAction, [buttonSize]: true, disabled: disableButton }\"\n    >\n      <v-dropdown\n        placement=\"bottom-start\"\n        :container=\"false\"\n        :disabled=\"disableButton\"\n        :popper-options=\"{ modifiers: { flip: { enabled: false } } }\"\n      >\n        <slot\n          name=\"button-content\"\n          :button-size=\"buttonSize\"\n        >\n          <button\n            ref=\"popoverButton\"\n            class=\"icon-container bg-primary no-left-border-radius\"\n            :class=\"buttonSize\"\n            :disabled=\"disableButton\"\n            type=\"button\"\n          >\n            Button <i class=\"icon icon-chevron-down\" />\n          </button>\n        </slot>\n        <template #popper>\n          <slot name=\"popover-content\" />\n        </template>\n      </v-dropdown>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\">\n// load here instead of component so SSR render isn't all wonky\n.dropdown-button-group {\n  $xs-padding: 2px 3px;\n\n  .no-left-border-radius {\n    border-top-left-radius: 0px;\n    border-bottom-left-radius: 0px;\n  }\n\n  .no-right-border-radius {\n    border-top-right-radius: 0px;\n    border-bottom-right-radius: 0px;\n  }\n\n  .btn {\n    line-height: normal;\n    border: 0px;\n  }\n\n  .btn-xs,\n  .btn-group-xs > .btn,\n  .btn-xs .btn-label {\n      padding: $xs-padding;\n      font-size: 13px;\n  }\n\n  // this matches the top/bottom padding of the default button\n  $trigger-padding: 15px 10px 15px 10px;\n  $xs-trigger-padding: 2px 4px 4px 4px;\n  $sm-trigger-padding: 10px 10px 10px 10px;\n  $lg-trigger-padding: 18px 10px 10px 10px;\n\n  .v-popover {\n    .text-right {\n      margin-top: 5px;\n    }\n    .trigger {\n      height: 100%;\n      .icon-container {\n        height: 100%;\n        padding: 10px 10px 10px 10px;\n        i {\n          transform: scale(1);\n        }\n        &.btn-xs {\n          padding: $xs-trigger-padding;\n        }\n        &.btn-sm {\n          padding: $sm-trigger-padding;\n        }\n        &.btn-lg {\n          padding: $lg-trigger-padding;\n        }\n        &:focus {\n          outline-style: none;\n          box-shadow: none;\n          border-color: transparent;\n        }\n      }\n    }\n  }\n\n  .dropdown-button {\n    background: var(--tooltip-bg);\n    color: var(--link-text);\n    padding: 0;\n    display: inline-flex;\n\n    .wrapper-content {\n      button {\n        border-right: 0px;\n      }\n    }\n\n    &>*, .icon-chevron-down {\n      color: var(--primary);\n      background-color: rgba(0,0,0,0);\n    }\n\n    &.bg-primary:hover {\n      background: var(--accent-btn-hover);\n    }\n\n    &.one-action {\n      position: relative;\n      &>.btn {\n        padding: 15px 35px 15px 15px;\n      }\n      .v-popover{\n        .trigger{\n          position: absolute;\n          top: 0px;\n          right: 0px;\n          left: 0px;\n          bottom: 0px;\n          BUTTON {\n            position: absolute;\n            right: 0px;\n          }\n        }\n      }\n    }\n  }\n  .popover {\n    border: none;\n  }\n  .tooltip {\n    margin-top: 0px;\n\n    &[x-placement^=\"bottom\"] {\n      .tooltip-arrow {\n        border-bottom-color: var(--dropdown-border);\n\n        &:after {\n          border-bottom-color: var(--dropdown-bg);\n        }\n      }\n    }\n\n    .tooltip-inner {\n      color: var(--dropdown-text);\n      background-color: var(--dropdown-bg);\n      border: 1px solid var(--dropdown-border);\n      padding: 0px;\n      text-align: left;\n\n      LI {\n        padding: 10px;\n\n        &.divider {\n          padding-top: 0px;\n          padding-bottom: 0px;\n\n          > .divider-inner {\n            padding: 0;\n            border-bottom: 1px solid var(--dropdown-divider);\n            width: 125%;\n            margin: 0 auto;\n          }\n        }\n\n        &:not(.divider):hover {\n          background-color: var(--dropdown-hover-bg);\n          color: var(--dropdown-hover-text);\n          cursor: pointer;\n        }\n      }\n\n    }\n  }\n\n  //header\n  .user-info {\n    border-bottom: 1px solid var(--border);\n    display: block;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ActionMenu.vue",
    "content": "<script>\nimport $ from 'jquery';\nimport { mapGetters } from 'vuex';\n\nimport { isAlternate } from '@pkg/utils/platform';\nimport { AUTO, CENTER, fitOnScreen } from '@pkg/utils/position';\n\nconst HIDDEN = 'hide';\nconst CALC = 'calculate';\nconst SHOW = 'show';\n\nexport default {\n  data() {\n    return {\n      phase: HIDDEN,\n      style: {},\n    };\n  },\n\n  computed: {\n    ...mapGetters({\n      targetElem:  'action-menu/elem',\n      targetEvent: 'action-menu/event',\n      shouldShow:  'action-menu/showing',\n      options:     'action-menu/options',\n    }),\n\n    showing() {\n      return this.phase !== HIDDEN;\n    },\n\n  },\n\n  watch: {\n    shouldShow: {\n      handler(show) {\n        if ( show ) {\n          this.phase = CALC;\n          this.updateStyle();\n          this.$nextTick(() => {\n            if ( this.phase === CALC ) {\n              this.phase = SHOW;\n              this.updateStyle();\n            }\n          });\n        } else {\n          this.phase = HIDDEN;\n        }\n      },\n    },\n\n    '$route.path'(val, old) {\n      this.hide();\n    },\n  },\n\n  methods: {\n    hide() {\n      this.$store.commit('action-menu/hide');\n    },\n\n    updateStyle() {\n      if ( this.phase === SHOW ) {\n        const menu = $('.menu', this.$el)[0];\n        const event = this.targetEvent;\n        const elem = this.targetElem;\n\n        this.style = fitOnScreen(menu, event || elem, {\n          overlapX:  true,\n          fudgeX:    elem ? 4 : 0,\n          fudgeY:    elem ? 4 : 0,\n          positionX: (elem ? AUTO : CENTER),\n          positionY: AUTO,\n        });\n\n        this.style.visibility = 'visible';\n      } else {\n        this.style = {};\n      }\n    },\n\n    execute(action, event, args) {\n      const opts = { alt: isAlternate(event) };\n\n      this.$store.dispatch('action-menu/execute', {\n        action, args, opts,\n      });\n      this.hide();\n    },\n\n    hasOptions(options) {\n      return options.length !== undefined ? options.length : Object.keys(options).length > 0;\n    },\n  },\n};\n</script>\n\n<template>\n  <div v-if=\"showing\">\n    <div\n      class=\"background\"\n      @click=\"hide\"\n      @contextmenu.prevent\n    />\n    <ul\n      class=\"list-unstyled menu\"\n      :style=\"style\"\n    >\n      <li\n        v-for=\"opt in options\"\n        :key=\"opt.action\"\n        :class=\"{ divider: opt.divider }\"\n        @click=\"execute(opt, $event)\"\n      >\n        <i\n          v-if=\"opt.icon\"\n          :class=\"{ icon: true, [opt.icon]: true }\"\n        />\n        <span v-html=\"opt.label\" />\n      </li>\n      <li\n        v-if=\"!hasOptions(options)\"\n        class=\"no-actions\"\n      >\n        <span v-t=\"'sortableTable.noActions'\" />\n      </li>\n    </ul>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .root {\n    position: absolute;\n  }\n\n  .menu {\n    position: absolute;\n    visibility: hidden;\n    top: 0;\n    left: 0;\n    z-index: z-index('dropdownContent');\n\n    color: var(--dropdown-text);\n    background-color: var(--dropdown-bg);\n    border: 1px solid var(--dropdown-border);\n    border-radius: 5px;\n    box-shadow: 0 5px 20px var(--shadow);\n\n    LI {\n      padding: 10px;\n      margin: 0;\n\n      &.divider {\n        padding: 0;\n        border-bottom: 1px solid var(--dropdown-divider);\n      }\n\n      &:not(.divider):hover {\n        background-color: var(--dropdown-hover-bg);\n        color: var(--dropdown-hover-text);\n        cursor: pointer;\n      }\n\n      .icon {\n        display: unset;\n      }\n\n      &.no-actions {\n        color: var(--disabled-text);\n      }\n\n      &.no-actions:hover {\n        background-color: initial;\n        color: var(--disabled-text);\n        cursor: default;\n      }\n    }\n  }\n\n  .background {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    opacity: 0;\n    z-index: z-index('dropdownOverlay');\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Alert.vue",
    "content": "<script lang=\"ts\">\nimport { Banner } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:       'alert',\n  components: { Banner },\n  props:      {\n    icon: {\n      type:     String,\n      required: true,\n    },\n    bannerText: {\n      type:     String,\n      required: true,\n    },\n    color: {\n      type:     String,\n      required: true,\n    },\n  },\n});\n</script>\n\n<template>\n  <transition\n    name=\"fade\"\n    appear\n  >\n    <banner\n      class=\"banner-notify\"\n      :color=\"color\"\n    >\n      <span\n        class=\"icon\"\n        :class=\"icon\"\n      />\n      {{ bannerText }}\n    </banner>\n  </transition>\n</template>\n\n<style lang=\"scss\" scoped>\n  .banner-notify {\n    margin: 0;\n  }\n\n  .fade-enter, .fade-leave-to {\n    opacity: 0;\n  }\n\n  .fade-active {\n    transition: all 0.25s ease-in;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/AsyncButton.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent, PropType, inject } from 'vue';\n\nimport typeHelper from '@pkg/utils/type-helpers';\n\nexport const ASYNC_BUTTON_STATES = {\n  ACTION:  'action',\n  WAITING: 'waiting',\n  SUCCESS: 'success',\n  ERROR:   'error',\n};\n\nconst TEXT = 'text';\nconst TOOLTIP = 'tooltip';\n\nexport type AsyncButtonCallback = (success: boolean) => void;\n\ninterface NonReactiveProps {\n  timer: NodeJS.Timeout | undefined;\n}\n\nconst provideProps: NonReactiveProps = { timer: undefined };\n\n// i18n-uses asyncButton.*\nexport default defineComponent({\n  props: {\n    /**\n     * Mode maps to keys in asyncButton.* translations\n     */\n    mode: {\n      type:    String,\n      default: 'edit',\n    },\n    delay: {\n      type:    Number,\n      default: 5000,\n    },\n\n    name: {\n      type:    String,\n      default: null,\n    },\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n    type: {\n      type:    String as PropType<'button' | 'submit' | 'reset' | undefined>,\n      default: 'button',\n    },\n    tabIndex: {\n      type:    Number,\n      default: null,\n    },\n\n    actionColor: {\n      type:    String,\n      default: 'role-primary',\n    },\n    waitingColor: {\n      type:    String,\n      default: 'bg-primary',\n    },\n    successColor: {\n      type:    String,\n      default: 'bg-success',\n    },\n    errorColor: {\n      type:    String,\n      default: 'bg-error',\n    },\n\n    actionLabel: {\n      type:    String,\n      default: null,\n    },\n    waitingLabel: {\n      type:    String,\n      default: null,\n    },\n    successLabel: {\n      type:    String,\n      default: null,\n    },\n    errorLabel: {\n      type:    String,\n      default: null,\n    },\n\n    icon: {\n      type:    String,\n      default: null,\n    },\n    labelAs: {\n      type:    String,\n      default: TEXT,\n    },\n    size: {\n      type:    String,\n      default: '',\n    },\n\n    currentPhase: {\n      type:    String,\n      default: ASYNC_BUTTON_STATES.ACTION,\n    },\n\n    /**\n     * Inherited global identifier prefix for tests\n     * Define a term based on the parent component to avoid conflicts on multiple components\n     */\n    componentTestid: {\n      type:    String,\n      default: 'action-button',\n    },\n\n    manual: {\n      type:    Boolean,\n      default: false,\n    },\n\n  },\n\n  setup() {\n    const timer = inject('timer', provideProps.timer);\n\n    return { timer };\n  },\n\n  emits: ['click'],\n\n  data() {\n    return { phase: this.currentPhase };\n  },\n\n  watch: {\n    currentPhase(neu) {\n      this.phase = neu;\n    },\n  },\n\n  computed: {\n    classes(): { btn: boolean, [color: string]: boolean } {\n      const key = `${ this.phase }Color`;\n      const color = typeHelper.memberOfComponent(this, key);\n\n      const out = {\n        btn:     true,\n        [color]: true,\n      };\n\n      if (this.size) {\n        out[`btn-${ this.size }`] = true;\n      }\n\n      return out;\n    },\n\n    displayIcon(): string {\n      const exists = this.$store.getters['i18n/exists'];\n      const t = this.$store.getters['i18n/t'];\n      const key = `asyncButton.${ this.mode }.${ this.phase }Icon`;\n      const defaultKey = `asyncButton.default.${ this.phase }Icon`;\n\n      let out = '';\n\n      if ( this.icon ) {\n        out = this.icon;\n      } else if ( exists(key) ) {\n        out = `icon-${ t(key) }`;\n      } else if ( exists(defaultKey) ) {\n        out = `icon-${ t(defaultKey) }`;\n      }\n\n      if ( this.isSpinning ) {\n        if ( !out ) {\n          out = 'icon-spinner';\n        }\n\n        out += ' icon-spin';\n      }\n\n      return out;\n    },\n\n    displayLabel(): string {\n      const override = typeHelper.memberOfComponent(this, `${ this.phase }Label`);\n      const exists = this.$store.getters['i18n/exists'];\n      const t = this.$store.getters['i18n/t'];\n      const key = `asyncButton.${ this.mode }.${ this.phase }`;\n      const defaultKey = `asyncButton.default.${ this.phase }`;\n\n      if ( override ) {\n        return override;\n      } else if ( exists(key) ) {\n        return t(key);\n      } else if ( exists(defaultKey) ) {\n        return t(defaultKey);\n      } else {\n        return '';\n      }\n    },\n\n    isSpinning(): boolean {\n      return this.phase === ASYNC_BUTTON_STATES.WAITING;\n    },\n\n    isDisabled(): boolean {\n      return this.disabled || this.phase === ASYNC_BUTTON_STATES.WAITING;\n    },\n\n    tooltip(): { content: string, hideOnTargetClick: boolean } | null {\n      if ( this.labelAs === TOOLTIP ) {\n        return {\n          content:           this.displayLabel,\n          hideOnTargetClick: false,\n        };\n      }\n\n      return null;\n    },\n  },\n\n  beforeUnmount() {\n    if (this.timer) {\n      clearTimeout(this.timer);\n    }\n  },\n\n  methods: {\n    clicked() {\n      if ( this.isDisabled ) {\n        return;\n      }\n\n      if (this.timer) {\n        clearTimeout(this.timer);\n      }\n\n      // If manual property is set, don't automatically change the button on click\n      if (!this.manual) {\n        this.phase = ASYNC_BUTTON_STATES.WAITING;\n      }\n\n      const cb: AsyncButtonCallback = (success) => {\n        this.done(success);\n      };\n\n      this.$emit('click', cb);\n    },\n\n    done(success: boolean | 'cancelled') {\n      if (success === 'cancelled') {\n        this.phase = ASYNC_BUTTON_STATES.ACTION;\n      } else {\n        this.phase = (success ? ASYNC_BUTTON_STATES.SUCCESS : ASYNC_BUTTON_STATES.ERROR );\n        this.timer = setTimeout(() => {\n          this.timerDone();\n        }, this.delay);\n      }\n    },\n\n    timerDone() {\n      if ( this.phase === ASYNC_BUTTON_STATES.SUCCESS || this.phase === ASYNC_BUTTON_STATES.ERROR ) {\n        this.phase = ASYNC_BUTTON_STATES.ACTION;\n      }\n    },\n\n    focus() {\n      (this.$refs.btn as HTMLElement).focus();\n    },\n  },\n});\n</script>\n\n<template>\n  <button\n    ref=\"btn\"\n    :class=\"classes\"\n    :name=\"name\"\n    :type=\"type\"\n    :disabled=\"isDisabled\"\n    :tab-index=\"tabIndex\"\n    :data-testid=\"componentTestid + '-async-button'\"\n    @click=\"clicked\"\n  >\n    <span v-if=\"mode === 'manual-refresh'\">{{ t('action.refresh') }}</span>\n    <i\n      v-if=\"displayIcon\"\n      v-clean-tooltip=\"tooltip\"\n      :class=\"{ icon: true, 'icon-lg': true, [displayIcon]: true }\"\n    />\n    <span\n      v-if=\"labelAs === 'text' && displayLabel\"\n      v-clean-tooltip=\"tooltip\"\n      v-clean-html=\"displayLabel\"\n    />\n  </button>\n</template>\n\n<style lang=\"scss\" scoped>\n// refresh mode has icon + text. We need to fix the positioning of the icon and sizing\n.manual-refresh i {\n  margin: 0 0 0 8px !important;\n  font-size: 1rem !important;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/BackendProgress.vue",
    "content": "<!-- This is the Kubernetes backend progress notification in the bottom left\n   - corner of the default layout.\n   -->\n\n<template>\n  <div\n    v-if=\"progressBusy\"\n    class=\"progress\"\n  >\n    <label\n      class=\"details\"\n      :title=\"progressDetails\"\n    >{{ progressDetails }}</label>\n    <RdProgress\n      class=\"progress-bar\"\n      :indeterminate=\"progressIndeterminate\"\n      :value=\"progress.current\"\n      :maximum=\"progress.max\"\n    />\n    <label\n      class=\"duration\"\n      :title=\"progressDuration\"\n    >{{ progressDuration }}</label>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport RdProgress from '@pkg/components/RdProgress.vue';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'backend-progress',\n  components: { RdProgress },\n  data() {\n    return {\n      /** Current Kubernetes backend action progress. */\n      progress: { current: 1, max: 1 } as {\n        /** The current progress, from 0 to max. */\n        readonly current:         number;\n        /** Maximum possible progress; if less than zero, the progress is indeterminate. */\n        readonly max:             number;\n        /** Description of current action. */\n        readonly description?:    string;\n        /** Time since the description became valid. */\n        readonly transitionTime?: Date;\n      },\n      progressInterval: undefined as ReturnType<typeof setInterval> | undefined,\n      progressDuration: '',\n    };\n  },\n\n  computed: {\n    progressDetails(): string {\n      return this.progress.description || '';\n    },\n    progressIndeterminate(): boolean {\n      return this.progress.max <= 0;\n    },\n    progressBusy(): boolean {\n      return this.progressIndeterminate || this.progress.current < this.progress.max;\n    },\n  },\n\n  mounted() {\n    ipcRenderer.on('k8s-progress', (event, progress) => {\n      this.progress = progress;\n      if (this.progress.transitionTime) {\n        if (!this.progressInterval) {\n          const start = this.progress.transitionTime.valueOf();\n\n          this.progressInterval = setInterval(() => {\n            this.progressDuration = this.describeElapsed(start);\n          }, 500);\n        }\n      } else if (this.progressInterval) {\n        clearInterval(this.progressInterval);\n        this.progressInterval = undefined;\n        this.progressDuration = '';\n      }\n    });\n\n    ipcRenderer.invoke('k8s-progress').then((progress) => {\n      this.progress = progress;\n    });\n  },\n\n  methods: {\n    /** Return a string describing the elapsed time or progress. */\n    describeElapsed(since: number): string {\n      if (this.progress.max > 0) {\n        // If we have numbers, give a description about that.\n        const units = ['', 'K', 'M', 'G', 'T'];\n        const scales = [2 ** 0, 2 ** 10, 2 ** 20, 2 ** 30, 2 ** 40];\n        const remaining = this.progress.max - this.progress.current;\n\n        const unitIndex = scales.findLastIndex((scale) => remaining * 2 >= scale);\n        const fraction = remaining / scales[unitIndex];\n        // If the fraction is 0.5...0.9999 display it as single significant figure.\n        const display = fraction > 1 ? Math.round(fraction) : Math.round(fraction * 10) / 10;\n        return `${ display }${ units[unitIndex] } left`;\n      }\n      if (!since) {\n        return '';\n      }\n      // We have a starting time; describe how much time has elapsed since.\n      // Start from the smallest unit, and modify `remaining` to be the next\n      // unit up at every iteration.\n      let remaining = Math.floor((Date.now() - since) / 1000); // Elapsed time, in seconds.\n      const scales: [number, string][] = [[60, 's'], [60, 'm'], [24, 'h'], [Number.POSITIVE_INFINITY, 'd']];\n      let label = '';\n\n      for (const [scale, unit] of scales) {\n        if (remaining % scale > 0) {\n          // Add the part, but only if it's non-zero.\n          label = `${ remaining % scale }${ unit }${ label }`;\n        }\n        remaining = Math.floor(remaining / scale);\n      }\n\n      return label;\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .progress {\n    display: flex;\n    flex-direction: row;\n    white-space: nowrap;\n    align-items: center;\n    flex: 1;\n\n    .details {\n      text-align: end;\n      text-overflow: ellipsis;\n      overflow: hidden;\n      padding-right: 0.25rem;\n      flex: 1;\n    }\n\n    .progress-bar {\n      max-width: 12rem;\n    }\n\n    .duration {\n      padding-left: 0.25rem;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ContainerLogs.vue",
    "content": "<template>\n  <div class=\"container-logs-component\">\n    <loading-indicator\n      v-if=\"isLoading || waitingForInitialLogs\"\n      class=\"content-state\"\n      data-testid=\"loading-indicator\"\n    >\n      Loading logs...\n    </loading-indicator>\n\n    <banner\n      v-if=\"error && !waitingForInitialLogs\"\n      class=\"content-state\"\n      color=\"error\"\n      data-testid=\"error-message\"\n    >\n      <span class=\"icon icon-info-circle icon-lg\" />\n      {{ error }}\n    </banner>\n\n    <div\n      v-if=\"!isLoading\"\n      ref=\"terminalContainer\"\n      :class=\"['terminal-container', { 'terminal-hidden': waitingForInitialLogs }]\"\n      data-testid=\"terminal\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport v1 from '@docker/extension-api-client-types/dist/v1';\nimport { Banner } from '@rancher/components';\nimport { FitAddon } from '@xterm/addon-fit';\nimport { SearchAddon } from '@xterm/addon-search';\nimport { WebLinksAddon } from '@xterm/addon-web-links';\nimport { Terminal } from '@xterm/xterm';\nimport { shell } from 'electron';\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';\n\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\n\ninterface RDXSpawnOptions extends v1.SpawnOptions {\n  namespace?: string;\n}\n\ndefineOptions({ name: 'ContainerLogs' });\n\nconst props = defineProps<{\n  containerId:         string;\n  isContainerRunning?: boolean;\n  namespace?:          string | null;\n}>();\n\ndefineExpose({\n  clearSearch,\n  performSearch,\n  searchNext,\n  searchPrevious,\n});\n\nconst isLoading = ref(true);\nconst error = ref<string | null>(null);\n\nlet terminal: Terminal | undefined;\nlet fitAddon: FitAddon | undefined;\nlet searchAddon: SearchAddon | undefined;\nlet streamProcess: v1.ExecProcess | undefined;\nlet resizeObserver: ResizeObserver | undefined;\nlet reconnectAttempts = 0;\nconst maxReconnectAttempts = 5;\nlet searchDebounceTimer: ReturnType<typeof setTimeout> | undefined;\n/**\n * Debounce the initial log loading to avoid showing the initial logs streaming\n * in right after loading.\n */\nconst waitingForInitialLogs = ref(true);\n\n/**\n * Timer used with `waitingForInitialLogs` to reveal the terminal after a delay.\n */\nlet revealTimeout: ReturnType<typeof setTimeout> | undefined;\n\nconst terminalContainer = ref<HTMLElement | null>(null);\n\nasync function initializeTerminal() {\n  isLoading.value = false;\n  await nextTick();\n  if (terminalContainer.value) {\n    terminal = new Terminal({\n      theme: {\n        background:    '#1a1a1a',\n        foreground:    '#e0e0e0',\n        cursor:        '#1a1a1a', // same as the background to effectively hide the cursor.\n        black:         '#000000',\n        red:           '#ff5555',\n        green:         '#50fa7b',\n        yellow:        '#f1fa8c',\n        blue:          '#8be9fd',\n        magenta:       '#ff79c6',\n        cyan:          '#8be9fd',\n        white:         '#f8f8f2',\n        brightBlack:   '#6272a4',\n        brightRed:     '#ff6e6e',\n        brightGreen:   '#69ff94',\n        brightYellow:  '#ffffa5',\n        brightBlue:    '#d6acff',\n        brightMagenta: '#ff92df',\n        brightCyan:    '#a4ffff',\n        brightWhite:   '#ffffff',\n      },\n      fontSize:     14,\n      fontFamily:   '\"Courier New\", \"Monaco\", monospace',\n      cursorBlink:  false,\n      disableStdin: true,\n      convertEol:   true,\n      scrollback:   50_000,\n    });\n\n    fitAddon = new FitAddon();\n    terminal.loadAddon(fitAddon);\n\n    searchAddon = new SearchAddon();\n    terminal.loadAddon(searchAddon);\n\n    terminal.loadAddon(new WebLinksAddon((event, uri) => {\n      event.preventDefault();\n      shell.openExternal(uri);\n    }));\n\n    // Disable key events to allow normal behaviour such as copy/paste.\n    terminal.attachCustomKeyEventHandler(() => false);\n\n    terminal.open(terminalContainer.value);\n\n    // Expose terminal instance for e2e testing\n    (terminalContainer.value as any).__xtermTerminal = terminal;\n\n    await nextTick();\n    resizeObserver = new ResizeObserver(fitAddon.fit.bind(fitAddon));\n    resizeObserver.observe(terminalContainer.value);\n    fitAddon.fit();\n  }\n}\n\nasync function startStreaming() {\n  try {\n    error.value = null;\n    waitingForInitialLogs.value = true;\n\n    if (!terminal) {\n      await initializeTerminal();\n    }\n\n    const streamOptions: RDXSpawnOptions = {\n      cwd:    '/',\n      stream: {\n        onOutput: (data) => {\n          const output = (data.stdout || data.stderr);\n\n          if (terminal && output) {\n            terminal.write(output);\n\n            if (waitingForInitialLogs.value) {\n              // If we're still waiting for the initial logs, delay the reveal\n              // until after all of the initial logs have streamed in.\n              clearTimeout(revealTimeout);\n\n              revealTimeout = setTimeout(() => {\n                waitingForInitialLogs.value = false;\n                nextTick(() => {\n                  fitAddon?.fit();\n                  terminal?.scrollToBottom();\n                });\n              }, 200);\n            }\n          }\n        },\n        onError: (err: Error) => {\n          console.error('Stream error:', err);\n          handleStreamError(err);\n        },\n        onClose: (code: number) => {\n          streamProcess = undefined;\n          if (code !== 0 && props.isContainerRunning) {\n            handleStreamError(new Error(`Stream closed with code ${ code }`));\n          }\n        },\n        splitOutputLines: false,\n      },\n    };\n\n    if (props.namespace) {\n      streamOptions.namespace = props.namespace;\n    }\n\n    const streamArgs = ['--follow', '--timestamps', '--tail', '10000', props.containerId];\n    streamProcess = window.ddClient.docker.cli.exec('logs', streamArgs, streamOptions);\n    reconnectAttempts = 0;\n\n    // If we haven't received any logs within 500ms, reveal the terminal anyway.\n    revealTimeout = setTimeout(() => {\n      waitingForInitialLogs.value = false;\n    }, 500);\n  } catch (err: unknown) {\n    console.error('Error starting log stream:', err);\n    waitingForInitialLogs.value = false;\n\n    const errorMessages: Record<string, string> = {\n      'No such container':  'Container not found. It may have been removed.',\n      'permission denied':  'Permission denied. Check Docker access permissions.',\n      'connection refused': 'Cannot connect to Docker. Is Docker running?',\n    };\n\n    error.value = Object.entries(errorMessages)\n      .find(([key]) => err instanceof Error && err.message.includes(key))?.[1] ??\n      (err instanceof Error ? err.message : 'Failed to fetch logs');\n  }\n}\n\nfunction stopStreaming() {\n  try {\n    streamProcess?.close();\n  } catch (err) {\n    console.error('Error stopping log stream:', err);\n  }\n  streamProcess = undefined;\n}\n\nfunction handleStreamError(err: Error) {\n  if (reconnectAttempts < maxReconnectAttempts && props.isContainerRunning) {\n    reconnectAttempts++;\n    const delay = Math.pow(2, reconnectAttempts - 1) * 1000;\n    setTimeout(startStreaming, delay);\n  } else {\n    const retryMessage = reconnectAttempts >= maxReconnectAttempts ? ' (max retries exceeded)' : '';\n\n    error.value = `Streaming error: ${ err.message }${ retryMessage }`;\n  }\n}\n\nfunction clearSearch() {\n  searchAddon?.clearDecorations();\n}\n\nfunction performSearch(searchTerm: string) {\n  clearTimeout(searchDebounceTimer);\n\n  searchDebounceTimer = setTimeout(() => {\n    if (!searchAddon) return;\n\n    searchAddon.clearDecorations();\n    if (searchTerm) {\n      try {\n        searchAddon.findNext(searchTerm);\n      } catch (err) {\n        console.error('Search error:', err);\n      }\n    }\n  }, 300);\n}\n\nfunction searchNext(searchTerm: string) {\n  if (!searchAddon || !searchTerm) return;\n  executeSearch(() => searchAddon?.findNext(searchTerm));\n}\n\nfunction searchPrevious(searchTerm: string) {\n  if (!searchAddon || !searchTerm) return;\n  executeSearch(() => searchAddon?.findPrevious(searchTerm));\n}\n\nfunction executeSearch(searchFn: () => void) {\n  try {\n    searchFn();\n  } catch (err) {\n    console.error('Search error:', err);\n  }\n}\n\nfunction cleanup() {\n  stopStreaming();\n  if (terminal) {\n    try {\n      resizeObserver?.disconnect();\n      searchAddon?.clearDecorations();\n      searchAddon?.dispose();\n      searchAddon = undefined;\n      fitAddon?.dispose();\n      fitAddon = undefined;\n      terminal.dispose();\n      terminal = undefined;\n    } catch (err) {\n      console.error('Error disposing terminal:', err);\n    }\n  }\n  clearTimeout(searchDebounceTimer);\n  clearTimeout(revealTimeout);\n}\n\nasync function initializeLogs() {\n  if (window.ddClient) {\n    await startStreaming();\n  }\n}\n\nonMounted(() => {\n  initializeLogs();\n});\n\nonBeforeUnmount(() => {\n  cleanup();\n});\n\nwatch(() => props.containerId, () => {\n  if (terminal) {\n    cleanup();\n  }\n  initializeLogs();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@xterm/xterm/css/xterm.css';\n\n.container-logs-component {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  min-height: 0;\n  overflow: hidden;\n  flex: 1;\n}\n\n.content-state {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 2.5rem;\n}\n\n.terminal-container {\n  background: #1a1a1a;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  min-height: 0;\n\n  &.terminal-hidden {\n    visibility: hidden;\n  }\n\n  :deep(.xterm) {\n    height: 100%;\n  }\n\n  :deep(.xterm-selection) {\n    overflow: hidden;\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ContainerShell.vue",
    "content": "<template>\n  <div class=\"container-shell-component\">\n    <banner\n      v-if=\"error\"\n      class=\"content-state\"\n      color=\"error\"\n      data-testid=\"error-message\"\n    >\n      <span class=\"icon icon-info-circle icon-lg\" />\n      {{ error }}\n    </banner>\n\n    <banner\n      v-else-if=\"unsupported\"\n      class=\"content-state\"\n      color=\"warning\"\n      data-testid=\"shell-unsupported\"\n    >\n      <span class=\"icon icon-info-circle icon-lg\" />\n      Shell is not supported in this container (the <code>script</code> command is not available).\n    </banner>\n\n    <banner\n      v-else-if=\"!isContainerRunning\"\n      class=\"content-state\"\n      color=\"warning\"\n      data-testid=\"shell-not-running\"\n    >\n      <span class=\"icon icon-info-circle icon-lg\" />\n      Shell is only available for running containers.\n    </banner>\n\n    <div\n      v-if=\"!isLoading && !unsupported\"\n      v-show=\"isContainerRunning\"\n      ref=\"terminalContainer\"\n      class=\"terminal-container\"\n      data-testid=\"terminal\"\n      :data-session-active=\"sessionActive ? 'true' : undefined\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { Banner } from '@rancher/components';\nimport { FitAddon } from '@xterm/addon-fit';\nimport { Terminal } from '@xterm/xterm';\nimport { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\ndefineOptions({ name: 'ContainerShell' });\n\nconst props = defineProps<{\n  containerId:         string;\n  isContainerRunning?: boolean;\n  namespace?:          string;\n}>();\n\nconst isLoading = ref(true);\nconst error = ref<string | null>(null);\nconst unsupported = ref(false);\nconst terminalContainer = ref<HTMLElement | null>(null);\n\nlet terminal: Terminal | undefined;\nlet fitAddon: FitAddon | undefined;\nlet resizeObserver: ResizeObserver | undefined;\nconst sessionActive = ref(false);\n\nasync function initializeTerminal() {\n  isLoading.value = false;\n  await nextTick();\n\n  if (!terminalContainer.value) {\n    return;\n  }\n\n  terminal = new Terminal({\n    theme: {\n      background:    '#1a1a1a',\n      foreground:    '#e0e0e0',\n      cursor:        '#e0e0e0',\n      black:         '#000000',\n      red:           '#ff5555',\n      green:         '#50fa7b',\n      yellow:        '#f1fa8c',\n      blue:          '#8be9fd',\n      magenta:       '#ff79c6',\n      cyan:          '#8be9fd',\n      white:         '#f8f8f2',\n      brightBlack:   '#6272a4',\n      brightRed:     '#ff6e6e',\n      brightGreen:   '#69ff94',\n      brightYellow:  '#ffffa5',\n      brightBlue:    '#d6acff',\n      brightMagenta: '#ff92df',\n      brightCyan:    '#a4ffff',\n      brightWhite:   '#ffffff',\n    },\n    fontSize:     14,\n    fontFamily:   '\"Courier New\", \"Monaco\", monospace',\n    cursorBlink:  true,\n    disableStdin: false,\n    convertEol:   false,\n    scrollback:   10_000,\n  });\n\n  fitAddon = new FitAddon();\n  terminal.loadAddon(fitAddon);\n  terminal.open(terminalContainer.value);\n\n  // Expose terminal instance for e2e testing\n  (terminalContainer.value as any).__xtermTerminal = terminal;\n\n  await nextTick();\n  resizeObserver = new ResizeObserver(fitAddon.fit.bind(fitAddon));\n  resizeObserver.observe(terminalContainer.value);\n  fitAddon.fit();\n\n  // Forward keyboard input to the shell process.\n  terminal.onData((data) => {\n    if (sessionActive.value) {\n      ipcRenderer.send('container-exec/input', props.containerId, data);\n    }\n  });\n}\n\nfunction handleReady(_event: any, id: string, history: string) {\n  if (id !== props.containerId) {\n    return;\n  }\n  sessionActive.value = true;\n  if (history) {\n    terminal?.write(history);\n  }\n}\n\nfunction handleOutput(_event: any, id: string, data: string) {\n  if (id !== props.containerId) {\n    return;\n  }\n  terminal?.write(data);\n}\n\nfunction handleExit(_event: any, id: string, code: number) {\n  if (id !== props.containerId) {\n    return;\n  }\n  const msg = code === 0\n    ? '\\r\\n\\x1b[33mShell session ended.\\x1b[0m\\r\\n'\n    : `\\r\\n\\x1b[31mShell session ended (exit code: ${ code }).\\x1b[0m\\r\\n`;\n\n  terminal?.write(msg);\n  sessionActive.value = false;\n}\n\nfunction handleUnsupported() {\n  unsupported.value = true;\n}\n\nasync function startShell() {\n  if (!props.isContainerRunning || !props.containerId) {\n    return;\n  }\n\n  error.value = null;\n  unsupported.value = false;\n  sessionActive.value = false;\n\n  // Remove before re-adding to prevent duplicate listeners on reconnect.\n  ipcRenderer.removeListener('container-exec/ready', handleReady);\n  ipcRenderer.removeListener('container-exec/output', handleOutput);\n  ipcRenderer.removeListener('container-exec/exit', handleExit);\n  ipcRenderer.removeListener('container-exec/unsupported', handleUnsupported);\n  ipcRenderer.on('container-exec/ready', handleReady);\n  ipcRenderer.on('container-exec/output', handleOutput);\n  ipcRenderer.on('container-exec/exit', handleExit);\n  ipcRenderer.on('container-exec/unsupported', handleUnsupported);\n\n  if (!terminal) {\n    await initializeTerminal();\n  } else {\n    terminal.clear();\n    await nextTick();\n    fitAddon?.fit();\n  }\n\n  console.log('[ContainerShell] sending container-exec/start for:', props.containerId);\n  if (props.namespace) {\n    ipcRenderer.send('container-exec/start', props.containerId, props.namespace);\n  } else {\n    ipcRenderer.send('container-exec/start', props.containerId);\n  }\n}\n\nfunction stopShell() {\n  if (sessionActive.value) {\n    ipcRenderer.send('container-exec/detach', props.containerId);\n    sessionActive.value = false;\n  }\n  ipcRenderer.removeListener('container-exec/ready', handleReady);\n  ipcRenderer.removeListener('container-exec/output', handleOutput);\n  ipcRenderer.removeListener('container-exec/exit', handleExit);\n  ipcRenderer.removeListener('container-exec/unsupported', handleUnsupported);\n}\n\nfunction cleanup() {\n  stopShell();\n  if (terminal) {\n    try {\n      resizeObserver?.disconnect();\n      fitAddon?.dispose();\n      fitAddon = undefined;\n      terminal.dispose();\n      terminal = undefined;\n    } catch (err) {\n      console.error('Error disposing terminal:', err);\n    }\n  }\n  isLoading.value = true;\n}\n\nonMounted(() => {\n  if (props.isContainerRunning) {\n    startShell();\n  }\n});\n\nonBeforeUnmount(() => {\n  cleanup();\n});\n\nwatch(() => props.containerId, () => {\n  cleanup();\n  if (props.isContainerRunning) {\n    startShell();\n  }\n});\n\ndefineExpose({ focus: () => terminal?.focus() });\n\nwatch(() => props.isContainerRunning, (running) => {\n  if (running) {\n    startShell();\n  } else {\n    // Keep IPC listeners alive so a late container-exec/ready (e.g. when\n    // checkScriptAvailable completes while isContainerRunning briefly dips)\n    // can still set data-session-active.  Only detach the session so the\n    // background process is released from this frame.\n    if (sessionActive.value) {\n      ipcRenderer.send('container-exec/detach', props.containerId);\n      sessionActive.value = false;\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@import '@xterm/xterm/css/xterm.css';\n\n.container-shell-component {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  min-height: 0;\n  overflow: hidden;\n  flex: 1;\n}\n\n.content-state {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 2.5rem;\n}\n\n.terminal-container {\n  background: #1a1a1a;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n  min-height: 0;\n\n  :deep(.xterm) {\n    height: 100%;\n  }\n\n  :deep(.xterm-selection) {\n    overflow: hidden;\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ContainerStatusBadge.vue",
    "content": "<template>\n  <badge-state\n    v-if=\"currentContainer\"\n    :color=\"isRunning ? 'bg-success' : 'bg-darker'\"\n    :label=\"containerState\"\n    data-testid=\"container-state\"\n  />\n</template>\n\n<script>\nimport { BadgeState } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport { mapTypedState } from '@pkg/entry/store';\n\nexport default defineComponent({\n  name:       'ContainerStatusBadge',\n  components: { BadgeState },\n  computed:   {\n    ...mapTypedState('container-engine', ['containers']),\n    containerId() {\n      return this.$route.params.id || '';\n    },\n    currentContainer() {\n      if (!this.containers || !this.containerId) {\n        return null;\n      }\n      return this.containers[this.containerId];\n    },\n    containerState() {\n      if (!this.currentContainer) {\n        return 'unknown';\n      }\n      return this.currentContainer.state || this.currentContainer.status || 'unknown';\n    },\n    isRunning() {\n      if (!this.currentContainer) {\n        return false;\n      }\n      return this.currentContainer.state === 'running' || this.currentContainer.status === 'Up';\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/DashboardOpen.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport { State as K8sState } from '@pkg/backend/backend';\n\nexport default defineComponent({\n  name:     'dashboard-open',\n  computed: {\n    ...mapGetters('preferences', ['getPreferences']),\n    ...mapGetters('k8sManager', { k8sState: 'getK8sState' }),\n    kubernetesEnabled(): boolean {\n      return this.getPreferences.kubernetes.enabled;\n    },\n    kubernetesStarted(): boolean {\n      return this.k8sState === K8sState.STARTED;\n    },\n  },\n  methods: {\n    openDashboard() {\n      this.$emit('open-dashboard');\n    },\n  },\n});\n</script>\n\n<template>\n  <button\n    v-if=\"kubernetesEnabled\"\n    :disabled=\"!kubernetesStarted\"\n    class=\"btn role-secondary btn-icon-text\"\n    @click=\"openDashboard\"\n  >\n    {{ t('nav.userMenu.clusterDashboard') }}\n  </button>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/DiagnosticsBody.vue",
    "content": "<script lang=\"ts\">\nimport { ToggleSwitch } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport DiagnosticsButtonRun from '@pkg/components/DiagnosticsButtonRun.vue';\nimport EmptyState from '@pkg/components/EmptyState.vue';\nimport SortableTable from '@pkg/components/SortableTable/index.vue';\nimport type { DiagnosticsResult } from '@pkg/main/diagnostics/diagnostics';\nimport { DiagnosticsCategory } from '@pkg/main/diagnostics/types';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'DiagnosticsBody',\n  components: {\n    DiagnosticsButtonRun,\n    SortableTable,\n    ToggleSwitch,\n    EmptyState,\n  },\n  props: {\n    rows: {\n      type:     Array as PropType<DiagnosticsResult[]>,\n      required: true,\n    },\n    timeLastRun: Date as PropType<Date>,\n  },\n  data() {\n    return {\n      headers: [\n        {\n          name:  'description',\n          label: 'Name',\n        },\n        {\n          name:  'mute',\n          label: 'Mute',\n          width: 76,\n        },\n      ],\n      expanded: Object.fromEntries(Object.values(DiagnosticsCategory).map(c => [c, true])) as Record<DiagnosticsCategory, boolean>,\n    };\n  },\n  computed: {\n    ...mapGetters('preferences', ['showMuted']),\n    numFailed(): number {\n      return this.rows.length - this.numMuted;\n    },\n    numMuted(): number {\n      return this.rows.filter(row => row.mute).length;\n    },\n    filteredRows(): DiagnosticsResult[] {\n      if (this.showMuted) {\n        return this.rows;\n      }\n\n      return this.rows.filter(x => !x.mute);\n    },\n    areAllRowsMuted(): boolean {\n      return !!this.rows.length && this.rows.every(x => x.mute);\n    },\n    emptyStateIcon(): string {\n      return this.areAllRowsMuted ? this.t('diagnostics.results.muted.icon') : this.t('diagnostics.results.success.icon');\n    },\n    emptyStateHeading(): string {\n      return this.areAllRowsMuted ? this.t('diagnostics.results.muted.heading') : this.t('diagnostics.results.success.heading');\n    },\n    emptyStateBody(): string {\n      return this.areAllRowsMuted ? this.t('diagnostics.results.muted.body') : this.t('diagnostics.results.success.body');\n    },\n\n    featureFixes(): boolean {\n      return !!process.env.RD_ENV_DIAGNOSTICS_FIXES;\n    },\n  },\n  methods: {\n    pluralize(count: number, unit: string): string {\n      const units = count === 1 ? unit : `${ unit }s`;\n\n      return `${ count } ${ units } ago`;\n    },\n    muteRow(isMuted: boolean, row: DiagnosticsResult) {\n      if (typeof isMuted !== 'boolean') {\n        // Because <toggle-switch> doesn't define an explicit list of events,\n        // it triggers from the underlying component too; ignore it.\n        return;\n      }\n      this.$store.dispatch('diagnostics/updateDiagnostic', { isMuted, row });\n    },\n    toggleMute() {\n      this.$store.dispatch('preferences/setShowMuted', !this.showMuted);\n    },\n    toggleExpand(group: DiagnosticsCategory) {\n      this.expanded[group] = !this.expanded[group];\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    class=\"diagnostics\"\n    data-test=\"diagnostics\"\n  >\n    <div class=\"status\">\n      <div class=\"result-info\">\n        <div class=\"item-results\">\n          <span class=\"icon icon-dot text-error\" />{{ numFailed }} failed plus {{ numMuted }} muted\n        </div>\n        <toggle-switch\n          off-label=\"Show Muted\"\n          :value=\"showMuted\"\n          @update:value=\"toggleMute\"\n        />\n      </div>\n      <div class=\"spacer\" />\n      <diagnostics-button-run\n        class=\"button-run\"\n        :time-last-run=\"timeLastRun\"\n      />\n    </div>\n    <sortable-table\n      key-field=\"id\"\n      :headers=\"headers\"\n      :rows=\"filteredRows\"\n      :paging=\"true\"\n      group-by=\"category\"\n      :search=\"false\"\n      :table-actions=\"false\"\n      :row-actions=\"false\"\n      :show-headers=\"false\"\n      :sub-rows=\"featureFixes\"\n      :sub-expandable=\"featureFixes\"\n      :sub-expand-column=\"featureFixes\"\n    >\n      <template #no-rows>\n        <td :colspan=\"headers.length + 1\">\n          <empty-state\n            :icon=\"emptyStateIcon\"\n            :heading=\"emptyStateHeading\"\n            :body=\"emptyStateBody\"\n          >\n            <template\n              v-if=\"areAllRowsMuted\"\n              #primary-action\n            >\n              <button\n                class=\"btn role-primary\"\n                @click=\"toggleMute\"\n              >\n                Show Muted\n              </button>\n            </template>\n          </empty-state>\n        </td>\n      </template>\n      <template #group-row=\"{ group }\">\n        <tr\n          :ref=\"`group-${group.ref}`\"\n          class=\"group-row\"\n          :aria-expanded=\"expanded[group.ref]\"\n        >\n          <td\n            class=\"col-description\"\n            role=\"columnheader\"\n          >\n            <div class=\"group-tab\">\n              <i\n                data-title=\"Toggle Expand\"\n                :class=\"{\n                  icon: true,\n                  'icon-chevron-right': !expanded[group.ref],\n                  'icon-chevron-down': !!expanded[group.ref],\n                }\"\n                @click.stop=\"toggleExpand(group.ref)\"\n              />\n              {{ group.ref }}\n              <span v-if=\"!expanded[group.ref]\"> ({{ group.rows.length }})</span>\n            </div>\n          </td>\n          <td\n            class=\"col-mute\"\n            role=\"columnheader\"\n          >\n            <span>Mute</span>\n          </td>\n        </tr>\n      </template>\n      <template #col:description=\"{ row }\">\n        <td>\n          <span v-html=\"row.description\" />\n          <a\n            v-if=\"row.documentation\"\n            :href=\"row.documentation\"\n            class=\"doclink\"\n          ><span class=\"icon icon-external-link\" /></a>\n        </td>\n      </template>\n      <template #col:mute=\"{ row }\">\n        <td>\n          <toggle-switch\n            class=\"mute-toggle\"\n            :data-test=\"`diagnostics-mute-row-${row.id}`\"\n            :value=\"row.mute\"\n            @update:value=\"muteRow($event, row)\"\n          />\n        </td>\n      </template>\n      <template\n        v-if=\"featureFixes\"\n        #sub-row=\"{ row }\"\n      >\n        <tr>\n          <!--We want an empty data cell so description will align with name-->\n          <td />\n          <td\n            v-if=\"row.fixes.length > 0\"\n            class=\"sub-row\"\n          >\n            {{ row.fixes.map(fix => fix.description).join('\\n') }}\n          </td>\n          <td v-else>\n            (No fixes available)\n          </td>\n          <!--Empty data cells for remaining columns for row highlight-->\n          <td\n            v-for=\"header in headers.length - 1\"\n            :key=\"header.name\"\n          />\n        </tr>\n      </template>\n    </sortable-table>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .diagnostics {\n    display: flex;\n    flex-direction: column;\n    gap: 2rem;\n\n    .status {\n      display: flex;\n\n      .spacer {\n        flex-grow: 1;\n      }\n\n      .result-info {\n        display: flex;\n        flex-direction: column;\n        gap: 1em;\n\n        .item-results {\n          display: flex;\n          flex: 1;\n          gap: 0.5rem;\n          align-items: center;\n        }\n      }\n    }\n\n    .group-row {\n      .col-description {\n        font-weight: bold;\n        .group-tab .icon {\n          cursor: pointer;\n        }\n      }\n      .col-mute {\n        text-align: center;\n        width: 0; /* minimal width, to right-align it. */\n        /* Apply the same left/right padding so columns line up correctly. */\n        padding-left: 5px;\n        padding-right: 10px;\n        & > span {\n          /* Make the column label the same width as the toggle buttons */\n          display: inline-block;\n          width: 48px;\n        }\n      }\n\n      &[aria-expanded=\"false\"] {\n        :deep(~ .main-row) {\n          visibility: collapse;\n          .toggle-container {\n            /* When using visibility:collapse, the toggle switch produces some\n            * artifacts; force it to display:none to avoid flickering. */\n            display: none;\n          }\n        }\n        .col-mute {\n          display: none;\n        }\n      }\n    }\n\n    .mute-toggle :deep(.label) {\n      /* We have no labels on the mute toggles; force them to not exist so that\n         the two sides of the table have equal padding. */\n      display: none;\n    }\n\n    .doclink {\n      margin-left: 0.1rem;\n      .icon {\n        vertical-align: baseline;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/DiagnosticsButtonRun.vue",
    "content": "<script lang=\"ts\">\nimport dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport { PropType, defineComponent } from 'vue';\n\ndayjs.extend(relativeTime);\n\nexport default defineComponent({\n  name:  'diagnostics-button-run',\n  props: { timeLastRun: Date as PropType<Date> },\n  data() {\n    return {\n      lastRunInterval: undefined as ReturnType<typeof setInterval> | undefined,\n      currentTime:     dayjs(),\n    };\n  },\n  computed: {\n    friendlyTimeLastRun(): string {\n      // Because the currentTime is updated only every second it's possible for the last-time-run to have\n      // happened after the current-time.\n      // We can't update this.currentTime because computed methods can't have side-effects, so treat an\n      // older currentTime the same as timeLastRun.\n\n      if (this.timeLastRun.valueOf() === 0) {\n        return '(Never)';\n      }\n      if (this.currentTime.valueOf() >= this.timeLastRun.valueOf()) {\n        return this.currentTime.to(dayjs(this.timeLastRun));\n      } else {\n        return this.currentTime.to(this.currentTime);\n      }\n    },\n    timeLastRunTooltip(): string {\n      return this.timeLastRun.toLocaleString();\n    },\n  },\n  mounted() {\n    this.lastRunInterval = setInterval(() => {\n      this.currentTime = dayjs();\n    }, 1000);\n  },\n  beforeUnmount() {\n    clearInterval(this.lastRunInterval);\n  },\n  methods: {\n    async onClick() {\n      await this.$store.dispatch('diagnostics/runDiagnostics');\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"diagnostics-actions\">\n    <button\n      class=\"btn btn-xs role-secondary\"\n      @click=\"onClick\"\n    >\n      <span class=\"icon icon-refresh icon-diagnostics\" />\n      Rerun\n    </button>\n    <div class=\"diagnostics-status-history\">\n      Last run: <span\n        class=\"elapsed-timespan\"\n        :title=\"timeLastRunTooltip\"\n      >{{ friendlyTimeLastRun }}</span>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .diagnostics-actions {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-end;\n    gap: 0.5rem;\n  }\n\n  .btn-xs {\n    min-height: 2.25rem;\n    max-height: 2.25rem;\n    line-height: 0.25rem;\n  }\n\n  .icon-diagnostics {\n    padding-right: 0.25rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/EmptyState.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name:  'empty-state',\n  props: {\n    icon: {\n      type:    String,\n      default: 'icon-alert',\n    },\n    heading: {\n      type:     String,\n      required: true,\n    },\n    body: {\n      type:    String,\n      default: '',\n    },\n  },\n  computed: {\n    hasPrimaryActionSlot(): boolean {\n      return !!this.$slots['primary-action'];\n    },\n    hasBody(): boolean {\n      return !!this.body || !!this.$slots['body'];\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"empty-state\">\n    <div class=\"empty-state-icon\">\n      <slot name=\"icon\">\n        <span\n          class=\"icon\"\n          :class=\"icon\"\n        />\n      </slot>\n    </div>\n    <div class=\"empty-state-heading\">\n      <slot name=\"heading\">\n        {{ heading }}\n      </slot>\n    </div>\n    <div\n      v-if=\"hasBody\"\n      class=\"empty-state-body\"\n    >\n      <slot name=\"body\">\n        {{ body }}\n      </slot>\n    </div>\n    <div\n      v-if=\"hasPrimaryActionSlot\"\n      class=\"empty-state-primary-action\"\n    >\n      <slot name=\"primary-action\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .empty-state {\n    display: flex;\n    flex-direction: column;\n    gap: 0.75rem;\n    align-items: center;\n  }\n\n  .empty-state-icon {\n    font-size: 8rem;\n    line-height: 1;\n  }\n\n  .empty-state-heading {\n    font-size: 1.875rem;\n    line-height: 2.25rem;\n  }\n\n  .empty-state-body {\n    font-size: 1rem;\n    line-height: 1.5rem;\n    text-align: center;\n  }\n\n  .empty-state-primary-action {\n    padding-top: 1.5rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/EngineSelector.vue",
    "content": "<script>\nimport { RadioGroup } from '@rancher/components';\n\nimport { ContainerEngine } from '@pkg/config/settings';\n\nexport default {\n  components: { RadioGroup },\n  props:      {\n    containerEngine: {\n      type:    String,\n      default: 'containerd',\n    },\n    row: {\n      type:    Boolean,\n      default: false,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  computed: {\n    options() {\n      return Object.values(ContainerEngine)\n        .filter(x => x !== ContainerEngine.NONE)\n        .map((x) => {\n          return {\n            label:       this.t(`containerEngine.options.${ x }.label`),\n            value:       x,\n            description: this.t(`containerEngine.options.${ x }.description`),\n          };\n        });\n    },\n  },\n  methods: {\n    updateEngine(value) {\n      this.$emit('change', value);\n    },\n  },\n};\n</script>\n\n<template>\n  <div class=\"engine-selector\">\n    <radio-group\n      name=\"containerEngine\"\n      class=\"container-engine\"\n      :class=\"{ 'locked-radio': isLocked }\"\n      :value=\"containerEngine\"\n      :options=\"options\"\n      :row=\"row\"\n      :disabled=\"isLocked\"\n      @update:value=\"updateEngine\"\n    >\n      <template #label>\n        <slot name=\"label\" />\n      </template>\n    </radio-group>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.container-engine :deep(label) {\n  color: var(--input-label);\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ExtensionsError.vue",
    "content": "<script lang=\"ts\">\nimport { PropType, defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:  'extensions-error',\n  props: {\n    extensionId: {\n      type:    String,\n      default: '',\n    },\n    error: {\n      type:    Error as PropType<Error | undefined>,\n      default: undefined,\n    },\n  },\n});\n</script>\n\n<template>\n  <div>\n    <h2>Error rendering extension: {{ extensionId }}</h2>\n    <code>\n      {{ error }}\n    </code>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  code {\n    white-space: pre-line;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ExtensionsUninstalled.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport EmptyState from '@pkg/components/EmptyState.vue';\n\nexport default defineComponent({\n  name:       'extensions-uninstalled',\n  components: { EmptyState },\n  props:      {\n    extensionId: {\n      required: true,\n      type:     String,\n    },\n  },\n  computed: {\n    emptyStateIcon(): string {\n      return this.t('extensions.icon');\n    },\n    emptyStateHeading(): string {\n      return this.t('extensions.view.emptyState.heading');\n    },\n    emptyStateBody(): string {\n      return this.t(\n        'extensions.view.emptyState.body',\n        { extensionId: `<code>${ this.extensionId }</code>` },\n        true,\n      );\n    },\n  },\n  methods: {\n    browseExtensions() {\n      this.$emit('click:browse');\n    },\n  },\n});\n</script>\n\n<template>\n  <empty-state\n    :icon=\"emptyStateIcon\"\n    :heading=\"emptyStateHeading\"\n  >\n    <template #body>\n      <span v-html=\"emptyStateBody\" />\n    </template>\n    <template #primary-action>\n      <button\n        class=\"btn role-primary\"\n        @click=\"browseExtensions\"\n      >\n        {{ t('extensions.installed.emptyState.button.text') }}\n      </button>\n    </template>\n  </empty-state>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Help.vue",
    "content": "<script lang=\"ts\">\n\nimport { shell } from 'electron';\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:  'help',\n  props: {\n    url: {\n      type:    String,\n      default: null,\n    },\n    tooltip: {\n      type:    String,\n      default: null,\n    },\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  methods: {\n    openUrl() {\n      if (!this.disabled) {\n        if (this.url) {\n          shell.openExternal(this.url);\n        } else {\n          this.$emit('open:url');\n        }\n      }\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"help-button\">\n    <i\n      v-tooltip=\"{\n        content: tooltip,\n        placement: 'left',\n      }\"\n      class=\"icon icon-question-mark\"\n      :class=\"{\n        disabled,\n      }\"\n      @click=\"openUrl\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n\n  .help-button {\n\n    .icon {\n      background: transparent;\n      color: var(--primary);\n      font-size: 1.4rem;\n      cursor: pointer;\n\n      &:hover {\n        color: var(--primary-hover-bg);\n      }\n    }\n\n    .disabled {\n      background: transparent !important;\n      color: var(--body-text);\n      opacity: 0.2;\n      cursor: default;\n\n      &:hover {\n        color: var(--body-text);\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ImageAddTabs.vue",
    "content": "<template>\n  <rd-tabbed\n    v-bind=\"$attrs\"\n    default-tab=\"pull\"\n    class=\"action-tabs\"\n    :no-content=\"true\"\n    @changed=\"tabSelected\"\n  >\n    <tab\n      :label=\"t('images.add.action.build')\"\n      name=\"build\"\n      :weight=\"0\"\n    />\n    <tab\n      :label=\"t('images.add.action.pull')\"\n      name=\"pull\"\n      :weight=\"1\"\n    />\n    <slot />\n  </rd-tabbed>\n</template>\n\n<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\n\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\n\nexport default defineComponent({\n  name: 'image-add-tabs',\n\n  components: {\n    RdTabbed,\n    Tab,\n  },\n\n  data() {\n    return { activeTab: 'pull' };\n  },\n\n  emits: ['click'],\n\n  methods: {\n    tabSelected({ tab }: { tab: any }) {\n      this.activeTab = tab.name;\n      this.$emit('click', this.activeTab);\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Images.vue",
    "content": "<!--\n  - This is the Images table in the K8s page.\n  -->\n<template>\n  <div>\n    <div\n      v-if=\"state === 'READY'\"\n      ref=\"fullWindow\"\n    >\n      <SortableTable\n        ref=\"imagesTable\"\n        class=\"imagesTable\"\n        data-test=\"imagesTableRows\"\n        key-field=\"_key\"\n        default-sort-by=\"imageName\"\n        :headers=\"headers\"\n        :rows=\"rows\"\n        no-rows-key=\"images.sortableTables.noRows\"\n        :table-actions=\"true\"\n        :paging=\"true\"\n        @selection=\"updateSelection\"\n      >\n        <template #header-middle>\n          <div class=\"header-middle\">\n            <Checkbox\n              class=\"all-images\"\n              :value=\"showAll\"\n              :label=\"t('images.manager.table.label')\"\n              :disabled=\"!supportsShowAll\"\n              @update:value=\"handleShowAllCheckbox\"\n            />\n            <div v-if=\"supportsNamespaces\">\n              <label>Namespace</label>\n              <select\n                class=\"select-namespace\"\n                :value=\"selectedNamespace\"\n                @change=\"handleChangeNamespace($event)\"\n              >\n                <option\n                  v-for=\"item in imageNamespaces\"\n                  :key=\"item\"\n                  :value=\"item\"\n                  :selected=\"item === selectedNamespace\"\n                >\n                  {{ item }}\n                </option>\n              </select>\n            </div>\n          </div>\n        </template>\n        <!-- The SortableTable component puts the Filter box goes in the #header-right slot\n             Too bad, because it means we can't use a css grid to manage the relative\n             positions of these three widgets\n        -->\n      </SortableTable>\n\n      <Card\n        v-if=\"showImageManagerOutput\"\n        :show-highlight-border=\"false\"\n        :show-actions=\"false\"\n      >\n        <template #title>\n          <div class=\"type-title\">\n            <h3>{{ t('images.manager.title') }}</h3>\n          </div>\n        </template>\n        <template #body>\n          <images-output-window\n            id=\"imageManagerOutput\"\n            ref=\"image-output-window\"\n            :current-command=\"currentCommand\"\n            :image-output-culler=\"imageOutputCuller\"\n            :show-status=\"false\"\n            :image-to-pull=\"imageToPull\"\n            @ok:process-end=\"resetCurrentCommand\"\n            @ok:show=\"toggleOutput\"\n          />\n        </template>\n      </Card>\n    </div>\n    <div v-else>\n      <h3 v-if=\"state === 'IMAGE_MANAGER_UNREADY'\">\n        {{ t('images.state.imagesUnready') }}\n      </h3>\n      <h3 v-else>\n        {{ t('images.state.unknown') }}\n      </h3>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { Card, Checkbox } from '@rancher/components';\nimport _ from 'lodash';\nimport { PropType } from 'vue';\nimport { mapMutations } from 'vuex';\n\nimport ImagesOutputWindow from '@pkg/components/ImagesOutputWindow.vue';\nimport SortableTable from '@pkg/components/SortableTable';\nimport { mapTypedState } from '@pkg/entry/store';\nimport type { IpcRendererEvents } from '@pkg/typings/electron-ipc';\nimport getImageOutputCuller, { ImageOutputCuller } from '@pkg/utils/imageOutputCuller';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { parseSi } from '@pkg/utils/units';\n\ntype Image = Parameters<IpcRendererEvents['images-changed']>[0][number];\n\ntype RowItem = Image & {\n  availableActions: {\n    label:       string;\n    action:      string;\n    enabled:     boolean;\n    icon:        string;\n    bulkable?:   boolean;\n    bulkAction?: string;\n  }[];\n  doPush:       () => void;\n  deleteImage:  () => Promise<void>;\n  deleteImages: () => Promise<void>;\n  scanImage:    () => void;\n};\n\nexport default {\n  components: {\n    Card,\n    Checkbox,\n    SortableTable,\n    ImagesOutputWindow,\n  },\n  props: {\n    images: {\n      type:     Array as PropType<Image[]>,\n      required: true,\n    },\n    protectedImages: {\n      type:    Array as PropType<string[]>,\n      default: () => [],\n    },\n    imageNamespaces: {\n      type:     Array as PropType<string[]>,\n      required: true,\n    },\n    selectedNamespace: {\n      type:    String,\n      default: 'default',\n    },\n    supportsNamespaces: {\n      type:    Boolean,\n      default: false,\n    },\n    state: {\n      type:      String as PropType<'IMAGE_MANAGER_UNREADY' | 'READY'>,\n      default:   'IMAGE_MANAGER_UNREADY',\n      validator: (value: string) => ['IMAGE_MANAGER_UNREADY', 'READY'].includes(value),\n    },\n    showAll: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  data() {\n    return {\n      currentCommand: null as string | null,\n      headers:\n      [\n        {\n          name:  'imageName',\n          label: this.t('images.manager.table.header.imageName'),\n          sort:  ['imageName', 'tag', 'imageID'],\n        },\n        {\n          name:  'tag',\n          label: this.t('images.manager.table.header.tag'),\n          sort:  ['tag', 'imageName', 'imageID'],\n        },\n        {\n          name:  'imageID',\n          label: this.t('images.manager.table.header.imageId'),\n          sort:  ['imageID', 'imageName', 'tag'],\n        },\n        {\n          name:  'size',\n          label: this.t('images.manager.table.header.size'),\n          sort:  ['si', 'imageName', 'tag'],\n        },\n      ],\n      keepImageManagerOutputWindowOpen: false,\n      imageOutputCuller:                undefined as ImageOutputCuller | undefined,\n      mainWindowScroll:                 -1,\n      selected:                         [] as RowItem[],\n      imageToPull:                      undefined,\n    };\n  },\n  computed: {\n    ...mapTypedState('action-menu', { menuImages: state => state.resources?.map((i: RowItem) => i.imageName) ?? [] }),\n    main() {\n      return document.getElementsByTagName('main')[0];\n    },\n    keyedImages() {\n      return this.images\n        .map((image, index) => {\n          return {\n            ...image,\n            si:   parseSi(image.size),\n            _key: `${ index }-${ image.imageID }-${ this.imageTag(image.tag) }`,\n          };\n        });\n    },\n    filteredImages() {\n      // Images with '<none>' or empty name are not allowed at the moment.\n      const filteredImages = this.keyedImages.filter(this.isNotNoneImage);\n\n      if (!this.supportsShowAll || this.showAll) {\n        return filteredImages;\n      }\n\n      return filteredImages\n        .filter(this.isDeletable);\n    },\n    imagesToDelete() {\n      return this.selected.filter(image => this.isDeletable(image));\n    },\n    imageIdsToDelete() {\n      return this.imagesToDelete\n        .map(this.getTaggedImage);\n    },\n    rows(): RowItem[] {\n      return this.filteredImages\n        .map(image => ({\n          ...image,\n          // The `availableActions` property is used by the ActionMenu to fill\n          // out the menu entries.\n          availableActions: [\n            {\n              label:   this.t('images.manager.table.action.push'),\n              action:  'doPush',\n              enabled: this.isPushable(image),\n              icon:    'icon icon-upload',\n            },\n            {\n              label:      this.t('images.manager.table.action.delete'),\n              action:     'deleteImage',\n              enabled:    this.isDeletable(image),\n              icon:       'icon icon-delete',\n              bulkable:   true,\n              bulkAction: 'deleteImages',\n            },\n            {\n              label:   this.t('images.manager.table.action.scan'),\n              action:  'scanImage',\n              enabled: true,\n              icon:    'icon icon-info-circle',\n            },\n          ].filter(x => x.enabled),\n          // ActionMenu callbacks - SortableTable assumes that these methods live\n          // on the rows directly.\n          doPush:       this.doPush.bind(this, image),\n          deleteImage:  this.deleteImage.bind(this, image),\n          deleteImages: this.deleteImages.bind(this),\n          scanImage:    this.scanImage.bind(this, image),\n        }));\n    },\n    showImageManagerOutput() {\n      return this.keepImageManagerOutputWindowOpen;\n    },\n    supportsShowAll() {\n      return this.selectedNamespace === 'k8s.io';\n    },\n  },\n\n  watch: {\n    rows: {\n      handler(newRows: RowItem[]) {\n        if (this.menuImages.some(name => newRows.map(r => r.imageName).includes(name))) {\n          this.hideMenu();\n        }\n      },\n      deep: true,\n    },\n  },\n\n  methods: {\n    ...mapMutations('action-menu', { hideMenu: 'hide' }),\n    updateSelection(val: RowItem[]) {\n      this.selected = val;\n    },\n    startImageManagerOutput() {\n      this.keepImageManagerOutputWindowOpen = true;\n      this.scrollToOutputWindow();\n    },\n    scrollToOutputWindow() {\n      this.$nextTick(() => {\n        if (this.main) {\n          // move to the bottom\n          this.main.scrollTop = this.main.scrollHeight;\n        }\n      });\n    },\n    scrollToTop() {\n      this.$nextTick(() => {\n        if (this.main) {\n          try {\n            this.main.scrollTop = this.mainWindowScroll;\n          } catch (e) {\n            console.log(`Trying to reset scroll to ${ this.mainWindowScroll }, got error:`, e);\n          }\n        }\n\n        this.mainWindowScroll = -1;\n      });\n    },\n    startRunningCommand(command: Parameters<typeof getImageOutputCuller>[0]) {\n      this.imageOutputCuller = getImageOutputCuller(command);\n    },\n    async deleteImages() {\n      const message = `Delete ${ this.imagesToDelete.length } ${ this.imagesToDelete.length > 1 ? 'images' : 'image' }?`;\n      const detail = this.imageIdsToDelete.join('\\n');\n\n      const options: Electron.MessageBoxOptions = {\n        message,\n        detail,\n        type:      'question',\n        buttons:   ['Yes', 'No'],\n        defaultId: 1,\n        title:     'Confirming image deletion',\n        cancelId:  1,\n      };\n\n      const result = await ipcRenderer.invoke('show-message-box', options);\n\n      if (result.response === 1) {\n        return;\n      }\n\n      this.currentCommand = `delete ${ this.imageIdsToDelete }`;\n      this.mainWindowScroll = this.main.scrollTop;\n      this.startRunningCommand('delete');\n      ipcRenderer.send('do-image-deletion-batch', this.imageIdsToDelete);\n      this.startImageManagerOutput();\n    },\n    async deleteImage(obj: Image) {\n      const options: Electron.MessageBoxOptions = {\n        message:   `Delete image ${ obj.imageName }:${ obj.tag }?`,\n        type:      'question',\n        buttons:   ['Yes', 'No'],\n        defaultId: 1,\n        title:     'Confirming image deletion',\n        cancelId:  1,\n      };\n      const result = await ipcRenderer.invoke('show-message-box', options);\n\n      if (result.response === 1) {\n        return;\n      }\n      this.currentCommand = `delete ${ obj.imageName }:${ obj.tag }`;\n      this.mainWindowScroll = this.main.scrollTop;\n      this.startRunningCommand('delete');\n\n      ipcRenderer.send('do-image-deletion', obj.imageName.trim(), this.getTaggedImage(obj));\n\n      this.startImageManagerOutput();\n    },\n    doPush(obj: Image) {\n      this.currentCommand = `push ${ obj.imageName }:${ obj.tag }`;\n      this.mainWindowScroll = this.main.scrollTop;\n      this.startRunningCommand('push');\n      ipcRenderer.send('do-image-push', obj.imageName.trim(), obj.imageID.trim(), obj.tag.trim());\n    },\n    scanImage(obj: Image) {\n      const taggedImageName = `${ obj.imageName.trim() }:${ this.imageTag(obj.tag) }`;\n\n      this.$router.push({ name: 'images-scans-image-name', params: { image: taggedImageName, namespace: this.selectedNamespace } });\n    },\n    imageTag(tag: string) {\n      return tag === '<none>' ? 'latest' : `${ tag.trim() }`;\n    },\n    isNotNoneImage(row: Image) {\n      return row.imageName && row.imageName !== '<none>';\n    },\n    isDeletable(row: Image) {\n      return !this.protectedImages.includes(row.imageName);\n    },\n    isPushable(row: Image) {\n      // If it doesn't contain a '/', it's certainly not pushable,\n      // but having a '/' isn't sufficient, but it's all we have to go on.\n      return this.isDeletable(row) && row.imageName.includes('/');\n    },\n    hasDropdownActions(row: Image) {\n      return this.isDeletable(row);\n    },\n    handleShowAllCheckbox(value: boolean) {\n      this.$emit('toggledShowAll', value);\n    },\n    handleChangeNamespace(event: Event) {\n      this.$emit('switchNamespace', (event.target as HTMLSelectElement).value);\n    },\n    resetCurrentCommand() {\n      this.currentCommand = null;\n    },\n    toggleOutput(val: boolean) {\n      this.keepImageManagerOutputWindowOpen = val;\n\n      if (!val && this.mainWindowScroll >= 0) {\n        this.scrollToTop();\n      }\n    },\n    getTaggedImage(image: Image) {\n      return image.tag !== '<none>' ? `${ image.imageName }:${ image.tag }` : `${ image.imageName }@${ image.digest }`;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n  .labeled-input > .btn {\n    position: absolute;\n    bottom: -1px;\n    right: -1px;\n    border-start-start-radius: var(--border-radius);\n    border-radius: var(--border-radius) 0 0 0;\n  }\n\n  @keyframes highlightFade {\n    from {\n      background: var(--accent-btn);\n    } to {\n      background: transparent;\n    }\n  }\n\n  .select-namespace {\n    max-width: 24rem;\n    min-width: 8rem;\n  }\n\n  .header-middle {\n    display: flex;\n    align-items: flex-end;\n    gap: 1rem;\n    height: 100%;\n  }\n\n  .all-images {\n    margin-bottom: 12px;\n  }\n\n  .imagesTable :deep(.search-box) {\n    align-self: flex-end;\n  }\n  .imagesTable :deep(.bulk) {\n    align-self: flex-end;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ImagesButtonAdd.vue",
    "content": "<template>\n  <button\n    data-test=\"addImageButton\"\n    type=\"button\"\n    class=\"btn btn-xs role-secondary\"\n    @click=\"route\"\n  >\n    {{ t('images.action.add') }}\n  </button>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'images-button-add',\n\n  methods: {\n    route() {\n      this.$router.push({ name: 'images-add' });\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n\n  .btn-xs {\n    min-height: 2.25rem;\n    max-height: 2.25rem;\n    line-height: 0.25rem;\n  }\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ImagesFormAdd.vue",
    "content": "<template>\n  <div class=\"image-input\">\n    <labeled-input\n      v-model:value=\"image\"\n      v-focus\n      type=\"text\"\n      class=\"image\"\n      :disabled=\"isInputDisabled\"\n      :placeholder=\"inputPlaceholder\"\n      :label=\"inputLabel\"\n      @keyup.enter=\"submit\"\n    />\n    <button\n      class=\"btn role-primary btn-lg\"\n      :disabled=\"isButtonDisabled\"\n      @click=\"submit\"\n    >\n      {{ buttonText }}\n    </button>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { LabeledInput } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'images-form-add',\n\n  components: { LabeledInput },\n\n  props: {\n    currentCommand: {\n      type:    String,\n      default: '',\n    },\n    keepOutputWindowOpen: {\n      type:    Boolean,\n      default: false,\n    },\n    action: {\n      type:     String,\n      required: true,\n      validator(value: string) {\n        return ['pull', 'build'].includes(value);\n      },\n    },\n  },\n\n  data() {\n    return { image: '' };\n  },\n\n  computed: {\n    isButtonDisabled(): boolean {\n      return this.isInputDisabled || !this.image;\n    },\n    isInputDisabled(): boolean {\n      return !~this.currentCommand || this.keepOutputWindowOpen;\n    },\n    isActionPull(): boolean {\n      return this.action === 'pull';\n    },\n    buttonText(): string {\n      return this.t(`images.manager.input.${ this.action }.button`);\n    },\n    inputLabel(): string {\n      return this.t(`images.manager.input.${ this.action }.label`);\n    },\n    inputPlaceholder(): string {\n      return this.t(`images.manager.input.${ this.action }.placeholder`);\n    },\n  },\n\n  methods: {\n    submit() {\n      this.$emit('click', { action: this.action, image: this.image.trim() });\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .image {\n    min-width: 32rem;\n  }\n\n  .btn {\n    margin-bottom: 14px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ImagesOutputWindow.vue",
    "content": "<script lang=\"ts\">\n\nimport { Banner } from '@rancher/components';\nimport { defineComponent, PropType } from 'vue';\n\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\nimport { ImageOutputCuller } from '@pkg/utils/imageOutputCuller';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name: 'images-output-window',\n\n  components: {\n    Banner,\n    LoadingIndicator,\n  },\n\n  props: {\n    currentCommand: {\n      type:    String as PropType<string | null>,\n      default: null,\n    },\n    action: {\n      type:    String,\n      default: '',\n    },\n    imageOutputCuller: {\n      type:    Object as PropType<ImageOutputCuller | undefined>,\n      default: undefined,\n    },\n    showStatus: {\n      type:    Boolean,\n      default: true,\n    },\n    imageToPull: {\n      type:    String as PropType<string | undefined>,\n      default: undefined,\n    },\n  },\n\n  data() {\n    return {\n      keepImageManagerOutputWindowOpen: false,\n      postCloseOutputWindowHandler:     null as (() => void) | null,\n      imageManagerOutput:               '',\n      completionStatus:                 false,\n    };\n  },\n\n  computed: {\n    imageManagerProcessIsFinished(): boolean {\n      return !this.currentCommand;\n    },\n    imageManagerProcessFinishedWithSuccess() {\n      return this.imageManagerProcessIsFinished && this.completionStatus;\n    },\n    imageManagerProcessFinishedWithFailure() {\n      return this.imageManagerProcessIsFinished && !this.completionStatus;\n    },\n    actionCapitalized(): string {\n      const action = this.action;\n\n      return `${ action?.charAt(0).toUpperCase() }${ action.slice(1) }`;\n    },\n    loadingText(): string {\n      return this.t('images.add.loadingText', { action: this.actionCapitalized });\n    },\n    successText() {\n      const pastTense = this.t(`images.add.action.pastTense.${ this.action }`);\n\n      return this.t('images.add.successText', { action: pastTense });\n    },\n    errorText(): string {\n      return this.t('images.add.errorText', { action: this.action, image: this.imageToPull }, true);\n    },\n  },\n\n  mounted() {\n    ipcRenderer.on('images-process-output', (_event, data) => {\n      this.appendImageManagerOutput(data);\n    });\n\n    ipcRenderer.on('images-process-ended', (_event, status) => {\n      this.handleProcessEnd(status);\n    });\n\n    ipcRenderer.on('images-process-cancelled', () => {\n      this.handleProcessCancelled();\n    });\n  },\n\n  methods: {\n    closeOutputWindow() {\n      this.keepImageManagerOutputWindowOpen = false;\n      this.$emit('ok:show', this.keepImageManagerOutputWindowOpen);\n      if (this.postCloseOutputWindowHandler) {\n        this.postCloseOutputWindowHandler();\n        this.postCloseOutputWindowHandler = null;\n      } else {\n        this.imageManagerOutput = '';\n      }\n    },\n    appendImageManagerOutput(data: string) {\n      if (!this.imageOutputCuller) {\n        this.imageManagerOutput += data;\n      } else {\n        this.imageOutputCuller.addData(data);\n        this.imageManagerOutput = this.imageOutputCuller.getProcessedData();\n      }\n      // Delay moving to the output-window until there's a reason to\n      if (!this.keepImageManagerOutputWindowOpen) {\n        if (!data?.trim()) {\n        // Could be just a newline at the end of processing, so wait\n          return;\n        }\n        this.keepImageManagerOutputWindowOpen = true;\n        this.$emit('ok:show', this.keepImageManagerOutputWindowOpen);\n      }\n    },\n    handleProcessEnd(status: number) {\n      if (this.imageOutputCuller) {\n        // Don't know what would make this null, but it happens on windows sometimes\n        this.imageManagerOutput = this.imageOutputCuller.getProcessedData();\n      }\n\n      this.completionStatus = status === 0;\n      this.$emit('ok:process-end', this.completionStatus);\n      if (!this.keepImageManagerOutputWindowOpen) {\n        this.closeOutputWindow();\n      }\n    },\n    handleProcessCancelled() {\n      this.closeOutputWindow();\n      this.$emit('ok:process-end');\n    },\n  },\n});\n</script>\n\n<template>\n  <div>\n    <template v-if=\"showStatus\">\n      <slot\n        name=\"loading\"\n        :is-loading=\"!imageManagerProcessIsFinished\"\n      >\n        <banner\n          v-if=\"!imageManagerProcessIsFinished\"\n        >\n          <loading-indicator>\n            {{ loadingText }}\n          </loading-indicator>\n        </banner>\n      </slot>\n      <slot\n        name=\"error\"\n        :has-error=\"imageManagerProcessFinishedWithFailure\"\n      >\n        <banner\n          v-if=\"imageManagerProcessFinishedWithFailure\"\n          color=\"error\"\n        >\n          <span class=\"icon icon-info-circle icon-lg \" />\n          {{ errorText }}\n        </banner>\n      </slot>\n      <slot\n        name=\"success\"\n        :is-success=\"imageManagerProcessFinishedWithSuccess\"\n      >\n        <banner\n          v-if=\"imageManagerProcessFinishedWithSuccess\"\n          color=\"success\"\n        >\n          <span class=\"icon icon-checkmark icon-lg \" />\n          {{ successText }}\n        </banner>\n      </slot>\n    </template>\n    <div\n      v-if=\"imageManagerProcessIsFinished\"\n      class=\"actions\"\n    >\n      <button\n        class=\"role-tertiary btn-close\"\n        @click=\"closeOutputWindow\"\n      >\n        {{ t('images.manager.close') }}\n      </button>\n    </div>\n    <textarea\n      id=\"imageManagerOutput\"\n      ref=\"outputWindow\"\n      v-model=\"imageManagerOutput\"\n      :class=\"{\n        success: imageManagerProcessFinishedWithSuccess,\n        failure: imageManagerProcessFinishedWithFailure,\n      }\"\n      rows=\"10\"\n      readonly=\"true\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  textarea#imageManagerOutput {\n    font-family: monospace;\n    font-size: smaller;\n  }\n\n  textarea#imageManagerOutput.success {\n    border: 2px solid var(--success);\n  }\n\n  textarea#imageManagerOutput.failure {\n    border: 2px solid var(--error);\n  }\n\n  .actions {\n    margin-top: 15px;\n    margin-bottom: 15px;\n    display: flex;\n    flex-flow: row-reverse;\n  }\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/ImagesScanResults.vue",
    "content": "<script>\nimport { BadgeState } from '@rancher/components';\n\nimport SortableTable from '@pkg/components/SortableTable';\n\nconst SEVERITY_MAP = {\n  LOW: {\n    color: 'bg-darker',\n    id:    0,\n  },\n  MEDIUM: {\n    color: 'bg-info',\n    id:    1,\n  },\n  HIGH: {\n    color: 'bg-warning',\n    id:    2,\n  },\n  CRITICAL: {\n    color: 'bg-error',\n    id:    3,\n  },\n};\n\nexport default {\n  components: {\n    SortableTable,\n    BadgeState,\n  },\n\n  props: {\n    image: {\n      type:     String,\n      required: true,\n    },\n    tableData: {\n      type:    Array,\n      default: () => [],\n    },\n  },\n\n  data() {\n    return {\n      headers: [\n        {\n          name:  'Severity',\n          label: this.t('images.scan.results.headers.severity'),\n          sort:  ['SeverityId:desc', 'PkgName', 'InstalledVersion'],\n        },\n        {\n          name:  'PkgName',\n          label: this.t('images.scan.results.headers.package'),\n          sort:  ['PkgName', 'Severity', 'InstalledVersion'],\n        },\n        {\n          name:  'VulnerabilityID',\n          label: this.t('images.scan.results.headers.vulnerabilityId'),\n          sort:  ['VulnerabilityID', 'Severity', 'PkgName', 'InstalledVersion'],\n        },\n        {\n          name:  'InstalledVersion',\n          label: this.t('images.scan.results.headers.installed'),\n        },\n        {\n          name:  'FixedVersion',\n          label: this.t('images.scan.results.headers.fixed'),\n        },\n      ],\n    };\n  },\n\n  computed: {\n    rows() {\n      return this.tableData\n        .map(({ Severity, ...rest }) => {\n          return {\n            SeverityId: this.id(Severity),\n            Severity,\n            ...rest,\n          };\n        });\n    },\n    criticalCount() {\n      return this.issueCount(SEVERITY_MAP.CRITICAL.id);\n    },\n    criticalLabel() {\n      return `${ this.t('images.scan.labels.critical') }: ${ this.criticalCount }`;\n    },\n    highCount() {\n      return this.issueCount(SEVERITY_MAP.HIGH.id);\n    },\n    highLabel() {\n      return `${ this.t('images.scan.labels.high') }: ${ this.highCount }`;\n    },\n    mediumCount() {\n      return this.issueCount(SEVERITY_MAP.MEDIUM.id);\n    },\n    mediumLabel() {\n      return `${ this.t('images.scan.labels.medium') }: ${ this.mediumCount }`;\n    },\n    lowCount() {\n      return this.issueCount(SEVERITY_MAP.LOW.id);\n    },\n    lowLabel() {\n      return `${ this.t('images.scan.labels.low') }: ${ this.lowCount }`;\n    },\n    issueSum() {\n      return this.criticalCount + this.highCount + this.mediumCount + this.lowCount;\n    },\n    issueLabel() {\n      return `${ this.t('images.scan.labels.issuesFound') }: ${ this.issueSum }`;\n    },\n  },\n\n  methods: {\n    color(severity) {\n      return SEVERITY_MAP[severity]?.color;\n    },\n    id(severity) {\n      return SEVERITY_MAP[severity]?.id;\n    },\n    issueCount(severity) {\n      return this.rows.filter(row => row.SeverityId === severity).length;\n    },\n  },\n};\n</script>\n\n<template>\n  <sortable-table\n    :headers=\"headers\"\n    :rows=\"rows\"\n    key-field=\"id\"\n    default-sort-by=\"Severity\"\n    :table-actions=\"false\"\n    :row-actions=\"false\"\n    :paging=\"true\"\n    :sub-rows=\"true\"\n    :sub-expandable=\"true\"\n    :sub-expand-column=\"true\"\n    :rows-per-page=\"25\"\n  >\n    <template #header-left>\n      <div class=\"issue-header\">\n        <badge-state\n          v-if=\"issueSum\"\n          :label=\"issueLabel\"\n        />\n        <badge-state\n          v-if=\"criticalCount\"\n          color=\"bg-error\"\n          :label=\"criticalLabel\"\n        />\n        <badge-state\n          v-if=\"highCount\"\n          color=\"bg-warning\"\n          :label=\"highLabel\"\n        />\n        <badge-state\n          v-if=\"mediumCount\"\n          color=\"bg-info\"\n          :label=\"mediumLabel\"\n        />\n        <badge-state\n          v-if=\"lowCount\"\n          color=\"bg-darker\"\n          :label=\"lowLabel\"\n        />\n      </div>\n    </template>\n    <template #col:VulnerabilityID=\"{ row }\">\n      <td>\n        <span>\n          <a :href=\"row.PrimaryURL\">{{ row.VulnerabilityID }}</a>\n        </span>\n      </td>\n    </template>\n    <template #col:Severity=\"{ row }\">\n      <td>\n        <badge-state\n          :label=\"row.Severity\"\n          :color=\"color(row.Severity)\"\n        />\n      </td>\n    </template>\n    <template #sub-row=\"{ row, fullColspan }\">\n      <td\n        :colspan=\"fullColspan\"\n        class=\"sub-row\"\n      >\n        <div class=\"details\">\n          <div class=\"col description\">\n            <section>\n              <section class=\"title\">\n                {{ t('images.scan.details.description') }}\n              </section>\n              {{ row.Description }}\n            </section>\n          </div>\n          <div class=\"col\">\n            <section>\n              <section class=\"title\">\n                {{ t('images.scan.details.primaryUrl') }}\n              </section>\n              <a :href=\"row.PrimaryURL\">{{ row.PrimaryURL }}</a>\n            </section>\n            <section>\n              <section class=\"title\">\n                {{ t('images.scan.details.references') }}\n              </section>\n              <section\n                v-for=\"(reference, idx) in row.References\"\n                :key=\"idx\"\n                class=\"reference\"\n              >\n                <a :href=\"reference\"> {{ reference }} </a>\n              </section>\n            </section>\n          </div>\n        </div>\n      </td>\n    </template>\n  </sortable-table>\n</template>\n\n<style lang=\"scss\" scoped>\n  .sub-row {\n    background-color: var(--body-bg);\n    padding-left: 1rem;\n    padding-right: 1rem;\n  }\n\n  .details {\n    display: grid;\n    grid-auto-flow: column;\n    grid-auto-columns: minmax(0, 1fr);\n    gap: 1em;\n\n    .col {\n      display: flex;\n      flex-direction: column;\n\n      section {\n        margin-bottom: 1.5rem;\n      }\n\n      .title, .reference {\n        margin-bottom: 0.5rem;\n      }\n\n      .reference a {\n        overflow-wrap: break-word;\n      }\n\n      .title {\n        color: var(--muted);\n      }\n    }\n  }\n\n  .issue-header {\n    display: flex;\n    gap: 0.25rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/IncompatiblePreferencesAlert.vue",
    "content": "<script lang=\"ts\">\nimport { Banner } from '@rancher/components';\nimport { PropType, defineComponent } from 'vue';\n\nimport type { NavItemName } from '@pkg/config/transientSettings';\n\nexport type CompatiblePrefs = {\n  /** title is the string to display to the user to describe the preference. */\n  title:       string,\n  /** navItemName is the nav item (top level navigation) to switch to. */\n  navItemName: NavItemName;\n  /** tabName is the tab to switch to, if any */\n  tabName?:    string,\n}[];\n\nexport default defineComponent({\n  name:       'incompatible-preferences-alert',\n  components: { Banner },\n  props:      {\n    compatiblePrefs: {\n      type:     Array as PropType<CompatiblePrefs>,\n      required: true,\n    },\n    mode: {\n      type:    String as PropType<'selected' | 'disabled'>,\n      default: 'selected',\n    },\n  },\n  computed: {\n    messagePost(): string {\n      switch (this.mode) {\n      case 'selected':\n        return this.t('preferences.incompatibleTypeWarningPostSelected');\n      case 'disabled':\n        return this.t('preferences.incompatibleTypeWarningPostDisabled');\n      }\n\n      return this.t('preferences.incompatibleTypeWarningPostSelected');\n    },\n  },\n  methods: {\n    navigate(info: CompatiblePrefs[number]) {\n      (this.$root as any).navigate(info.navItemName, info.tabName);\n    },\n  },\n});\n</script>\n\n<template>\n  <banner\n    v-if=\"compatiblePrefs.length > 0\"\n    color=\"warning\"\n  >\n    <p>{{ t('preferences.incompatibleTypeWarningPre') }}</p>\n    <p\n      v-for=\"(pref, index) in compatiblePrefs\"\n      :key=\"index\"\n    >\n      <a\n        href=\"#\"\n        @click.prevent=\"navigate(pref)\"\n      >\n        {{ pref.title }}\n      </a>\n      <span v-if=\"compatiblePrefs.length > 2 && index < (compatiblePrefs.length - 2)\">\n        {{ ',' }}\n      </span>\n      <span v-else-if=\"compatiblePrefs.length >= 2 && index === (compatiblePrefs.length - 2)\">\n        {{ t('preferences.incompatiblePrefWarningOr') }}\n      </span>\n    </p>\n    <p>{{ messagePost }}</p>\n  </banner>\n</template>\n\n<style scoped lang=\"scss\">\n  :deep(.banner__content) {\n    flex-wrap: wrap;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/LoadingIndicator.vue",
    "content": "<template>\n  <section class=\"loading-indicator\">\n    <slot name=\"icon\">\n      <svg\n        class=\"icon loading-icon\"\n        viewBox=\"0 0 32 32\"\n      >\n        <title>spinner</title>\n        <path d=\"M16 7.904c0 0 0 0-0 0-0.707 0-1.28-0.573-1.28-1.28 0 0 0 0 0-0v0-2.464c0-0.707 0.573-1.28 1.28-1.28s1.28 0.573 1.28 1.28v0 2.464c0 0 0 0 0 0 0 0.707-0.573 1.28-1.28 1.28 0 0 0 0-0 0v0z\" />\n        <path d=\"M16 29.12c0 0 0 0-0 0-0.707 0-1.28-0.573-1.28-1.28 0 0 0 0 0-0v0-2.464c0-0.707 0.573-1.28 1.28-1.28s1.28 0.573 1.28 1.28v0 2.464c0 0 0 0 0 0 0 0.707-0.573 1.28-1.28 1.28 0 0 0 0-0 0v0z\" />\n        <path d=\"M6.624 17.28h-2.464c-0.707 0-1.28-0.573-1.28-1.28s0.573-1.28 1.28-1.28v0h2.464c0.707 0 1.28 0.573 1.28 1.28s-0.573 1.28-1.28 1.28v0z\" />\n        <path d=\"M27.84 17.28h-2.464c-0.707 0-1.28-0.573-1.28-1.28s0.573-1.28 1.28-1.28v0h2.464c0.707 0 1.28 0.573 1.28 1.28s-0.573 1.28-1.28 1.28v0z\" />\n        <path d=\"M11.279 9.18c-0 0-0.001 0-0.001 0-0.469 0-0.88-0.253-1.102-0.63l-0.003-0.006-1.241-2.129c-0.109-0.185-0.174-0.407-0.174-0.645 0-0.707 0.573-1.28 1.28-1.28 0.469 0 0.88 0.253 1.103 0.63l0.003 0.006 1.241 2.129c0.109 0.185 0.174 0.407 0.174 0.644 0 0.707-0.572 1.279-1.279 1.28h-0z\" />\n        <path d=\"M21.964 27.509c-0 0-0.001 0-0.001 0-0.469 0-0.88-0.253-1.102-0.63l-0.003-0.006-1.241-2.129c-0.109-0.185-0.174-0.407-0.174-0.645 0-0.707 0.573-1.28 1.28-1.28 0.469 0 0.88 0.253 1.103 0.63l0.003 0.006 1.241 2.129c0.109 0.185 0.174 0.407 0.174 0.644 0 0.707-0.572 1.279-1.279 1.28h-0z\" />\n        <path d=\"M5.772 23.243c-0 0-0.001 0-0.001 0-0.707 0-1.28-0.573-1.28-1.28 0-0.469 0.253-0.88 0.629-1.103l0.006-0.003 2.129-1.241c0.185-0.109 0.407-0.174 0.645-0.174 0.707 0 1.28 0.573 1.28 1.28 0 0.469-0.253 0.88-0.63 1.103l-0.006 0.003-2.129 1.241c-0.184 0.11-0.406 0.174-0.643 0.174-0 0-0 0-0 0v0z\" />\n        <path d=\"M24.102 12.558c-0 0-0.001 0-0.001 0-0.707 0-1.28-0.573-1.28-1.28 0-0.469 0.253-0.88 0.629-1.103l0.006-0.003 2.129-1.241c0.185-0.109 0.407-0.174 0.645-0.174 0.707 0 1.28 0.573 1.28 1.28 0 0.469-0.253 0.88-0.63 1.103l-0.006 0.003-2.129 1.241c-0.184 0.11-0.406 0.174-0.643 0.174-0 0-0 0-0 0v0z\" />\n        <path d=\"M7.878 12.593c-0 0-0 0-0 0-0.235 0-0.455-0.064-0.645-0.175l0.006 0.003-2.134-1.232c-0.385-0.225-0.64-0.637-0.64-1.108 0-0.707 0.573-1.28 1.28-1.28 0.236 0 0.456 0.064 0.646 0.175l-0.006-0.003 2.134 1.232c0.385 0.225 0.64 0.637 0.64 1.109 0 0.707-0.573 1.28-1.28 1.28-0 0-0.001 0-0.001 0h0z\" />\n        <path d=\"M26.253 23.199c-0 0-0 0-0 0-0.235 0-0.455-0.064-0.645-0.175l0.006 0.003-2.134-1.232c-0.386-0.225-0.642-0.638-0.642-1.11 0-0.707 0.573-1.28 1.28-1.28 0.236 0 0.458 0.064 0.648 0.176l-0.006-0.003 2.134 1.232c0.385 0.225 0.64 0.637 0.64 1.109 0 0.707-0.573 1.28-1.28 1.28-0 0-0.001 0-0.001 0h0z\" />\n        <path d=\"M10.080 27.535c-0.706-0.001-1.279-0.574-1.279-1.28 0-0.236 0.064-0.456 0.175-0.646l-0.003 0.006 1.232-2.134c0.225-0.386 0.637-0.641 1.109-0.641 0.707 0 1.28 0.573 1.28 1.28 0 0.236-0.064 0.457-0.175 0.647l0.003-0.006-1.232 2.134c-0.225 0.385-0.637 0.64-1.108 0.64-0 0-0.001 0-0.001 0h0z\" />\n        <path d=\"M20.686 9.16c-0.706-0.001-1.279-0.574-1.279-1.28 0-0.236 0.064-0.456 0.175-0.646l-0.003 0.006 1.232-2.134c0.225-0.385 0.637-0.64 1.108-0.64 0.707 0 1.28 0.573 1.28 1.28 0 0.236-0.064 0.456-0.175 0.646l0.003-0.006-1.232 2.134c-0.225 0.385-0.637 0.64-1.108 0.64-0 0-0.001 0-0.001 0h0z\" />\n      </svg>\n    </slot>\n    <span>\n      <slot>\n        {{ loadingText }}\n      </slot>\n    </span>\n  </section>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name: 'loading-indicator',\n\n  props: {\n    loadingText: {\n      type:    String,\n      default: '',\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .loading-indicator {\n    display: flex;\n    align-items: center;\n    gap: 0.25rem;\n\n    span {\n      line-height: 2rem;\n      color: var(--primary);\n    }\n\n    path {\n      fill: var(--primary);\n    }\n  }\n\n  .icon {\n    width: 1.5rem;\n    height: 1.5rem;\n  }\n\n  .loading-icon {\n    animation:spin 4s linear infinite;\n  }\n\n  @keyframes spin {\n    100% {\n      transform: rotate(360deg);\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/MarketplaceCard.vue",
    "content": "<template>\n  <div class=\"extensions mt-10\">\n    <div class=\"extensions-card\">\n      <div class=\"extensions-card-header\">\n        <img\n          :src=\"extension.logo\"\n          alt=\"\"\n        >\n        <div class=\"extensions-card-header-top\">\n          <span class=\"extensions-card-header-title\">{{ extension.title }}</span>\n          <span class=\"extensions-card-header-subtitle\">{{\n            extension.publisher\n          }}</span>\n          <span class=\"extensions-card-header-version\">{{ extension.version }}</span>\n        </div>\n      </div>\n      <div class=\"extensions-card-content\">\n        <span>{{ extension.short_description }}</span>\n      </div>\n\n      <a\n        :href=\"extensionLink\"\n        :title=\"extensionLink\"\n        target=\"_blank\"\n      >\n        {{ t('marketplace.moreInfo') }}\n        <i class=\"icon icon-external-link \" />\n      </a>\n    </div>\n    <div class=\"extensions-card-footer\">\n      <Banner\n        v-if=\"error\"\n        color=\"error\"\n        class=\"banner\"\n      >\n        {{ error }}\n      </Banner>\n      <!-- install button -->\n      <button\n        v-if=\"!error && !currentAction && !installed\"\n        data-test=\"button-install\"\n        class=\"role-primary btn btn-xs\"\n        @click=\"appInstallation('install')\"\n      >\n        {{ t('marketplace.labels.install') }}\n      </button>\n      <!-- upgrade button -->\n      <button\n        v-if=\"!error && !currentAction && installed?.canUpgrade\"\n        class=\"role-primary btn btn-xs\"\n        @click=\"appInstallation('upgrade')\"\n      >\n        {{ t('marketplace.labels.upgrade') }}\n      </button>\n      <!-- uninstall button -->\n      <button\n        v-if=\"!error && !currentAction && installed\"\n        data-test=\"button-uninstall\"\n        class=\"role-danger btn btn-xs\"\n        @click=\"appInstallation('uninstall')\"\n      >\n        {{ t('marketplace.labels.uninstall') }}\n      </button>\n      <!-- \"loading\" fake button -->\n      <button\n        v-if=\"!error && currentAction\"\n        data-test=\"button-loading\"\n        class=\"role-primary btn btn-xs\"\n        disabled=\"true\"\n      >\n        <span\n          name=\"loading\"\n          is-loading=\"true\"\n        >\n          <loading-indicator>{{ loadingLabel }}</loading-indicator>\n        </span>\n      </button>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { Banner } from '@rancher/components';\n\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\nimport type { ExtensionState, MarketplaceData } from '@pkg/store/extensions';\n\nimport type { PropType } from 'vue';\n\ntype action = 'install' | 'uninstall' | 'upgrade';\n\nexport default {\n  components: { LoadingIndicator, Banner },\n  props:      {\n    extension: {\n      type:     Object as PropType<MarketplaceData>,\n      required: true,\n    },\n    installed: {\n      type:     Object as undefined | PropType<ExtensionState>,\n      required: false,\n      default:  undefined,\n    },\n  },\n  data() {\n    return {\n      currentAction: null as null | action,\n      error:         null as string | null,\n      response:      null,\n      bannerActive:  false,\n    };\n  },\n  computed: {\n    versionedExtension() {\n      return `${ this.extensionWithoutVersion }:${ this.extension.version }`;\n    },\n    extensionWithoutVersion() {\n      return this.extension.slug;\n    },\n    extensionLink() {\n      // Try to use labels, if available.\n      const preferredLabel = 'io.rancherdesktop.extension.more-info';\n\n      const preferredURL = this.extension.labels[preferredLabel]?.trim();\n\n      if (preferredURL) {\n        return preferredURL;\n      }\n\n      if (!/^[^./]+\\//.test(this.extension.slug)) {\n        return `https://${ this.extension.slug }`;\n      }\n\n      return `https://hub.docker.com/extensions/${ this.extension.slug }`;\n    },\n    loadingLabel() {\n      return this.t(`marketplace.loading.${ this.currentAction }`);\n    },\n  },\n\n  methods: {\n    resetBanners() {\n      this.error = null;\n    },\n    async appInstallation(action: action) {\n      this.currentAction = action;\n      this.resetBanners();\n      const id = action === 'uninstall' ? this.extensionWithoutVersion : this.versionedExtension;\n      const verb = action === 'uninstall' ? 'uninstall' : 'install'; // upgrades are installs\n\n      try {\n        const result = await this.$store.dispatch(`extensions/${ verb }`, { id });\n\n        if (typeof result === 'string') {\n          this.error = result;\n          this.currentAction = null;\n        } else if (result) {\n          this.currentAction = null;\n        }\n      } finally {\n        setTimeout(() => {\n          this.resetBanners();\n        }, 3_000);\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.extensions {\n  height: 100%;\n  box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);\n  border: 1px solid var(--muted);\n  border-top: 4px solid var(--muted);\n  transition: all 0.2s ease-in-out;\n  padding: 20px;\n  display: flex;\n  flex-direction: column;\n\n  .extensions-card {\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n    flex-grow: 1;\n\n    &-header {\n      align-content: flex;\n      align-items: flex-start;\n      display: flex;\n      flex-direction: row;\n      gap: 10px;\n\n      &-top {\n        flex: 1;\n        display: grid;\n        grid-template:\n          \"title title title\"\n          \"subtitle . version\"\n          / max-content 1fr max-content;\n        gap: 5px;\n      }\n\n      &-title {\n        grid-area: title;\n        font-size: 1.2rem;\n        font-weight: 600;\n      }\n\n      &-subtitle {\n        grid-area: subtitle;\n        font-size: 0.8rem;\n        font-weight: 400;\n      }\n\n      &-version {\n        grid-area: version;\n        font-size: 0.8rem;\n        font-weight: 400;\n      }\n\n      img {\n        max-width: 40px;\n        width: 100%;\n        max-height: 40px;\n      }\n    }\n  }\n\n  &-card-footer {\n    margin-top: 15px;\n\n    .banner {\n      margin: 0;\n    }\n\n    button:not(:first-of-type) {\n      margin-left: 10px;\n    }\n  }\n\n  &-more-info {\n    position: relative;\n    background: red;;\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/MarketplaceCatalog.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport MarketplaceCard from '@pkg/components/MarketplaceCard.vue';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport type { ExtensionState, MarketplaceData } from '@pkg/store/extensions';\n\ntype ExtensionData = MarketplaceData;\n\nexport default defineComponent({\n  name:       'marketplace-catalog',\n  components: { MarketplaceCard },\n  data() {\n    return { searchValue: '' };\n  },\n  computed: {\n    ...mapGetters('preferences', ['getPreferences']),\n    ...mapGetters('extensions', ['installedExtensions', 'marketData']) as {\n      installedExtensions: () => ExtensionState[],\n      marketData:          () => MarketplaceData[],\n    },\n    containerEngine(): string {\n      return this.getPreferences.containerEngine.name;\n    },\n    isMobyActive(): boolean {\n      return this.containerEngine === ContainerEngine.MOBY;\n    },\n    allowedListEnabled(): boolean {\n      return this.getPreferences.application.extensions.allowed.enabled;\n    },\n    allowedExtensions(): string[] {\n      return this.getPreferences.application.extensions.allowed.list;\n    },\n    filteredExtensions(): ExtensionData[] {\n      let tempExtensions = this.marketData\n        .filter((item) => {\n          return this.isAllowed(item.slug);\n        });\n\n      if (this.searchValue) {\n        tempExtensions = tempExtensions.filter((item) => {\n          return item.title\n            .toLowerCase()\n            .includes(this.searchValue.toLowerCase());\n        });\n      }\n      const filteredExtensions = tempExtensions.filter(item => this.isMobyActive || item.containerd_compatible);\n      const collator = new Intl.Collator('en', { sensitivity: 'base' });\n\n      return filteredExtensions.sort((s1, s2) => {\n        return collator.compare(s1.title, s2.title);\n      });\n    },\n  },\n  methods: {\n    installedVersion(slug: string) {\n      return this.installedExtensions.find(item => item.id === slug)?.version;\n    },\n    isAllowed(slug: string) {\n      return !this.allowedListEnabled || this.allowedExtensions.includes(slug);\n    },\n    installedExtension(slug: string) {\n      return this.installedExtensions.find(item => item.id === slug);\n    },\n  },\n});\n</script>\n\n<template>\n  <div>\n    <input\n      v-model=\"searchValue\"\n      type=\"text\"\n      placeholder=\"Search\"\n    >\n    <div\n      v-if=\"filteredExtensions.length === 0\"\n      class=\"extensions-content-missing\"\n    >\n      {{ t('marketplace.noResults') }}\n    </div>\n    <div\n      class=\"extensions-content\"\n    >\n      <div\n        v-for=\"item in filteredExtensions\"\n        :key=\"item.slug\"\n        :v-if=\"filteredExtensions\"\n      >\n        <MarketplaceCard\n          :extension=\"item\"\n          :installed=\"installedExtension(item.slug)\"\n          :data-test=\"`extension-card-${item.title.toLowerCase()}`\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.extensions-content {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));\n  gap: 20px;\n  margin-top: 20px;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/MountTypeSelector.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { RadioButton, RadioGroup } from '@rancher/components';\nimport semver from 'semver';\nimport { defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport IncompatiblePreferencesAlert, { CompatiblePrefs } from '@pkg/components/IncompatiblePreferencesAlert.vue';\nimport RdInput from '@pkg/components/RdInput.vue';\nimport RdSelect from '@pkg/components/RdSelect.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport TooltipIcon from '@pkg/components/form/TooltipIcon.vue';\nimport {\n  CacheMode, MountType, ProtocolVersion, SecurityModel, Settings, VMType,\n} from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  components: {\n    TooltipIcon,\n    IncompatiblePreferencesAlert,\n    RadioGroup,\n    RdFieldset,\n    RadioButton,\n    RdSelect,\n    RdInput,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    ...mapState('transientSettings', ['macOsVersion', 'isArm']),\n    options(): {\n      label:           string,\n      value:           MountType,\n      description:     string,\n      experimental:    boolean,\n      disabled:        boolean,\n      compatiblePrefs: CompatiblePrefs | []\n    }[] {\n      const defaultOption = MountType.REVERSE_SSHFS;\n\n      return Object.values(MountType)\n        .sort((x, y) => { // Non-experimental (default) option should go first\n          return x === defaultOption ? -1 : y === defaultOption ? 1 : 0;\n        })\n        .map((x) => {\n          return {\n            label:           this.t(`virtualMachine.mount.type.options.${ x }.label`),\n            value:           x,\n            description:     this.t(`virtualMachine.mount.type.options.${ x }.description`, {}, true),\n            experimental:    x === MountType.NINEP,\n            disabled:        x === MountType.VIRTIOFS && this.virtIoFsDisabled,\n            compatiblePrefs: this.getCompatiblePrefs(x),\n          };\n        });\n    },\n    groupName() {\n      return 'mountType';\n    },\n    ninePSelected(): boolean {\n      return this.preferences.virtualMachine.mount.type === MountType.NINEP;\n    },\n    virtIoFsDisabled(): boolean {\n      // virtiofs should only be disabled on macOS WITHOUT the possibility to select the VM type VZ. VZ doesn't need to\n      // be selected, yet. We're going to show a warning banner in that case.\n      return os.platform() === 'darwin' &&\n        this.macOsVersion &&\n        (semver.lt(this.macOsVersion.version, '13.0.0') || (this.isArm && semver.lt(this.macOsVersion.version, '13.3.0')));\n    },\n    arch(): string {\n      return this.isArm ? 'arm64' : 'x64';\n    },\n  },\n  methods: {\n    ninePOptions(setting: string) {\n      let items: CacheMode[] | ProtocolVersion[] | SecurityModel[] = [];\n      let selected: CacheMode | ProtocolVersion | SecurityModel;\n\n      switch (setting) {\n      case 'cacheMode':\n        items = Object.values(CacheMode);\n        selected = this.preferences.experimental.virtualMachine.mount['9p'].cacheMode;\n        break;\n      case 'protocolVersion':\n        items = Object.values(ProtocolVersion);\n        selected = this.preferences.experimental.virtualMachine.mount['9p'].protocolVersion;\n        break;\n      case 'securityModel':\n        items = Object.values(SecurityModel);\n        selected = this.preferences.experimental.virtualMachine.mount['9p'].securityModel;\n        break;\n      }\n\n      return items\n        .map((x: SecurityModel | ProtocolVersion | CacheMode) => {\n          return {\n            label:    this.t(`virtualMachine.mount.type.options.9p.options.${ setting }.options.${ x.replace('.', '') }`),\n            value:    x,\n            selected: x === selected,\n          };\n        });\n    },\n    updateValue<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$emit('update', property, value);\n    },\n    disabledVirtIoFsTooltip(disabled: boolean): { content: string } | Record<string, never> {\n      let tooltip = {};\n\n      if (disabled) {\n        tooltip = { content: this.t(`prefs.onlyWithVZ_${ this.arch }`, undefined, true) };\n      }\n\n      return tooltip;\n    },\n    getCompatiblePrefs(mountType: MountType): CompatiblePrefs | [] {\n      const compatiblePrefs: CompatiblePrefs = [];\n\n      if (os.platform() === 'darwin') {\n        switch (mountType) {\n        case MountType.NINEP:\n          if (this.preferences.virtualMachine.type === VMType.VZ) {\n            compatiblePrefs.push( {\n              title: VMType.QEMU, navItemName: 'Virtual Machine', tabName: 'emulation',\n            } );\n          }\n          break;\n        case MountType.VIRTIOFS:\n          if (this.preferences.virtualMachine.type === VMType.QEMU) {\n            compatiblePrefs.push( {\n              title: VMType.VZ, navItemName: 'Virtual Machine', tabName: 'emulation',\n            } );\n          }\n          break;\n        }\n      }\n\n      return compatiblePrefs;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"mount-type-selector\">\n    <div class=\"row\">\n      <div class=\"col span-6\">\n        <rd-fieldset\n          data-test=\"mountType\"\n          :legend-text=\"t('virtualMachine.mount.type.legend')\"\n          :is-locked=\"isPreferenceLocked('virtualMachine.mount.type')\"\n        >\n          <template #default=\"{ isLocked }\">\n            <radio-group\n              :name=\"groupName\"\n              :options=\"options\"\n              :disabled=\"isLocked\"\n              :class=\"{ 'locked-radio': isLocked }\"\n            >\n              <template\n                v-for=\"(option, index) in options\"\n                #[index]=\"{ isDisabled }\"\n              >\n                <radio-button\n                  :key=\"groupName + '-' + index\"\n                  v-tooltip=\"disabledVirtIoFsTooltip(option.disabled)\"\n                  :name=\"groupName\"\n                  :value=\"preferences.virtualMachine.mount.type\"\n                  :val=\"option.value\"\n                  :disabled=\"option.disabled || isDisabled\"\n                  :data-test=\"option.label\"\n                  @update:value=\"updateValue('virtualMachine.mount.type', $event)\"\n                >\n                  <template #label>\n                    {{ option.label }}\n                    <tooltip-icon\n                      v-if=\"option.experimental\"\n                    />\n                  </template>\n                  <template #description>\n                    {{ option.description }}\n                    <incompatible-preferences-alert\n                      v-if=\"option.value === preferences.virtualMachine.mount.type\"\n                      :compatible-prefs=\"option.compatiblePrefs\"\n                    />\n                  </template>\n                </radio-button>\n              </template>\n            </radio-group>\n          </template>\n        </rd-fieldset>\n      </div>\n      <div\n        v-if=\"ninePSelected\"\n        class=\"col span-6 mount-type-sub-options\"\n      >\n        <rd-fieldset\n          data-test=\"cacheMode\"\n          :legend-text=\"t('virtualMachine.mount.type.options.9p.options.cacheMode.legend')\"\n          :legend-tooltip=\"t('virtualMachine.mount.type.options.9p.options.cacheMode.tooltip')\"\n        >\n          <rd-select\n            :model-value=\"preferences.experimental.virtualMachine.mount['9p'].cacheMode\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.mount.9p.cacheMode')\"\n            @change=\"updateValue('experimental.virtualMachine.mount.9p.cacheMode', $event.target.value)\"\n          >\n            <option\n              v-for=\"item in ninePOptions('cacheMode')\"\n              :key=\"item.label\"\n              :value=\"item.value\"\n              :selected=\"item.selected\"\n            >\n              {{ item.label }}\n            </option>\n          </rd-select>\n        </rd-fieldset>\n        <rd-fieldset\n          data-test=\"msizeInKib\"\n          :legend-text=\"t('virtualMachine.mount.type.options.9p.options.mSizeInKib.legend')\"\n          :legend-tooltip=\"t('virtualMachine.mount.type.options.9p.options.mSizeInKib.tooltip')\"\n        >\n          <rd-input\n            type=\"number\"\n            :value=\"preferences.experimental.virtualMachine.mount['9p'].msizeInKib\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.mount.9p.msizeInKib')\"\n            min=\"4\"\n            @input=\"updateValue('experimental.virtualMachine.mount.9p.msizeInKib', $event.target.value)\"\n          />\n        </rd-fieldset>\n        <rd-fieldset\n          data-test=\"protocolVersion\"\n          :legend-text=\"t('virtualMachine.mount.type.options.9p.options.protocolVersion.legend')\"\n          :legend-tooltip=\"t('virtualMachine.mount.type.options.9p.options.protocolVersion.tooltip')\"\n        >\n          <rd-select\n            :model-value=\"preferences.experimental.virtualMachine.mount['9p'].protocolVersion\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.mount.9p.protocolVersion')\"\n            @change=\"updateValue('experimental.virtualMachine.mount.9p.protocolVersion', $event.target.value)\"\n          >\n            <option\n              v-for=\"item in ninePOptions('protocolVersion')\"\n              :key=\"item.label\"\n              :value=\"item.value\"\n              :selected=\"item.selected\"\n            >\n              {{ item.label }}\n            </option>\n          </rd-select>\n        </rd-fieldset>\n        <rd-fieldset\n          data-test=\"securityModel\"\n          :legend-text=\"t('virtualMachine.mount.type.options.9p.options.securityModel.legend')\"\n          :legend-tooltip=\"t('virtualMachine.mount.type.options.9p.options.securityModel.tooltip')\"\n        >\n          <rd-select\n            :model-value=\"preferences.experimental.virtualMachine.mount['9p'].securityModel\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.mount.9p.securityModel')\"\n            @change=\"updateValue('experimental.virtualMachine.mount.9p.securityModel', $event.target.value)\"\n          >\n            <option\n              v-for=\"item in ninePOptions('securityModel')\"\n              :key=\"item.label\"\n              :value=\"item.value\"\n              :selected=\"item.selected\"\n            >\n              {{ item.label }}\n            </option>\n          </rd-select>\n        </rd-fieldset>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .mount-type-sub-options {\n    border-left: 1px solid var(--border);\n    padding-left: 1rem;\n    display: flex;\n    flex-direction: column;\n    gap: 0.5rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Nav.vue",
    "content": "<template>\n  <nav>\n    <ul>\n      <li\n        v-for=\"item in items\"\n        :key=\"item.route\"\n        :item=\"item.route\"\n      >\n        <RouterLink\n          :class=\"{ 'rd-link-active': isRouteActive(item.route) }\"\n          :to=\"item.route\"\n        >\n          {{ routes[item.route].name }}\n          <badge-state\n            v-if=\"item.error\"\n            color=\"bg-error\"\n            class=\"nav-badge\"\n            :label=\"item.error.toString()\"\n          />\n          <i\n            v-if=\"item.experimental\"\n            v-tooltip=\"{\n              content: t('prefs.experimental', undefined, true),\n              placement: 'right',\n            }\"\n            :class=\"`icon icon-flask`\"\n          />\n        </RouterLink>\n      </li>\n    </ul>\n    <hr v-if=\"extensionsWithUI.length\">\n    <div class=\"nav-extensions\">\n      <RouterLink\n        v-for=\"extension in extensionsWithUI\"\n        :key=\"extension.id\"\n        :data-test=\"`extension-nav-${extension.metadata.ui['dashboard-tab'].title.toLowerCase()}`\"\n        :to=\"extensionRoute(extension)\"\n      >\n        <nav-item :id=\"`extension:${extension.id}`\">\n          <template #before>\n            <nav-icon-extension :extension-id=\"extension.id\" />\n          </template>\n          {{ extension.metadata.ui['dashboard-tab'].title }}\n        </nav-item>\n      </RouterLink>\n    </div>\n    <div class=\"nav-button-container\">\n      <dashboard-button\n        data-testid=\"dashboard-button\"\n        class=\"nav-button\"\n        @open-dashboard=\"openDashboard\"\n      />\n      <preferences-button\n        data-testid=\"preferences-button\"\n        class=\"nav-button\"\n        @open-preferences=\"openPreferences\"\n      />\n    </div>\n  </nav>\n</template>\n\n<script lang=\"ts\">\nimport os from 'os';\n\nimport { BadgeState } from '@rancher/components';\nimport { PropType, defineComponent } from 'vue';\nimport { RouteRecordPublic } from 'vue-router';\n\nimport NavIconExtension from './NavIconExtension.vue';\nimport NavItem from './NavItem.vue';\n\nimport DashboardButton from '@pkg/components/DashboardOpen.vue';\nimport PreferencesButton from '@pkg/components/Preferences/ButtonOpen.vue';\nimport router from '@pkg/entry/router';\nimport type { ExtensionState } from '@pkg/store/extensions';\nimport { hexEncode } from '@pkg/utils/string-encode';\n\ntype ExtensionWithUI = ExtensionState & {\n  metadata: { ui: { 'dashboard-tab': { title: string } } };\n};\n\nexport default defineComponent({\n  name:       'Nav',\n  components: {\n    BadgeState,\n    NavItem,\n    NavIconExtension,\n    DashboardButton,\n    PreferencesButton,\n  },\n  props: {\n    items: {\n      type:      Array as PropType<{ route: string; error?: number; experimental?: boolean }[]>,\n      required:  true,\n      validator: (value: { route: string, error?: number }[]) => {\n        const routes = router.getRoutes().reduce((paths: Record<string, RouteRecordPublic>, route) => {\n          paths[route.path] = route;\n\n          return paths;\n        }, {});\n\n        return value && (value.length > 0) && value.every(({ route }) => {\n          const result = route in routes;\n\n          if (!result) {\n            console.error(`<Nav> error: path ${ JSON.stringify(route) } not found in routes ${ JSON.stringify(Object.keys(routes)) }`);\n          }\n\n          return result;\n        });\n      },\n    },\n    extensions: {\n      type:     Array as PropType<ExtensionState[]>,\n      required: true,\n    },\n  },\n  data() {\n    return {\n      // Generate a route (path) to route entry mapping, so that we can pick out\n      // their names based on the paths given.\n      routes: this.$router.getRoutes().reduce((paths: Record<string, RouteRecordPublic>, route) => {\n        paths[route.path] = route;\n        if (route.name === 'Supporting Utilities' && os.platform() === 'win32') {\n          route.name = 'WSL Integrations';\n        }\n\n        return paths;\n      }, {}),\n    };\n  },\n  computed: {\n    extensionsWithUI(): ExtensionWithUI[] {\n      function hasUI(ext: ExtensionState): ext is ExtensionWithUI {\n        return !!ext.metadata.ui?.['dashboard-tab']?.title;\n      }\n\n      return this.extensions.filter<ExtensionWithUI>(hasUI);\n    },\n  },\n  methods: {\n    extensionRoute({ id, metadata }: { id: string, metadata: any }) {\n      const { ui: { 'dashboard-tab': { root, src } } } = metadata;\n\n      return {\n        name:   'rdx-root-src-id',\n        params: {\n          root,\n          src,\n          id: hexEncode(id),\n        },\n      };\n    },\n    isRouteActive(route: string): boolean {\n      // It is needed e.g. for sub-route /images/add not matching /Images\n      // Prevents the parent item \"Extensions\" to be shown as active if an extension child (e.g. Epinio, Logs Explorer,\n      // ...) is selected.\n      if (this.$route.name === 'rdx-root-src-id') {\n        return false;\n      }\n\n      return this.$route.path.toLowerCase().startsWith(route.toLowerCase());\n    },\n    openPreferences(): void {\n      this.$emit('open-preferences');\n    },\n    openDashboard(): void {\n      this.$emit('open-dashboard');\n    },\n  },\n});\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped lang=\"scss\">\nnav {\n    background-color: var(--nav-bg);\n    padding: 0;\n    margin: 0;\n    padding: 20px 0;\n    display: flex;\n    flex-direction: column;\n\n    a {\n      text-decoration: none;\n    }\n\n    .nav-extensions {\n      overflow: auto;\n      flex-grow: 1\n    }\n}\n\nul {\n    margin: 0;\n    padding: 0;\n    list-style-type: none;\n\n    li {\n        padding: 0;\n\n        a {\n            display: flex;\n            align-items: center;\n            gap: 0.25rem;\n            color: var(--body-text);\n            text-decoration: none;\n            font-size: 1.125rem;\n            line-height: 1.75rem;\n            padding: 0.5rem 0.75rem;\n            outline: none;\n        }\n\n        a:is(.router-link-active, .rd-link-active) {\n            background-color: var(--nav-active);\n        }\n    }\n}\n\na {\n  &:hover {\n    text-decoration: none;\n  }\n\n  &:is(.router-link-active, .rd-link-active) :deep(div) {\n    background-color: var(--nav-active);\n  }\n}\n\n.nav-badge {\n  line-height: initial;\n  letter-spacing: initial;\n  font-size: 0.75rem;\n}\n\n.nav-button-container {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n\n  .nav-button {\n    flex: 1;\n    margin: 5px 10px 0px 10px;\n    justify-content: center;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/NavIconExtension.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { hexEncode } from '@pkg/utils/string-encode';\n\nconst knownMonochromeIcons = [\n  'ghcr.io/rancher-sandbox/epinio-desktop-extension',\n  'julianb90/tachometer',\n  'prakhar1989/dive-in',\n  'joycelin79/newman-extension',\n  'ivancurkovic046/excalidraw-docker-extension', // spellcheck-ignore-line\n];\n\nexport default defineComponent({\n  name:  'nav-icon-extension',\n  props: {\n    extensionId: {\n      type:     String,\n      required: true,\n    },\n  },\n  data() {\n    return { imageError: false };\n  },\n  computed: {\n    imageUri(): string {\n      return `x-rd-extension://${ hexEncode(this.extensionId) }/icon.svg`;\n    },\n    isKnownMonochrome(): boolean {\n      return !!this.extensionId && knownMonochromeIcons.includes(this.extensionId.split(':')[0]);\n    },\n  },\n  methods: {\n    handleImageError(): void {\n      this.imageError = true;\n    },\n  },\n});\n</script>\n\n<template>\n  <img\n    v-if=\"!imageError\"\n    class=\"extension-icon\"\n    :class=\"{\n      'known-monochrome': isKnownMonochrome,\n    }\"\n    :src=\"imageUri\"\n    @error=\"handleImageError\"\n  >\n  <i\n    v-else\n    class=\"icon icon-extension icon-lg\"\n  />\n</template>\n\n<style lang=\"scss\" scoped>\n  .extension-icon {\n    max-height: 1.5rem;\n    max-width: 1.5rem;\n  }\n\n  /**\n    * Change the icon colors by setting a class 'known-monochrome' containing dark theme properties.\n    */\n  @media (prefers-color-scheme: dark) {\n    .known-monochrome {\n      filter: brightness(0) invert(100%) grayscale(1) brightness(2);\n    }\n  }\n\n  /**\n    * Change the icon colors by setting a class 'known-monochrome' containing light theme properties.\n    */\n  @media (prefers-color-scheme: light) {\n    .known-monochrome {\n      filter: brightness(0) grayscale(1) brightness(4);\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/NavItem.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name:  'nav-item',\n  props: { id: { type: String, default: '' } },\n});\n</script>\n\n<template>\n  <div\n    v-bind=\"$attrs\"\n    class=\"nav-item\"\n    :data-id=\"id\"\n  >\n    <div\n      v-if=\"$slots.before\"\n      class=\"before\"\n    >\n      <slot name=\"before\" />\n    </div>\n    <slot name=\"default\">\n      Extensions\n    </slot>\n    <slot name=\"after\" />\n  </div>\n</template>\n\n<style scoped lang=\"scss\">\n  .nav-item {\n    color: var(--body-text);\n    text-decoration: none;\n    font-size: 1.125rem;\n    line-height: 1.75rem;\n    padding: 0.5rem 0.75rem;\n    display: flex;\n    gap: 0.5rem;\n    align-items: center;\n\n    &:hover {\n      cursor: pointer;\n    }\n  }\n\n  .before {\n    min-width: 1.25rem;\n    max-width: 1.25rem;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n\n    img {\n      width: 100%;\n      height: auto;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/NetworkStatus.vue",
    "content": "<script>\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { networkStatus } from '@pkg/utils/networks';\n\nexport default defineComponent({\n  name:  'network-status',\n  props: {\n    icon: {\n      type:    String,\n      default: '',\n    },\n    isStatusBarItem: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  data() {\n    return { networkStatus: true };\n  },\n  computed: {\n    networkStatusLabel() {\n      return this.networkStatus ? networkStatus.CONNECTED : networkStatus.OFFLINE;\n    },\n    getTooltip() {\n      return {\n        content:     `<b>${ this.t('product.networkStatus') }</b>: ${ this.networkStatusLabel }`,\n        html:        true,\n        placement:   'top',\n        popperClass: 'tooltip-footer',\n      };\n    },\n  },\n  mounted() {\n    this.onNetworkStatusUpdate(window.navigator.onLine);\n    ipcRenderer.on('update-network-status', (event, status) => {\n      this.onNetworkStatusUpdate(status);\n    });\n    window.addEventListener('online', () => {\n      this.onNetworkStatusUpdate(true);\n    });\n    window.addEventListener('offline', () => {\n      this.onNetworkStatusUpdate(false);\n    });\n    // This event is triggered when the Preferences page is revealed (among other times).\n    // If the network status changed while the window was closed, this will update it.\n    window.addEventListener('pageshow', () => {\n      this.onNetworkStatusUpdate(window.navigator.onLine);\n    });\n  },\n  methods: {\n    onNetworkStatusUpdate(status) {\n      this.$data.networkStatus = status;\n    },\n  },\n});\n</script>\n\n<template>\n  <span\n    v-tooltip=\"isStatusBarItem ? getTooltip : {}\"\n    class=\"networkStatusInfo\"\n  >\n    <i\n      v-if=\"icon\"\n      class=\"item-icon\"\n      :class=\"icon\"\n    />\n    <span\n      class=\"item-label\"\n    >\n      <b>{{ t('product.networkStatus') }}:</b>\n    </span>\n    <span\n      class=\"item-value\"\n    >\n      {{ networkStatusLabel }}\n    </span>\n    <i\n      v-if=\"isStatusBarItem\"\n      class=\"icon icon-dot\"\n      :class=\"networkStatus ? 'online' : 'offline'\"\n    />\n  </span>\n</template>\n\n<style scoped lang=\"scss\">\n.networkStatusInfo {\n  .icon-dot {\n    font-size: 8px;\n    padding: 2px;\n\n    &.online {\n      color: #32CD32FF;\n    }\n    &.offline {\n      color: #B30000;\n    }\n  }\n\n  @media (max-width: 900px) {\n    .icon-dot {\n      vertical-align: top;\n      padding: 0;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Notifications.vue",
    "content": "<!--\n  - This is a component which displays notifications which will be stacked on\n  - top of each other, in a bar above the rest of the content.\n  -->\n<template>\n  <div class=\"stack\">\n    <div class=\"contents\">\n      <slot />\n    </div>\n    <div class=\"banner-background\">\n      <Banner\n        v-for=\"item in items\"\n        :key=\"item.key\"\n        :color=\"item.color\"\n        :closable=\"true\"\n        class=\"banner\"\n        @close=\"close(item.key)\"\n      >\n        {{ item.message }}\n      </Banner>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { Banner } from '@rancher/components';\n\nexport default {\n  components: { Banner },\n  props:      {\n    notifications: {\n      type: Array,\n      default() {\n        return [];\n      },\n      validator(values) {\n        return values.every(value => value.key && value.color && value.message);\n      },\n    },\n  },\n  data() {\n    return { closed: {} };\n  },\n  computed: {\n    items() {\n      // Remove closed marker for any notifications that no longer exist, so\n      // that they will show up again if they get re-added.\n      for (const key of Object.keys(this.closed)) {\n        if (!this.notifications.some(item => item.key === key)) {\n          this.$delete(this.closed, key);\n        }\n      }\n\n      return this.notifications.filter(v => !this.closed[v.key]);\n    },\n  },\n  methods: {\n    close(key) {\n      this.$set(this.closed, key, true);\n    },\n  },\n};\n</script>\n\n<style scoped lang=\"scss\">\n  .stack {\n    display: flex;\n    flex-direction: column;\n  }\n  .contents {\n    flex: 1;\n  }\n  .banner-background {\n    flex: none;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/PathManagementSelector.vue",
    "content": "<script lang=\"ts\">\nimport { RadioButton, RadioGroup } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\n\ninterface pathManagementOptions {\n  label:       string,\n  value:       PathManagementStrategy,\n  description: string\n}\n\nexport default defineComponent({\n  name:       'path-management-selector',\n  components: {\n    RadioGroup,\n    RadioButton,\n  },\n  props: {\n    value: {\n      type:    String,\n      default: PathManagementStrategy.RcFiles,\n    },\n    row: {\n      type:    Boolean,\n      default: false,\n    },\n    showLabel: {\n      type:    Boolean,\n      default: true,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  emits:    ['input'],\n  computed: {\n    options(): pathManagementOptions[] {\n      return [\n        {\n          label:       this.t('pathManagement.options.rcFiles.label'),\n          value:       PathManagementStrategy.RcFiles,\n          description: this.t('pathManagement.options.rcFiles.description', { }, true),\n        },\n        {\n          label:       this.t('pathManagement.options.manual.label'),\n          value:       PathManagementStrategy.Manual,\n          description: this.t('pathManagement.options.manual.description', { }, true),\n        },\n      ];\n    },\n    groupName(): string {\n      return 'pathManagement';\n    },\n    label(): string {\n      return this.showLabel ? this.t('pathManagement.label') : '';\n    },\n    tooltip(): string {\n      return this.showLabel ? this.t('pathManagement.tooltip', { }, true) : '';\n    },\n  },\n  methods: {\n    updateVal(value: PathManagementStrategy) {\n      this.$emit('input', value);\n    },\n  },\n});\n</script>\n\n<template>\n  <radio-group\n    :name=\"groupName\"\n    :label=\"label\"\n    :tooltip=\"tooltip\"\n    :value=\"value\"\n    :options=\"options\"\n    :row=\"row\"\n    :disabled=\"isLocked\"\n    :class=\"{ 'locked-radio': isLocked }\"\n    class=\"path-management\"\n    @update:value=\"updateVal\"\n  >\n    <template\n      v-if=\"showLabel\"\n      #label\n    >\n      <slot name=\"label\" />\n    </template>\n    <template #1=\"{ option, isDisabled, mode }\">\n      <radio-button\n        v-bind=\"$attrs\"\n        :key=\"groupName + '-' + option.value\"\n        :name=\"groupName\"\n        :value=\"value\"\n        :label=\"option.label\"\n        :val=\"option.value\"\n        :disabled=\"isDisabled\"\n        :mode=\"mode\"\n        @update:value=\"updateVal(option.value)\"\n      >\n        <template #description>\n          <span v-html=\"option.description\" />\n        </template>\n      </radio-button>\n    </template>\n  </radio-group>\n</template>\n\n<style lang=\"scss\" scoped>\n.path-management :deep(code) {\n  user-select: text;\n  cursor: text;\n  padding: 2px;\n}\n\n.path-management :deep(label) {\n  color: var(--input-label);\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/PortForwarding.vue",
    "content": "<!--\n  - This is the PortForwarding table in the K8s page.\n  -->\n<template>\n  <div>\n    <Banner\n      v-if=\"errorMessage\"\n      color=\"error\"\n      :closable=\"true\"\n      class=\"banner\"\n      @close=\"emitCloseError\"\n    >\n      {{ errorMessage }}\n    </Banner>\n    <SortableTable\n      :headers=\"headers\"\n      :rows=\"rows\"\n      no-rows-key=\"portForwarding.sortableTables.noRows\"\n      key-field=\"key\"\n      default-sort-by=\"namespace\"\n      :table-actions=\"false\"\n      :paging=\"true\"\n      :row-actions-width=\"parseInt(95, 10)\"\n    >\n      <template #header-middle>\n        <div class=\"header-middle\">\n          <Checkbox\n            class=\"kubernetes-services\"\n            :label=\"'Include Kubernetes services'\"\n            :value=\"includeKubernetesServices\"\n            :disabled=\"!isRunning || kubernetesIsDisabled\"\n            @update:value=\"handleCheckbox\"\n          />\n        </div>\n      </template>\n      <template #col:listenPort=\"{ row }\">\n        <div\n          v-if=\"serviceBeingEditedIs(row)\"\n          class=\"listen-port-div\"\n        >\n          <input\n            v-focus\n            type=\"number\"\n            :value=\"serviceBeingEdited.listenPort\"\n            class=\"listen-port-input\"\n            @input=\"emitUpdatePort\"\n            @keyup.enter=\"emitUpdatePortForward\"\n          >\n        </div>\n        <div v-else>\n          <p class=\"listen-port-p\">\n            {{ row.listenPort }}\n          </p>\n        </div>\n      </template>\n      <template #row-actions=\"{ row }\">\n        <div\n          v-if=\"row.listenPort === undefined && !serviceBeingEditedIs(row)\"\n          class=\"action-div\"\n        >\n          <button\n            class=\"btn btn-sm role-tertiary\"\n            @click=\"emitEditPortForward(row)\"\n          >\n            Forward\n          </button>\n        </div>\n        <div\n          v-else-if=\"serviceBeingEditedIs(row)\"\n          class=\"action-div\"\n        >\n          <button\n            class=\"btn btn-sm role-tertiary btn-icon\"\n            @click=\"emitCancelEditPortForward(row)\"\n          >\n            <span class=\"icon icon-x icon-lg\" />\n          </button>\n          <button\n            class=\"btn btn-sm role-tertiary btn-icon\"\n            @click=\"emitUpdatePortForward()\"\n          >\n            <span class=\"icon icon-checkmark icon-lg\" />\n          </button>\n        </div>\n        <div\n          v-else\n          class=\"action-div\"\n        >\n          <button\n            class=\"btn btn-sm role-tertiary\"\n            @click=\"emitCancelPortForward(row)\"\n          >\n            Cancel\n          </button>\n        </div>\n      </template>\n    </SortableTable>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { Banner, Checkbox } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport * as K8s from '@pkg/backend/k8s';\nimport SortableTable from '@pkg/components/SortableTable/index.vue';\n\nimport type { PropType } from 'vue';\n\ntype ServiceEntryWithKey = K8s.ServiceEntry & { key: string };\n\nexport default defineComponent({\n  name:       'port-forwarding',\n  components: {\n    SortableTable, Checkbox, Banner,\n  },\n  directives: {\n    focus: {\n      mounted(element) {\n        element.focus();\n      },\n    },\n  },\n  props: {\n    services: {\n      type:     Array as PropType<K8s.ServiceEntry[]>,\n      required: true,\n    },\n    includeKubernetesServices: {\n      type:    Boolean,\n      default: false,\n    },\n    k8sState: {\n      type:    String as PropType<K8s.State>,\n      default: K8s.State.STOPPED,\n    },\n    kubernetesIsDisabled: {\n      type:    Boolean,\n      default: false,\n    },\n    // Used to determine which row to allow editing of listenPort on.\n    serviceBeingEdited: {\n      type:    Object as PropType<K8s.ServiceEntry>,\n      default: null,\n    },\n    errorMessage: {\n      type:    String,\n      default: null,\n    },\n  },\n\n  data() {\n    return {\n      headers: [\n        {\n          name:  'namespace',\n          label: 'Namespace',\n          sort:  ['namespace', 'name'],\n        },\n        {\n          name:  'name',\n          label: 'Name',\n          sort:  ['name', 'namespace'],\n        },\n        {\n          name:  'portName',\n          label: 'Port',\n          sort:  ['portName', 'namespace', 'name'],\n        },\n        {\n          name:  'listenPort',\n          label: 'Local Port',\n          sort:  ['listenPort', 'namespace', 'name'],\n        },\n      ],\n    };\n  },\n  computed: {\n    isRunning(): boolean {\n      return this.k8sState === K8s.State.STARTED;\n    },\n    rows(): ServiceEntryWithKey[] {\n      let services = this.services;\n\n      if (!this.includeKubernetesServices) {\n        services = services\n          .filter(service => service.namespace !== 'kube-system')\n          .filter(service => !(service.namespace === 'default' && service.name === 'kubernetes'));\n      }\n\n      return services.map((service) => {\n        const port = typeof service.port === 'number' ? service.port.toString() : service.port;\n\n        return {\n          namespace:  service.namespace,\n          name:       service.name,\n          portName:   service.portName ?? port,\n          port:       service.port,\n          listenPort: service.listenPort,\n          key:        `${ service.namespace }/${ service.name }:${ service.portName }`,\n        };\n      });\n    },\n  },\n  methods: {\n    serviceBeingEditedIs(service: K8s.ServiceEntry): boolean {\n      if (this.serviceBeingEdited === null) {\n        return false;\n      }\n\n      // compare the two services, minus listenPort property, since this may differ\n      return this.serviceBeingEdited.name === service.name &&\n        this.serviceBeingEdited.namespace === service.namespace &&\n        this.serviceBeingEdited.port === service.port;\n    },\n    emitUpdatePort(event: any): void {\n      const portBeingEdited = parseInt(event.target.value, 10);\n\n      this.$emit('updatePort', portBeingEdited);\n    },\n    handleCheckbox(value: boolean): void {\n      this.$emit('toggledServiceFilter', value);\n    },\n    emitEditPortForward(service: K8s.ServiceEntry): void {\n      this.$emit('editPortForward', service);\n    },\n    emitCancelPortForward(service: K8s.ServiceEntry): void {\n      this.$emit('cancelPortForward', service);\n    },\n    emitCancelEditPortForward(service: K8s.ServiceEntry): void {\n      this.$emit('cancelEditPortForward', service);\n    },\n    emitUpdatePortForward(): void {\n      this.$emit('updatePortForward');\n    },\n    emitCloseError(): void {\n      this.$emit('closeError');\n    },\n  },\n});\n</script>\n\n<style>\n  .btn-icon {\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n  }\n\n  .action-div {\n    display: flex;\n    flex-direction: row-reverse;\n    gap: 0.5rem;\n  }\n\n  .listen-port-div {\n    height: 100%;\n    width: 6rem;\n  }\n\n  .listen-port-input {\n    max-height: 30px;\n    margin: 8px 0;\n  }\n\n  .listen-port-p {\n    margin: 15px 11px;\n  }\n\n  .header-middle {\n    display: flex;\n    align-items: flex-end;\n    gap: 1rem;\n    height: 100%;\n  }\n\n  .kubernetes-services {\n    margin-bottom: 12px;\n  }\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/Alert.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapState } from 'vuex';\n\ntype AlertMap = Record<'reset' | 'restart' | 'error', string>;\n\nconst alertMap: AlertMap = {\n  reset:   'preferences.actions.banner.reset',\n  restart: 'preferences.actions.banner.restart',\n  error:   'preferences.actions.banner.error',\n};\n\nexport default defineComponent({\n  name:     'preferences-alert',\n  computed: {\n    ...mapState('preferences', ['severities', 'preferencesError']),\n    severity(): keyof AlertMap | undefined {\n      if (this.severities.error) {\n        return 'error';\n      }\n\n      if (this.severities.reset) {\n        return 'reset';\n      }\n\n      if (this.severities.restart) {\n        return 'restart';\n      }\n\n      return undefined;\n    },\n    alert(): string {\n      if (!this.severity) {\n        return '';\n      }\n\n      return alertMap[this.severity];\n    },\n    alertText(): string | null {\n      if (this.preferencesError) {\n        return this.preferencesError;\n      }\n\n      if (this.alert) {\n        return this.t(this.alert, { }, true);\n      }\n\n      return null;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"alert\">\n    <span\n      v-if=\"alert\"\n      class=\"alert-text\"\n    >\n      {{ alertText }}\n    </span>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .alert {\n    .alert-text {\n      color: var(--body-text);\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ApplicationBehavior.vue",
    "content": "<script lang=\"ts\">\n\nimport { RadioButton, RadioGroup } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings, Theme } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-application-behavior',\n  components: {\n    RadioGroup, RadioButton, RdCheckbox, RdFieldset,\n  },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    themeOptions(): { label: string, value: Theme, description: string }[] {\n      return Object.values(Theme).map(value => ({\n        label:       this.t(`application.behavior.theme.options.${ value }.label`),\n        value,\n        description: this.t(`application.behavior.theme.options.${ value }.description`),\n      }));\n    },\n  },\n  methods:  {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"application-behavior\">\n    <div class=\"row\">\n      <div class=\"col span-6\">\n        <rd-fieldset\n          data-test=\"autoStart\"\n          :legend-text=\"t('application.behavior.autoStart.legendText')\"\n        >\n          <rd-checkbox\n            :label=\"t('application.behavior.autoStart.label')\"\n            :value=\"preferences.application.autoStart\"\n            :is-locked=\"isPreferenceLocked('application.autoStart')\"\n            @update:value=\"onChange('application.autoStart', $event)\"\n          />\n        </rd-fieldset>\n        <rd-fieldset\n          data-test=\"background\"\n          :legend-text=\"t('application.behavior.background.legendText')\"\n          :legend-tooltip=\"t('application.behavior.background.legendTooltip')\"\n          class=\"checkbox-group\"\n        >\n          <rd-checkbox\n            :label=\"t('application.behavior.startInBackground.label')\"\n            :value=\"preferences.application.startInBackground\"\n            :is-locked=\"isPreferenceLocked('application.startInBackground')\"\n            @update:value=\"onChange('application.startInBackground', $event)\"\n          />\n          <rd-checkbox\n            :label=\"t('application.behavior.windowQuitOnClose.label')\"\n            :value=\"preferences.application.window.quitOnClose\"\n            :is-locked=\"isPreferenceLocked('application.window.quitOnClose')\"\n            @update:value=\"onChange('application.window.quitOnClose', $event)\"\n          />\n        </rd-fieldset>\n        <rd-fieldset\n          data-test=\"notificationIcon\"\n          :legend-text=\"t('application.behavior.notificationIcon.legendText')\"\n        >\n          <rd-checkbox\n            :label=\"t('application.behavior.notificationIcon.label')\"\n            :value=\"preferences.application.hideNotificationIcon\"\n            :is-locked=\"isPreferenceLocked('application.hideNotificationIcon')\"\n            @update:value=\"onChange('application.hideNotificationIcon', $event)\"\n          />\n        </rd-fieldset>\n      </div>\n      <div class=\"col span-6 theme-options\">\n        <rd-fieldset\n          data-test=\"theme\"\n          :legend-text=\"t('application.behavior.theme.legendText')\"\n          :is-locked=\"isPreferenceLocked('application.theme')\"\n        >\n          <template #default=\"{ isLocked }\">\n            <radio-group\n              :options=\"themeOptions\"\n              name=\"theme\"\n              :disabled=\"isLocked\"\n              :class=\"{ 'locked-radio': isLocked }\"\n            >\n              <template\n                v-for=\"(option, index) in themeOptions\"\n                #[index]=\"{ isDisabled }\"\n              >\n                <radio-button\n                  :key=\"'theme-' + index\"\n                  name=\"theme\"\n                  :value=\"preferences.application.theme\"\n                  :val=\"option.value\"\n                  :disabled=\"isDisabled\"\n                  :data-test=\"option.label\"\n                  @update:value=\"onChange('application.theme', $event)\"\n                >\n                  <template #label>\n                    {{ option.label }}\n                  </template>\n                  <template #description>\n                    {{ option.description }}\n                  </template>\n                </radio-button>\n              </template>\n            </radio-group>\n          </template>\n        </rd-fieldset>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .application-behavior {\n    display: flex;\n    flex-direction: column;\n    gap: 1rem;\n\n    .checkbox-group {\n      display: flex;\n      flex-direction: column;\n      gap: 0.5rem;\n    }\n\n    .theme-options {\n      border-left: 1px solid var(--border);\n      padding-left: 1rem;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ApplicationEnvironment.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport PathManagementSelector from '@pkg/components/PathManagementSelector.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-application-environment',\n  components: { PathManagementSelector, RdFieldset },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('applicationSettings', ['pathManagementStrategy']),\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n  },\n  mounted() {\n    ipcRenderer.on('settings-read', (_, currentSettings: Settings) => {\n      this.$store.dispatch('preferences/updatePreferencesData', { property: 'application.pathManagementStrategy', value: currentSettings.application.pathManagementStrategy });\n    });\n    ipcRenderer.send('settings-read');\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-fieldset\n    data-test=\"pathManagement\"\n    :legend-text=\"t('pathManagement.label')\"\n    :legend-tooltip=\"t('pathManagement.tooltip', { }, true)\"\n    :is-locked=\"isPreferenceLocked('application.pathManagementStrategy')\"\n  >\n    <template #default=\"{ isLocked }\">\n      <path-management-selector\n        :show-label=\"false\"\n        :value=\"preferences.application.pathManagementStrategy\"\n        :is-locked=\"isLocked\"\n        @input=\"onChange('application.pathManagementStrategy', $event)\"\n      />\n    </template>\n  </rd-fieldset>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ApplicationGeneral.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-application-general',\n  components: { RdCheckbox, RdFieldset },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  data() {\n    return {\n      sudoAllowedTooltip: `\n        If checked, Rancher Desktop will attempt to acquire administrative\n        credentials (\"sudo access\") when starting for some operations.  This\n        allows for enhanced functionality, including bridged networking and\n        default docker socket support.  Changes will only be applied next time\n        Rancher Desktop starts.\n      `,\n      automaticUpdates: true,\n      statistics:       false,\n    };\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPlatformWindows', 'isPreferenceLocked']),\n    isSudoAllowed(): boolean {\n      return this.preferences?.application?.adminAccess ?? false;\n    },\n    canAutoUpdate(): boolean {\n      return this.preferences?.application.updater.enabled ?? false;\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"application-general\">\n    <rd-fieldset\n      v-if=\"!isPlatformWindows\"\n      data-test=\"administrativeAccess\"\n      legend-text=\"Administrative Access\"\n      :legend-tooltip=\"sudoAllowedTooltip\"\n    >\n      <rd-checkbox\n        label=\"Allow to acquire administrative credentials (sudo access)\"\n        :value=\"isSudoAllowed\"\n        :is-locked=\"isPreferenceLocked('application.adminAccess')\"\n        @update:value=\"onChange('application.adminAccess', $event)\"\n      />\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"automaticUpdates\"\n      legend-text=\"Automatic Updates\"\n    >\n      <rd-checkbox\n        data-test=\"automaticUpdatesCheckbox\"\n        label=\"Check for updates automatically\"\n        :value=\"canAutoUpdate\"\n        :is-locked=\"isPreferenceLocked('application.updater.enabled')\"\n        @update:value=\"onChange('application.updater.enabled', $event)\"\n      />\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"statistics\"\n      legend-text=\"Statistics\"\n    >\n      <rd-checkbox\n        label=\"Allow collection of anonymous statistics to help us improve Rancher Desktop\"\n        :value=\"preferences.application.telemetry.enabled\"\n        :is-locked=\"isPreferenceLocked('application.telemetry.enabled')\"\n        @update:value=\"onChange('application.telemetry.enabled', $event)\"\n      />\n    </rd-fieldset>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .application-general {\n    display: flex;\n    flex-direction: column;\n    gap: 1rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/BodyApplication.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent, Component, PropType } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport PreferencesApplicationBehavior from '@pkg/components/Preferences/ApplicationBehavior.vue';\nimport PreferencesApplicationEnvironment from '@pkg/components/Preferences/ApplicationEnvironment.vue';\nimport PreferencesApplicationGeneral from '@pkg/components/Preferences/ApplicationGeneral.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport { Settings } from '@pkg/config/settings';\nimport type { TransientSettings } from '@pkg/config/transientSettings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nexport default defineComponent({\n  name:       'preferences-body-application',\n  components: {\n    RdTabbed,\n    Tab,\n    PreferencesApplicationBehavior,\n    PreferencesApplicationEnvironment,\n    PreferencesApplicationGeneral,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPlatformWindows']),\n    ...mapGetters('transientSettings', ['getActiveTab']),\n    ...mapState('credentials', ['credentials']),\n    activeTab(): string {\n      return this.getActiveTab || 'behavior';\n    },\n  },\n  async beforeMount() {\n    await this.$store.dispatch('credentials/fetchCredentials');\n  },\n  methods: {\n    async tabSelected({ tab }: { tab: Component }) {\n      if (this.activeTab !== tab.name) {\n        await this.commitPreferences(tab.name || '');\n      }\n    },\n    async commitPreferences(tabName: string) {\n      await this.$store.dispatch(\n        'transientSettings/commitPreferences',\n        {\n          ...this.credentials as ServerState,\n          payload: { preferences: { navItem: { currentTabs: { Application: tabName } } } } as RecursivePartial<TransientSettings>,\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-tabbed\n    v-bind=\"$attrs\"\n    class=\"action-tabs\"\n    :no-content=\"true\"\n    :default-tab=\"activeTab\"\n    @changed=\"tabSelected\"\n  >\n    <template #tabs>\n      <tab\n        v-if=\"!isPlatformWindows\"\n        label=\"Environment\"\n        name=\"environment\"\n        :weight=\"1\"\n      />\n      <tab\n        label=\"Behavior\"\n        name=\"behavior\"\n        :weight=\"2\"\n      />\n      <tab\n        label=\"General\"\n        name=\"general\"\n        :weight=\"3\"\n      />\n    </template>\n    <div class=\"application-content\">\n      <component\n        v-bind=\"$attrs\"\n        :is=\"`preferences-application-${activeTab}`\"\n        :preferences=\"preferences\"\n      />\n    </div>\n  </rd-tabbed>\n</template>\n\n<style lang=\"scss\" scoped>\n  .application-content {\n    padding: var(--preferences-content-padding);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/BodyContainerEngine.vue",
    "content": "<script lang=\"ts\">\n\nimport { Component, defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport PreferencesContainerEngineAllowedImages from '@pkg/components/Preferences/ContainerEngineAllowedImages.vue';\nimport PreferencesContainerEngineGeneral from '@pkg/components/Preferences/ContainerEngineGeneral.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { TransientSettings } from '@pkg/config/transientSettings';\nimport { ServerState } from '@pkg/main/credentialServer/httpCredentialHelperServer';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-body-container-engine',\n  components: {\n    PreferencesContainerEngineAllowedImages,\n    PreferencesContainerEngineGeneral,\n    RdTabbed,\n    Tab,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('transientSettings', ['getActiveTab']),\n    ...mapState('credentials', ['credentials']),\n    activeTab(): string {\n      return this.getActiveTab || 'general';\n    },\n  },\n  methods: {\n    async tabSelected({ tab }: { tab: Component }) {\n      if (this.activeTab !== tab.name) {\n        await this.commitPreferences(tab.name || '');\n      }\n    },\n    async commitPreferences(tabName: string) {\n      await this.$store.dispatch(\n        'transientSettings/commitPreferences',\n        {\n          ...this.credentials as ServerState,\n          payload: { preferences: { navItem: { currentTabs: { 'Container Engine': tabName } } } } as RecursivePartial<TransientSettings>,\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-tabbed\n    v-bind=\"$attrs\"\n    class=\"action-tabs\"\n    :no-content=\"true\"\n    :default-tab=\"activeTab\"\n    @changed=\"tabSelected\"\n  >\n    <template #tabs>\n      <tab\n        label=\"General\"\n        name=\"general\"\n        :weight=\"2\"\n      />\n      <tab\n        label=\"Allowed Images\"\n        name=\"allowed-images\"\n        :weight=\"1\"\n      />\n    </template>\n    <div class=\"container-engine-content\">\n      <component\n        v-bind=\"$attrs\"\n        :is=\"`preferences-container-engine-${activeTab}`\"\n        :preferences=\"preferences\"\n      />\n    </div>\n  </rd-tabbed>\n</template>\n\n<style lang=\"scss\" scoped>\n  .container-engine-content {\n    padding: var(--preferences-content-padding);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/BodyKubernetes.vue",
    "content": "<script lang=\"ts\">\n\nimport { Banner } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport RdInput from '@pkg/components/RdInput.vue';\nimport RdSelect from '@pkg/components/RdSelect.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { highestStableVersion, VersionEntry } from '@pkg/utils/kubeVersions';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-body-kubernetes',\n  components: {\n    Banner,\n    RdCheckbox,\n    RdFieldset,\n    RdSelect,\n    RdInput,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  data() {\n    return {\n      versions:           [] as VersionEntry[],\n      cachedVersionsOnly: false,\n    };\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    defaultVersion(): VersionEntry {\n      return highestStableVersion(this.recommendedVersions) ?? this.nonRecommendedVersions[0];\n    },\n    /** Versions that are the tip of a channel */\n    recommendedVersions(): VersionEntry[] {\n      return this.versions.filter(v => !!v.channels);\n    },\n    /** Versions that are not supported by a channel. */\n    nonRecommendedVersions(): VersionEntry[] {\n      return this.versions.filter(v => !v.channels);\n    },\n    isKubernetesDisabled(): boolean {\n      return !this.preferences.kubernetes.enabled;\n    },\n    kubernetesVersion(): string {\n      return this.preferences.kubernetes.version;\n    },\n    kubernetesVersionLabel(): string {\n      return `Kubernetes version${ this.cachedVersionsOnly ? ' (cached versions only)' : '' }`;\n    },\n    spinOperatorIncompatible(): boolean {\n      return !this.isKubernetesDisabled &&\n        !this.preferences.experimental.containerEngine.webAssembly.enabled &&\n        this.preferences.experimental.kubernetes.options.spinkube;\n    },\n  },\n  beforeMount() {\n    ipcRenderer.on('k8s-versions', (event, versions, cachedVersionsOnly) => {\n      this.versions = versions;\n      this.cachedVersionsOnly = cachedVersionsOnly;\n    });\n\n    ipcRenderer.send('k8s-versions');\n  },\n  methods: {\n    /**\n     * Get the display name of a given version.\n     * @param version The version to format.\n     */\n    versionName(version: VersionEntry) {\n      const names = (version.channels ?? []).filter(ch => !/^v?\\d+/.test(ch));\n\n      if (names.length > 0) {\n        return `v${ version.version } (${ names.join(', ') })`;\n      }\n\n      return `v${ version.version }`;\n    },\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n    castToNumber(val: string): number | null {\n      return val ? Number(val) : null;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"preferences-body\">\n    <rd-fieldset\n      data-test=\"kubernetesToggle\"\n      legend-text=\"Kubernetes\"\n    >\n      <rd-checkbox\n        label=\"Enable Kubernetes\"\n        :value=\"preferences.kubernetes.enabled\"\n        :is-locked=\"isPreferenceLocked('kubernetes.enabled')\"\n        @update:value=\"onChange('kubernetes.enabled', $event)\"\n      />\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"kubernetesVersion\"\n      class=\"width-xs\"\n      :legend-text=\"kubernetesVersionLabel\"\n    >\n      <rd-select\n        class=\"select-k8s-version\"\n        :model-value=\"kubernetesVersion\"\n        :disabled=\"isKubernetesDisabled\"\n        :is-locked=\"isPreferenceLocked('kubernetes.version')\"\n        @change=\"onChange('kubernetes.version', $event.target.value)\"\n      >\n        <!--\n            - On macOS Chrome / Electron can't style the <option> elements.\n            - We do the best we can by instead using <optgroup> for a recommended section.\n            -->\n        <optgroup\n          v-if=\"recommendedVersions.length > 0\"\n          label=\"Recommended Versions\"\n        >\n          <option\n            v-for=\"item in recommendedVersions\"\n            :key=\"item.version\"\n            :value=\"item.version\"\n            :selected=\"item.version === defaultVersion.version\"\n          >\n            {{ versionName(item) }}\n          </option>\n        </optgroup>\n        <optgroup\n          v-if=\"nonRecommendedVersions.length > 0\"\n          label=\"Other Versions\"\n        >\n          <option\n            v-for=\"item in nonRecommendedVersions\"\n            :key=\"item.version\"\n            :value=\"item.version\"\n            :selected=\"item.version === defaultVersion.version\"\n          >\n            v{{ item.version }}\n          </option>\n        </optgroup>\n      </rd-select>\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"kubernetesPort\"\n      class=\"width-xs\"\n      legend-text=\"Kubernetes Port\"\n    >\n      <rd-input\n        type=\"number\"\n        :disabled=\"isKubernetesDisabled\"\n        :value=\"preferences.kubernetes.port\"\n        :is-locked=\"isPreferenceLocked('kubernetes.port')\"\n        @input=\"onChange('kubernetes.port', castToNumber($event.target.value))\"\n      />\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"kubernetesOptions\"\n      legend-text=\"Options\"\n    >\n      <rd-checkbox\n        label=\"Enable Traefik\"\n        :disabled=\"isKubernetesDisabled\"\n        :value=\"preferences.kubernetes.options.traefik\"\n        :is-locked=\"isPreferenceLocked('kubernetes.options.traefik')\"\n        @update:value=\"onChange('kubernetes.options.traefik', $event)\"\n      />\n      <!-- Don't disable Spinkube option when Wasm is disabled; let validation deal with it  -->\n      <rd-checkbox\n        label=\"Install Spin Operator\"\n        :disabled=\"isKubernetesDisabled\"\n        :value=\"preferences.experimental.kubernetes.options.spinkube\"\n        :is-locked=\"isPreferenceLocked('experimental.kubernetes.options.spinkube')\"\n        :is-experimental=\"true\"\n        @update:value=\"onChange('experimental.kubernetes.options.spinkube', $event)\"\n      >\n        <template\n          v-if=\"spinOperatorIncompatible\"\n          #below\n        >\n          <banner color=\"warning\">\n            Spin operator requires\n            <a\n              href=\"#\"\n              @click.prevent=\"$root.navigate('Container Engine', 'general')\"\n            >WebAssembly</a>\n            to be enabled.\n          </banner>\n        </template>\n      </rd-checkbox>\n    </rd-fieldset>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .checkbox-title {\n    font-size: 1rem;\n    line-height: 1.5rem;\n    padding-bottom: 0.5rem;\n  }\n\n  .preferences-body {\n    padding: var(--preferences-content-padding);\n    display: flex;\n    flex-direction: column;\n    gap: 1rem;\n  }\n\n  .width-xs {\n    max-width: 20rem;\n    min-width: 20rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/BodyVirtualMachine.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { Component, defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport PreferencesVirtualMachineEmulation from '@pkg/components/Preferences/VirtualMachineEmulation.vue';\nimport PreferencesVirtualMachineHardware from '@pkg/components/Preferences/VirtualMachineHardware.vue';\nimport PreferencesVirtualMachineVolumes from '@pkg/components/Preferences/VirtualMachineVolumes.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport { Settings } from '@pkg/config/settings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-body-virtual-machine',\n  components: {\n    RdTabbed,\n    Tab,\n    PreferencesVirtualMachineHardware,\n    PreferencesVirtualMachineVolumes,\n    PreferencesVirtualMachineEmulation,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPlatformWindows']),\n    ...mapGetters('transientSettings', ['getActiveTab']),\n    ...mapState('credentials', ['credentials']),\n    activeTab(): string {\n      return this.getActiveTab || 'hardware';\n    },\n    isPlatformDarwin(): boolean {\n      return os.platform() === 'darwin';\n    },\n  },\n  methods: {\n    async tabSelected({ tab }: { tab: Component }) {\n      if (this.activeTab !== tab.name) {\n        await this.navigate('Virtual Machine', tab.name || '');\n      }\n    },\n    async navigate(navItem: string, tab: string) {\n      await this.$store.dispatch(\n        'transientSettings/navigatePrefDialog',\n        {\n          ...this.credentials as ServerState,\n          navItem,\n          tab,\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-tabbed\n    v-bind=\"$attrs\"\n    class=\"action-tabs\"\n    :no-content=\"true\"\n    :default-tab=\"activeTab\"\n    :active-tab=\"activeTab\"\n    @changed=\"tabSelected\"\n  >\n    <template #tabs>\n      <tab\n        v-if=\"isPlatformDarwin\"\n        label=\"Emulation\"\n        name=\"emulation\"\n        :weight=\"1\"\n      />\n      <tab\n        label=\"Volumes\"\n        name=\"volumes\"\n        :weight=\"3\"\n      />\n      <tab\n        label=\"Hardware\"\n        name=\"hardware\"\n        :weight=\"4\"\n      />\n    </template>\n    <div class=\"virtual-machine-content\">\n      <component\n        v-bind=\"$attrs\"\n        :is=\"`preferences-virtual-machine-${activeTab}`\"\n        :preferences=\"preferences\"\n      />\n    </div>\n  </rd-tabbed>\n</template>\n\n<style lang=\"scss\" scoped>\n  .virtual-machine-content {\n    padding: var(--preferences-content-padding);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/BodyWsl.vue",
    "content": "<script lang=\"ts\">\n\nimport { Component, defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport PreferencesWslIntegrations from '@pkg/components/Preferences/WslIntegrations.vue';\nimport PreferencesWslProxy from '@pkg/components/Preferences/WslProxy.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport { Settings } from '@pkg/config/settings';\nimport type { TransientSettings } from '@pkg/config/transientSettings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-body-wsl',\n  components: {\n    RdTabbed, Tab, PreferencesWslIntegrations, PreferencesWslProxy,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('transientSettings', ['getActiveTab']),\n    ...mapState('credentials', ['credentials']),\n    activeTab(): string {\n      return this.getActiveTab || 'integration';\n    },\n  },\n  methods: {\n    async tabSelected({ tab }: { tab: Component }) {\n      if (this.activeTab !== tab.name) {\n        await this.commitPreferences(tab.name || '');\n      }\n    },\n    async commitPreferences(tabName: string) {\n      await this.$store.dispatch(\n        'transientSettings/commitPreferences',\n        {\n          ...this.credentials as ServerState,\n          payload: { preferences: { navItem: { currentTabs: { WSL: tabName } } } } as RecursivePartial<TransientSettings>,\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-tabbed\n    v-bind=\"$attrs\"\n    class=\"action-tabs\"\n    :no-content=\"true\"\n    :default-tab=\"activeTab\"\n    @changed=\"tabSelected\"\n  >\n    <template #tabs>\n      <tab\n        label=\"Integrations\"\n        name=\"integrations\"\n        :weight=\"2\"\n      />\n      <tab\n        label=\"Proxy\"\n        name=\"proxy\"\n        :weight=\"1\"\n      />\n    </template>\n    <div class=\"wsl-content\">\n      <component\n        :is=\"`preferences-wsl-${activeTab}`\"\n        :preferences=\"preferences\"\n      />\n    </div>\n  </rd-tabbed>\n</template>\n\n<style lang=\"scss\" scoped>\n  .wsl-content {\n    padding: var(--preferences-content-padding);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ButtonOpen.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name:    'preferences-button',\n  emits:   ['open-preferences'],\n  methods: {\n    openPreferences() {\n      this.$emit('open-preferences');\n    },\n  },\n});\n</script>\n\n<template>\n  <button\n    class=\"btn role-secondary btn-icon-text\"\n    @click=\"openPreferences\"\n  >\n    {{ t('nav.userMenu.preferences') }}\n  </button>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ContainerEngineAllowedImages.vue",
    "content": "<script lang=\"ts\">\n\nimport { StringList } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-container-engine-allowed-images',\n  components: {\n    RdFieldset,\n    StringList,\n    RdCheckbox,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    patterns(): string[] {\n      return this.preferences.containerEngine.allowedImages.patterns;\n    },\n    isAllowedImagesEnabled(): boolean {\n      return this.preferences.containerEngine.allowedImages.enabled;\n    },\n    isPatternsFieldLocked(): boolean {\n      return this.isPreferenceLocked('containerEngine.allowedImages.patterns') || !this.isAllowedImagesEnabled;\n    },\n    patternsErrorMessages(): { duplicate: string } {\n      return { duplicate: this.t('allowedImages.errors.duplicate') };\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n    onType(item: string) {\n      if (item) {\n        this.setCanApply(item.trim().length > 0);\n      }\n    },\n    onDuplicate(err: boolean) {\n      if (err) {\n        this.setCanApply(false);\n      }\n    },\n    setCanApply(val: boolean) {\n      this.$store.dispatch('preferences/setCanApply', val);\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"container-engine-allowed-images\">\n    <rd-fieldset\n      data-test=\"allowedImages\"\n      :legend-text=\"t('allowedImages.label')\"\n      :is-experimental=\"true\"\n    >\n      <rd-checkbox\n        data-testid=\"allowedImagesCheckbox\"\n        :label=\"t('allowedImages.enable')\"\n        :value=\"isAllowedImagesEnabled\"\n        :is-locked=\"isPreferenceLocked('containerEngine.allowedImages.enabled')\"\n        @update:value=\"onChange('containerEngine.allowedImages.enabled', $event)\"\n      />\n    </rd-fieldset>\n    <string-list\n      :items=\"patterns\"\n      :case-sensitive=\"false\"\n      :placeholder=\"t('allowedImages.patterns.placeholder')\"\n      :readonly=\"isPatternsFieldLocked\"\n      actions-position=\"left\"\n      :error-messages=\"patternsErrorMessages\"\n      @change=\"Array.isArray($event) && onChange('containerEngine.allowedImages.patterns', $event)\"\n      @type:item=\"onType($event)\"\n      @errors=\"onDuplicate($event.duplicate)\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n\n  .container-engine-allowed-images {\n    display: flex;\n    flex-direction: column;\n    grid-gap: 1rem;\n    gap: 1rem;\n\n    .string-list {\n      flex: 1;\n    }\n  }\n\n</style>\n\n<style lang=\"scss\">\n  .string-list .string-list-box .item .label {\n    white-space: nowrap;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ContainerEngineGeneral.vue",
    "content": "<script lang=\"ts\">\n\nimport { Banner } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport EngineSelector from '@pkg/components/EngineSelector.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { ContainerEngine, Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-container-engine-general',\n  components: {\n    Banner,\n    EngineSelector,\n    RdCheckbox,\n    RdFieldset,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  data() {\n    return { containerEngine: ContainerEngine.CONTAINERD };\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    webAssemblyIncompatible(): boolean {\n      return this.preferences.kubernetes.enabled &&\n        this.preferences.experimental.kubernetes.options.spinkube &&\n        !this.preferences.experimental.containerEngine.webAssembly.enabled;\n    },\n  },\n  methods: {\n    onChangeEngine(desiredEngine: ContainerEngine) {\n      this.containerEngine = desiredEngine;\n      this.$emit('container-engine-change', desiredEngine);\n    },\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"container-engine-general\">\n    <rd-fieldset\n      data-test=\"containerEngine\"\n      :legend-text=\"t('containerEngine.label')\"\n      :is-locked=\"isPreferenceLocked('containerEngine.name')\"\n    >\n      <template #default=\"{ isLocked }\">\n        <engine-selector\n          :container-engine=\"preferences.containerEngine.name\"\n          :is-locked=\"isLocked\"\n          @change=\"onChange('containerEngine.name', $event)\"\n        />\n      </template>\n    </rd-fieldset>\n    <rd-fieldset\n      data-test=\"webAssembly\"\n      :legend-text=\"t('webAssembly.label')\"\n      :is-experimental=\"true\"\n    >\n      <rd-checkbox\n        data-test=\"webAssemblyCheckbox\"\n        :label=\"t('webAssembly.enabled')\"\n        :description=\"t('webAssembly.description')\"\n        :value=\"preferences.experimental.containerEngine.webAssembly.enabled\"\n        :is-locked=\"isPreferenceLocked('experimental.containerEngine.webAssembly.enabled')\"\n        @update:value=\"onChange('experimental.containerEngine.webAssembly.enabled', $event)\"\n      >\n        <template\n          v-if=\"webAssemblyIncompatible\"\n          #below\n        >\n          <banner color=\"warning\">\n            WebAssembly must be enabled for the\n            <a\n              href=\"#\"\n              @click.prevent=\"$root.navigate('Kubernetes')\"\n            >Spin Operator</a>\n            to be installed.\n          </banner>\n        </template>\n      </rd-checkbox>\n    </rd-fieldset>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.container-engine-general {\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/Help.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\n\nimport Help from '@pkg/components/Help.vue';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'preferences-help',\n  components: { Help },\n  methods:    {\n    openUrl() {\n      ipcRenderer.send('help/preferences/open-url');\n    },\n  },\n});\n</script>\n\n<template>\n  <help @open:url=\"openUrl\" />\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ModalBody.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\nimport { mapState } from 'vuex';\n\nimport PreferencesBodyApplication from '@pkg/components/Preferences/BodyApplication.vue';\nimport PreferencesBodyContainerEngine from '@pkg/components/Preferences/BodyContainerEngine.vue';\nimport PreferencesBodyKubernetes from '@pkg/components/Preferences/BodyKubernetes.vue';\nimport PreferencesBodyVirtualMachine from '@pkg/components/Preferences/BodyVirtualMachine.vue';\nimport PreferencesBodyWsl from '@pkg/components/Preferences/BodyWsl.vue';\nimport PreferencesHelp from '@pkg/components/Preferences/Help.vue';\nimport { Settings } from '@pkg/config/settings';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-body',\n  components: {\n    PreferencesBodyApplication,\n    PreferencesBodyVirtualMachine,\n    PreferencesBodyWsl,\n    PreferencesBodyContainerEngine,\n    PreferencesBodyKubernetes,\n    PreferencesHelp,\n  },\n  props: {\n    currentNavItem: {\n      type:     String,\n      required: true,\n    },\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapState('credentials', ['credentials']),\n    normalizeNavItem(): string {\n      return this.currentNavItem.toLowerCase().replaceAll(' ', '-');\n    },\n    componentFromNavItem(): string {\n      return `preferences-body-${ this.normalizeNavItem }`;\n    },\n  },\n  mounted() {\n    (this.$root as any).navigate = this.navigate;\n  },\n  methods: {\n    navigate(navItem: string, tab: string) {\n      console.log('Navigate!', Array.from(arguments));\n      this.$store.dispatch(\n        'transientSettings/navigatePrefDialog',\n        {\n          ...this.credentials,\n          navItem,\n          tab,\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"preferences-body\">\n    <slot>\n      <component\n        v-bind=\"$attrs\"\n        :is=\"componentFromNavItem\"\n        :preferences=\"preferences\"\n      />\n    </slot>\n    <preferences-help class=\"help\" />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .preferences-body {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n\n    .help {\n      position: absolute;\n      bottom: 0.75rem;\n      right: 0.75rem;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ModalFooter.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport PreferencesAlert from '@pkg/components/Preferences/Alert.vue';\n\nexport default defineComponent({\n  name:       'preferences-footer',\n  components: { PreferencesAlert },\n  computed:   {\n    ...mapGetters('preferences', ['canApply']),\n    isDisabled(): boolean {\n      return !this.canApply;\n    },\n  },\n  methods: {\n    cancel() {\n      this.$emit('cancel');\n    },\n    apply() {\n      this.$emit('apply');\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"preferences-footer\">\n    <div class=\"preferences-alert\">\n      <preferences-alert />\n    </div>\n    <div class=\"preferences-actions\">\n      <button\n        data-test=\"preferences-cancel\"\n        class=\"btn role-secondary\"\n        @click=\"cancel\"\n      >\n        Cancel\n      </button>\n      <button\n        class=\"btn role-primary\"\n        :disabled=\"isDisabled\"\n        @click=\"apply\"\n      >\n        Apply\n      </button>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .preferences-footer {\n    display: flex;\n    justify-content: space-between;\n    border-top: 1px solid var(--header-border);\n    padding: var(--preferences-content-padding);\n\n    .preferences-alert {\n      display: flex;\n      justify-content: right;\n      align-items: center;\n      height: 101%;\n      width: 100%;\n      padding-right: var(--preferences-content-padding);\n    }\n\n    .preferences-actions {\n      display: flex;\n      justify-content: flex-end;\n      gap: 1rem;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ModalHeader.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({ name: 'preferences-header' });\n</script>\n\n<template>\n  <div class=\"preferences-header\">\n    <div class=\"title\">\n      Preferences\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .preferences-header {\n    height: 3rem;\n    font-size: 1.5rem;\n    line-height: 2rem;\n    display: flex;\n    align-items: center;\n    padding: 0 0.75rem;\n    width: 100%;\n    border-bottom: 1px solid var(--header-border);\n  }\n\n  .title {\n    flex: 1;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ModalNav.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport PreferencesNavItem from '@pkg/components/Preferences/ModalNavItem.vue';\n\nexport default defineComponent({\n  name:       'preferences-nav',\n  components: { NavItem: PreferencesNavItem },\n  props:      {\n    currentNavItem: {\n      type:     String,\n      required: true,\n    },\n    navItems: {\n      type:     Array,\n      required: true,\n    },\n  },\n\n  methods: {\n    navClicked(tabName: string) {\n      if (tabName !== this.$props.currentNavItem) {\n        this.$emit('nav-changed', tabName);\n      }\n    },\n    navToKebab(navItem: string): string {\n      return `nav-${ navItem.toLowerCase().replaceAll(' ', '-') }`;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"preferences-nav\">\n    <nav-item\n      v-for=\"navItem in navItems\"\n      :key=\"navItem\"\n      :data-test=\"navToKebab(navItem)\"\n      :name=\"navItem\"\n      :active=\"currentNavItem === navItem\"\n      @click=\"navClicked\"\n    >\n      {{ navItem }}\n    </nav-item>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .preferences-nav {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    border-right: 1px solid var(--header-border);\n    padding-top: 0.75rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/ModalNavItem.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name:  'preferences-nav-item',\n  props: {\n    /**\n     * Indicates if the nav item is active\n     */\n    active: {\n      type:    Boolean,\n      default: false,\n    },\n    /**\n     * The Nav Item name\n     */\n    name: {\n      type:     String,\n      required: true,\n    },\n  },\n  emits:   ['click'],\n  methods: {\n    navClicked() {\n      this.$emit('click', this.name);\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    class=\"preferences-nav-item\"\n    :class=\"{ active }\"\n    @click=\"navClicked\"\n  >\n    <slot>Menu Item</slot>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .preferences-nav-item {\n    font-size: 1.125rem;\n    line-height: 1.75rem;\n    padding: 0.5rem 0.75rem;\n    cursor: pointer;\n    user-select: none;\n  }\n\n  .active {\n    background-color: var(--nav-active);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/VirtualMachineEmulation.vue",
    "content": "<script lang=\"ts\">\n\nimport { RadioButton, RadioGroup } from '@rancher/components';\nimport semver from 'semver';\nimport { defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport IncompatiblePreferencesAlert, { CompatiblePrefs } from '@pkg/components/IncompatiblePreferencesAlert.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { MountType, Settings, VMType } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-virtual-machine-emulation',\n  components: {\n    IncompatiblePreferencesAlert,\n    RadioGroup,\n    RdFieldset,\n    RdCheckbox,\n    RadioButton,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    ...mapState('transientSettings', ['macOsVersion', 'isArm']),\n    options(): {\n      label:           string,\n      value:           VMType,\n      description:     string,\n      disabled:        boolean,\n      compatiblePrefs: CompatiblePrefs | []\n    }[] {\n      return Object.values(VMType)\n        .map((x) => {\n          return {\n            label:           this.t(`virtualMachine.type.options.${ x }.label`),\n            value:           x,\n            description:     this.t(`virtualMachine.type.options.${ x }.description`, {}, true),\n            disabled:        x === VMType.VZ && this.vzDisabled,\n            compatiblePrefs: this.getCompatiblePrefs(x),\n          };\n        });\n    },\n    groupName(): string {\n      return 'vmType';\n    },\n    vZSelected(): boolean {\n      return this.preferences.virtualMachine.type === VMType.VZ;\n    },\n    vzDisabled(): boolean {\n      return semver.lt(this.macOsVersion.version, '13.0.0') || (this.isArm && semver.lt(this.macOsVersion.version, '13.3.0'));\n    },\n    rosettaDisabled(): boolean {\n      return !this.isArm && !(process.env.RD_TEST ?? '').includes('screenshots');\n    },\n    arch(): string {\n      return this.isArm ? 'arm64' : 'x64';\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n    disabledVmTypeTooltip(disabled: boolean): { content: string } | Record<string, never> {\n      let tooltip = {};\n\n      if (disabled) {\n        tooltip = { content: this.t(`prefs.onlyFromVentura_${ this.arch }`, undefined, true) };\n      }\n\n      return tooltip;\n    },\n    getCompatiblePrefs(vmType: VMType): CompatiblePrefs | [] {\n      const compatiblePrefs: CompatiblePrefs = [];\n\n      switch (vmType) {\n      case VMType.QEMU:\n        if (this.preferences.virtualMachine.mount.type === MountType.VIRTIOFS) {\n          compatiblePrefs.push(\n            {\n              title: MountType.REVERSE_SSHFS, navItemName: 'Virtual Machine', tabName: 'volumes',\n            },\n            {\n              title: MountType.NINEP, navItemName: 'Virtual Machine', tabName: 'volumes',\n            } );\n        }\n        break;\n      case VMType.VZ:\n        if (this.preferences.virtualMachine.mount.type === MountType.NINEP) {\n          compatiblePrefs.push(\n            {\n              title: MountType.REVERSE_SSHFS, navItemName: 'Virtual Machine', tabName: 'volumes',\n            },\n            {\n              title: MountType.VIRTIOFS, navItemName: 'Virtual Machine', tabName: 'volumes',\n            } );\n        }\n        break;\n      }\n\n      return compatiblePrefs;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"virtual-machine-emulation\">\n    <div class=\"row\">\n      <div class=\"col span-6\">\n        <rd-fieldset\n          data-test=\"vmType\"\n          :legend-text=\"t('virtualMachine.type.legend')\"\n          :is-locked=\"isPreferenceLocked('virtualMachine.type')\"\n        >\n          <template #default=\"{ isLocked }\">\n            <radio-group\n              :options=\"options\"\n              :name=\"groupName\"\n              :disabled=\"isLocked\"\n              :class=\"{ 'locked-radio': isLocked }\"\n            >\n              <template\n                v-for=\"(option, index) in options\"\n                #[index]=\"{ isDisabled }\"\n              >\n                <radio-button\n                  :key=\"groupName + '-' + index\"\n                  v-tooltip=\"disabledVmTypeTooltip(option.disabled)\"\n                  :name=\"groupName\"\n                  :value=\"preferences.virtualMachine.type\"\n                  :val=\"option.value\"\n                  :disabled=\"option.disabled || isDisabled\"\n                  :data-test=\"option.label\"\n                  @update:value=\"onChange('virtualMachine.type', $event)\"\n                >\n                  <template #label>\n                    {{ option.label }}\n                  </template>\n                  <template #description>\n                    {{ option.description }}\n                    <incompatible-preferences-alert\n                      v-if=\"option.value === preferences.virtualMachine.type\"\n                      :compatible-prefs=\"option.compatiblePrefs\"\n                    />\n                  </template>\n                </radio-button>\n              </template>\n            </radio-group>\n          </template>\n        </rd-fieldset>\n      </div>\n      <div\n        v-if=\"vZSelected && !rosettaDisabled\"\n        class=\"col span-6 vz-sub-options\"\n      >\n        <rd-fieldset\n          data-test=\"useRosetta\"\n          :legend-text=\"t('virtualMachine.useRosetta.legend')\"\n        >\n          <rd-checkbox\n            :label=\"t('virtualMachine.useRosetta.label')\"\n            :value=\"preferences.virtualMachine.useRosetta\"\n            :is-locked=\"isPreferenceLocked('virtualMachine.useRosetta')\"\n            @update:value=\"onChange('virtualMachine.useRosetta', $event)\"\n          />\n        </rd-fieldset>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .vz-sub-options {\n    border-left: 1px solid var(--border);\n    padding-left: 1rem;\n    display: flex;\n    flex-direction: column;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/VirtualMachineHardware.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport SystemPreferences from '@pkg/components/SystemPreferences.vue';\nimport { defaultSettings, Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-virtual-machine-hardware',\n  components: { SystemPreferences },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  data() {\n    return { settings: defaultSettings };\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    hasSystemPreferences(): boolean {\n      return !os.platform().startsWith('win');\n    },\n    availMemoryInGB(): number {\n      return Math.ceil(os.totalmem() / 2 ** 30);\n    },\n    availNumCPUs(): number {\n      return os.cpus().length;\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"virtual-machine-hardware\">\n    <system-preferences\n      v-if=\"hasSystemPreferences\"\n      :memory-in-g-b=\"preferences.virtualMachine.memoryInGB\"\n      :number-c-p-us=\"preferences.virtualMachine.numberCPUs\"\n      :avail-memory-in-g-b=\"availMemoryInGB\"\n      :avail-num-c-p-us=\"availNumCPUs\"\n      :reserved-memory-in-g-b=\"6\"\n      :reserved-num-c-p-us=\"1\"\n      :is-locked-memory=\"isPreferenceLocked('virtualMachine.memoryInGB')\"\n      :is-locked-cpu=\"isPreferenceLocked('virtualMachine.numberCPUs')\"\n      @update:memory=\"onChange('virtualMachine.memoryInGB', $event)\"\n      @update:cpu=\"onChange('virtualMachine.numberCPUs', $event)\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/VirtualMachineVolumes.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\n\nimport MountTypeSelector from '@pkg/components/MountTypeSelector.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-virtual-machine-volumes',\n  components: { MountTypeSelector },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"virtual-machine-volumes\">\n    <mount-type-selector\n      :preferences=\"preferences\"\n      @update=\"onChange\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/WslIntegrations.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport WslIntegration from '@pkg/components/WSLIntegration.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-wsl-integrations',\n  components: { WslIntegration, RdFieldset },\n  props:      {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: { ...mapGetters('preferences', ['getWslIntegrations']) },\n  methods:  {\n    onChange(distro: string, value: boolean) {\n      const property: keyof RecursiveTypes<Settings> = `WSL.integrations[\"${ distro }\"]` as any;\n\n      this.$store.dispatch('preferences/updateWslIntegrations', { distribution: `[\"${ distro }\"]`, value });\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"wsl-integrations\">\n    <rd-fieldset\n      data-test=\"wslIntegrations\"\n      :legend-text=\"t('integrations.windows.description', { }, true)\"\n    >\n      <wsl-integration\n        data-test-id=\"wsl-integration-list\"\n        :integrations=\"getWslIntegrations\"\n        @integration-set=\"onChange\"\n      />\n    </rd-fieldset>\n  </div>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Preferences/WslProxy.vue",
    "content": "<script lang=\"ts\">\n\nimport { StringList } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport RdInput from '@pkg/components/RdInput.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { Settings } from '@pkg/config/settings';\nimport { RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'preferences-wsl-proxy',\n  components: {\n    RdCheckbox, RdFieldset, RdInput, StringList,\n  },\n  props: {\n    preferences: {\n      type:     Object as PropType<Settings>,\n      required: true,\n    },\n  },\n  computed: {\n    ...mapGetters('preferences', ['isPreferenceLocked']),\n    isFieldDisabled() {\n      return !(this.preferences.experimental.virtualMachine.proxy.enabled);\n    },\n    noproxyErrorMessages(): { duplicate: string } {\n      return { duplicate: this.t('virtualMachine.proxy.noproxy.errors.duplicate') };\n    },\n    isNoProxyFieldReadOnly() {\n      return this.isFieldDisabled || this.isPreferenceLocked('experimental.virtualMachine.proxy.noproxy');\n    },\n  },\n  methods: {\n    onChange<P extends keyof RecursiveTypes<Settings>>(property: P, value: RecursiveTypes<Settings>[P]) {\n      this.$store.dispatch('preferences/updatePreferencesData', { property, value });\n    },\n    onType(item: string) {\n      if (item) {\n        this.$store.dispatch('preferences/setCanApply', item.trim().length > 0);\n      }\n    },\n    onDuplicate(err: boolean) {\n      if (err) {\n        this.$store.dispatch('preferences/setCanApply', false);\n      }\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"wsl-proxy\">\n    <rd-fieldset\n      :legend-text=\"t('virtualMachine.proxy.legend')\"\n      :is-experimental=\"true\"\n    >\n      <rd-checkbox\n        :label=\"t('virtualMachine.proxy.label', { }, true)\"\n        :value=\"preferences.experimental.virtualMachine.proxy.enabled\"\n        :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.enabled')\"\n        @update:value=\"onChange('experimental.virtualMachine.proxy.enabled', $event)\"\n      />\n    </rd-fieldset>\n    <hr>\n    <div class=\"proxy-row\">\n      <div class=\"proxy-col\">\n        <rd-fieldset\n          data-test=\"addressTitle\"\n          class=\"wsl-proxy-fieldset\"\n          :legend-text=\"t('virtualMachine.proxy.addressTitle', { }, true)\"\n        >\n          <rd-input\n            :placeholder=\"t('virtualMachine.proxy.address', { }, true)\"\n            :disabled=\"isFieldDisabled\"\n            :value=\"preferences.experimental.virtualMachine.proxy.address\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.address')\"\n            @input=\"onChange('experimental.virtualMachine.proxy.address', $event.target.value)\"\n          />\n          <rd-input\n            type=\"number\"\n            :placeholder=\"t('virtualMachine.proxy.port', { }, true)\"\n            :disabled=\"isFieldDisabled\"\n            :value=\"preferences.experimental.virtualMachine.proxy.port\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.port')\"\n            @input=\"onChange('experimental.virtualMachine.proxy.port', $event.target.value)\"\n          />\n        </rd-fieldset>\n        <rd-fieldset\n          class=\"wsl-proxy-fieldset\"\n          :legend-text=\"t('virtualMachine.proxy.authTitle', { }, true)\"\n        >\n          <rd-input\n            :placeholder=\"t('virtualMachine.proxy.username', { }, true)\"\n            :disabled=\"isFieldDisabled\"\n            :value=\"preferences.experimental.virtualMachine.proxy.username\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.username')\"\n            @input=\"onChange('experimental.virtualMachine.proxy.username', $event.target.value)\"\n          />\n          <rd-input\n            type=\"password\"\n            :placeholder=\"t('virtualMachine.proxy.password', { }, true)\"\n            :disabled=\"isFieldDisabled\"\n            :value=\"preferences.experimental.virtualMachine.proxy.password\"\n            :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.password')\"\n            @input=\"onChange('experimental.virtualMachine.proxy.password', $event.target.value)\"\n          />\n        </rd-fieldset>\n      </div>\n      <div class=\"proxy-col\">\n        <rd-fieldset\n          :legend-text=\"t('virtualMachine.proxy.noproxy.legend', { }, true)\"\n          :is-locked=\"isPreferenceLocked('experimental.virtualMachine.proxy.noproxy')\"\n        >\n          <string-list\n            :placeholder=\"t('virtualMachine.proxy.noproxy.placeholder', { }, true)\"\n            :readonly=\"isNoProxyFieldReadOnly\"\n            :actions-position=\"'left'\"\n            :items=\"preferences.experimental.virtualMachine.proxy.noproxy\"\n            :error-messages=\"noproxyErrorMessages\"\n            bulk-addition-delimiter=\",\"\n            @change=\"onChange('experimental.virtualMachine.proxy.noproxy', $event)\"\n            @type:item=\"onType($event)\"\n            @errors=\"onDuplicate($event.duplicate)\"\n          />\n        </rd-fieldset>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .proxy-row {\n    display: flex;\n    flex-direction: row;\n    gap: 1rem;\n  }\n  .proxy-col {\n    display: flex;\n    flex-direction: column;\n    gap: 1rem;\n    width: 50%;\n\n    .string-list {\n      :deep(.string-list-box) {\n        min-height: unset;\n        height: 195px;\n      }\n\n      :deep(.string-list-footer) {\n        padding-right: 2rem;\n      }\n\n      :deep(.readonly) {\n        background-color: var(--input-disabled-bg);\n        color: var(--input-disabled-text);\n        opacity: 1;\n        cursor: not-allowed;\n      }\n    }\n  }\n  .wsl-proxy {\n    display: flex;\n    flex-direction: column;\n  }\n  .wsl-proxy-fieldset {\n    display: flex;\n    flex-direction: row;\n    gap: .5rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/RdInput.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:         'rd-input',\n  inheritAttrs: false,\n  props:        {\n    value: {\n      type:    [String, Number],\n      default: null,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n    tooltip: {\n      type:    String,\n      default: null,\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    class=\"rd-input-container\"\n    :class=\"$attrs.class\"\n  >\n    <input\n      v-bind=\"$attrs\"\n      :value=\"value\"\n      :class=\"{ locked: isLocked && !$attrs.disabled }\"\n      :disabled=\"!!$attrs.disabled || isLocked\"\n    >\n    <slot name=\"after\">\n      <i\n        v-if=\"isLocked\"\n        v-tooltip=\"{\n          content: tooltip || t('preferences.locked.tooltip', undefined, true),\n          placement: 'right',\n        }\"\n        class=\"icon icon-lock\"\n      />\n    </slot>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .rd-input-container {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n\n    .locked {\n      color: var(--input-locked-text);\n\n      &:hover {\n        color: var(--input-locked-text);\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/RdProgress.vue",
    "content": "<!-- A progress bar, with support for indeterminate progress -->\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n  name:  'rd-progress',\n  props: {\n    indeterminate: {\n      type:    Boolean,\n      default: false,\n    },\n    value: {\n      type:    Number,\n      default: 0,\n    },\n    maximum: {\n      type:    Number,\n      default: 100,\n    },\n    primaryColor: {\n      type:    String,\n      default: '--primary',\n    },\n    secondaryColor: {\n      type:    String,\n      default: '--border',\n    },\n  },\n  computed: {\n    indicatorStyle(): Record<string, string> {\n      if (this.indeterminate) {\n        return {\n          width:      '200%',\n          background: `repeating-linear-gradient(\n              -45deg,\n              var(${ this.primaryColor }),\n              transparent 6.25%,\n              var(${ this.primaryColor }) 12.5%\n            )`.replace(/\\s+/g, ' '),\n        };\n      }\n\n      return {\n        width:           `${ this.value * 100 / this.maximum }%`,\n        backgroundColor: `var(${ this.primaryColor })`,\n      };\n    },\n    barStyle(): Record<string, string> {\n      return { backgroundColor: `var(${ this.secondaryColor })` };\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    class=\"bar\"\n    :class=\"{ indeterminate }\"\n    :style=\"barStyle\"\n  >\n    <div\n      class=\"indicator\"\n      :style=\"indicatorStyle\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .bar {\n    $height: 15px;\n\n    width: 100%;\n    height: $height;\n    border-radius: math.div($height, 2);\n    overflow: hidden;\n    position: relative;\n\n    .indicator {\n      height: 100%;\n      position: absolute;\n    }\n\n    &.indeterminate {\n      .indicator {\n        animation: linear infinite indeterminate 8s;\n      }\n      @keyframes indeterminate {\n        from {\n          left: -100%;\n        }\n        to {\n          left: 0%;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/RdSelect.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:         'rd-select',\n  inheritAttrs: false,\n  props:        {\n    modelValue: {\n      type:    String,\n      default: '',\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n    tooltip: {\n      type:    String,\n      default: null,\n    },\n  },\n  emits:    ['input'],\n  computed: {\n    selectedValue: {\n      get(): string {\n        return this.modelValue;\n      },\n      set(newValue: string): void {\n        this.$emit('input', newValue);\n      },\n    },\n  },\n  methods: {\n    /**\n     * Ensure that the correct value is emitted by overriding the default\n     * listeners to supply a custom input event. Resolves an issue where the\n     * entire vnode emits when using v-model.\n     */\n    overrideInput(e: any) {\n      this.$emit('input', e.target.value);\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    class=\"rd-select-container\"\n    :class=\"$attrs.class\"\n  >\n    <select\n      v-bind=\"$attrs\"\n      v-model=\"selectedValue\"\n      :class=\"{ locked: isLocked && !$attrs.disabled }\"\n      :disabled=\"!!$attrs.disabled || isLocked\"\n      @input=\"overrideInput($event)\"\n    >\n      <slot name=\"default\">\n        <!-- Slot contents -->\n      </slot>\n    </select>\n    <slot name=\"after\">\n      <i\n        v-if=\"isLocked\"\n        v-tooltip=\"{\n          content: tooltip || t('preferences.locked.tooltip', undefined, true),\n          placement: 'right',\n        }\"\n        class=\"icon icon-lock\"\n      />\n    </slot>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .rd-select-container {\n    display: flex;\n    align-items: center;\n    gap: 0.5rem;\n  }\n\n  .locked {\n    color: var(--dropdown-locked-text);\n\n    &:hover {\n      color: var(--dropdown-locked-text);\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SnapshotCard.vue",
    "content": "<script lang=\"ts\">\nimport dayjs from 'dayjs';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport { State as EngineStates } from '@pkg/backend/k8s';\nimport { Snapshot } from '@pkg/main/snapshots/types';\nimport { currentTime } from '@pkg/utils/dateUtils';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nimport type { PropType } from 'vue';\n\nfunction formatDate(value: string) {\n  if (!value) {\n    return null;\n  }\n\n  const date = dayjs(value);\n\n  return {\n    date: date.format('YYYY-MM-DD'),\n    time: date.format('HH:mm'),\n  };\n}\n\nexport default defineComponent({\n  name:  'snapshot-card',\n  props: {\n    value: {\n      type:     Object as PropType<Snapshot>,\n      required: true,\n    },\n  },\n\n  computed: {\n    ...mapGetters('k8sManager', { getK8sState: 'getK8sState' }),\n    snapshot(): Snapshot & { formattedCreateDate: { date: string, time: string } | null } {\n      return {\n        ...this.value,\n        formattedCreateDate: formatDate(this.value.created),\n      };\n    },\n    isRestoreDisabled(): boolean {\n      const k8sState = this.getK8sState;\n\n      return ![EngineStates.STARTED, EngineStates.DISABLED, EngineStates.ERROR].includes(k8sState);\n    },\n  },\n\n  methods: {\n    async restore() {\n      const ok = await this.showConfirmationDialog('restore');\n\n      /** Clear old event on Snapshots page */\n      ipcRenderer.send('snapshot', null);\n\n      let snapshotCancelled = false;\n\n      ipcRenderer.once('snapshot/cancel', () => {\n        snapshotCancelled = true;\n        ipcRenderer.send(\n          'snapshot',\n          {\n            type:         'restore',\n            result:       'cancel',\n            snapshotName: this.snapshot?.name,\n            eventTime:    currentTime(),\n          },\n        );\n      });\n\n      if (ok) {\n        ipcRenderer.send('preferences-close');\n        ipcRenderer.on('dialog/mounted', async() => {\n          const error = await this.$store.dispatch('snapshots/restore', this.snapshot.name);\n\n          if (error) {\n            ipcRenderer.send(\n              'dialog/error',\n              {\n                dialog:           'SnapshotsDialog',\n                error,\n                errorTitle:       this.t('snapshots.dialog.restore.error.header'),\n                errorDescription: this.t('snapshots.dialog.restore.error.description', { snapshot: this.snapshot.name }, true),\n                errorButton:      this.t('snapshots.dialog.restore.error.buttonText'),\n              });\n          } else {\n            ipcRenderer.send('dialog/close', { dialog: 'SnapshotsDialog', snapshotEventType: 'restore' });\n            ipcRenderer.send(\n              'snapshot',\n              {\n                type:         'restore',\n                result:       snapshotCancelled ? 'cancel' : 'success',\n                snapshotName: this.snapshot?.name,\n                eventTime:    currentTime(),\n              },\n            );\n          }\n        });\n\n        await this.showRestoringSnapshotDialog('restore');\n        ipcRenderer.removeAllListeners('dialog/mounted');\n      }\n    },\n\n    async remove() {\n      const ok = await this.showConfirmationDialog('delete');\n\n      /** Clear old event on Snapshots page */\n      ipcRenderer.send('snapshot', null);\n\n      if (ok) {\n        const error = await this.$store.dispatch('snapshots/delete', this.snapshot.name);\n\n        ipcRenderer.send('snapshot', {\n          type:         'delete',\n          result:       error ? 'error' : 'success',\n          error,\n          snapshotName: this.snapshot.name,\n          eventTime:    currentTime(),\n        });\n      }\n    },\n\n    async showConfirmationDialog(type: 'restore' | 'delete') {\n      const confirm: { response: number } = await ipcRenderer.invoke(\n        'show-snapshots-confirm-dialog',\n        {\n          window: {\n            buttons: [\n              this.t(`snapshots.dialog.${ type }.actions.cancel`),\n              this.t(`snapshots.dialog.${ type }.actions.ok`),\n            ],\n            cancelId: 0,\n          },\n          format: {\n            header:            this.t(`snapshots.dialog.${ type }.header`, { snapshot: this.snapshot.name }),\n            snapshot:          this.snapshot,\n            message:           type === 'restore' ? this.t(`snapshots.dialog.${ type }.info`, { }, true) : '',\n            showProgressBar:   false,\n            snapshotEventType: 'confirm',\n          },\n        },\n      );\n\n      return confirm.response;\n    },\n\n    async showRestoringSnapshotDialog(type: 'restore' | 'delete') {\n      const snapshot = this.snapshot.name.length > 32 ? `${ this.snapshot.name.substring(0, 30) }...` : this.snapshot.name;\n\n      await ipcRenderer.invoke(\n        'show-snapshots-blocking-dialog',\n        {\n          window: {\n            buttons: [\n              this.t(`snapshots.dialog.${ type }.actions.cancel`),\n            ],\n            cancelId: 0,\n          },\n          format: {\n            header:            this.t('snapshots.dialog.restoring.header', { snapshot }),\n            message:           this.t('snapshots.dialog.restoring.message', { snapshot }, true),\n            showProgressBar:   true,\n            snapshotEventType: 'restore',\n          },\n        },\n      );\n    },\n  },\n});\n\n</script>\n\n<template>\n  <div\n    v-if=\"snapshot\"\n    class=\"snapshot-card\"\n  >\n    <div class=\"content\">\n      <div class=\"header\">\n        <h2>\n          {{ snapshot.name }}\n        </h2>\n        <div class=\"created\">\n          <span\n            v-if=\"snapshot.formattedCreateDate\"\n            v-clean-html=\"t('snapshots.card.created', { date: snapshot.formattedCreateDate.date, time: snapshot.formattedCreateDate.time }, true)\"\n            class=\"value\"\n          />\n        </div>\n      </div>\n      <div\n        v-if=\"snapshot.description\"\n        class=\"description\"\n      >\n        <span class=\"value\">{{ snapshot.description }}</span>\n      </div>\n    </div>\n    <div class=\"actions\">\n      <button\n        class=\"btn btn-xs role-secondary remove\"\n        @click=\"remove\"\n      >\n        {{ t('snapshots.card.action.remove') }}\n      </button>\n      <button\n        class=\"btn btn-xs role-primary restore\"\n        :disabled=\"isRestoreDisabled\"\n        @click=\"restore\"\n      >\n        {{ t('snapshots.card.action.restore') }}\n      </button>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .snapshot-card {\n    display: grid;\n    grid-template-columns: auto 300px;\n    border: 1px solid var(--border);\n    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);\n    padding: 25px;\n    min-height: 130px;\n\n    .content {\n      display: flex;\n      flex-direction: column;\n      gap: 15px;\n      flex-grow: 1;\n      min-width: 300px;\n      .header {\n        h2 {\n          max-width: 500px;\n          white-space: nowrap;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          margin: 0 0 5px 0;\n        }\n      }\n      .description {\n        max-width: 550px;\n        word-wrap: break-word;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n    }\n\n    .actions {\n      display: flex;\n      .btn {\n        width: 145px;\n        height: 30px;\n        margin-left: 10px;\n      }\n    }\n\n    .value {\n      color: var(--input-label);\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Snapshots.vue",
    "content": "<script lang=\"ts\">\nimport { Banner } from '@rancher/components';\nimport isEmpty from 'lodash/isEmpty';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport EmptyState from '@pkg/components/EmptyState.vue';\nimport SnapshotCard from '@pkg/components/SnapshotCard.vue';\nimport { Snapshot, SnapshotEvent } from '@pkg/main/snapshots/types';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { escapeHtml } from '@pkg/utils/string';\n\ninterface Data {\n  snapshotEvent:            SnapshotEvent | null;\n  snapshotsPollingInterval: ReturnType<typeof setInterval> | null;\n  isEmpty:                  boolean;\n}\n\nexport default defineComponent({\n  name:       'snapshots',\n  components: {\n    Banner,\n    EmptyState,\n    SnapshotCard,\n  },\n\n  data(): Data {\n    return {\n      snapshotsPollingInterval: null,\n      snapshotEvent:            null,\n      isEmpty:                  false,\n    };\n  },\n\n  computed: { ...mapGetters('snapshots', { snapshots: 'list' }) },\n\n  watch: {\n    snapshots(list) {\n      this.isEmpty = list?.length === 0;\n    },\n  },\n\n  beforeMount() {\n    this.$store.dispatch('snapshots/fetch');\n    this.pollingStart();\n\n    ipcRenderer.on('snapshot', (_, event) => {\n      this.snapshotEvent = event;\n    });\n\n    if (isEmpty(this.$route.params)) {\n      return;\n    }\n\n    const {\n      type, result, snapshotName, eventTime,\n    } = this.$route.params as SnapshotEvent;\n\n    this.snapshotEvent = {\n      type, result, snapshotName, eventTime,\n    };\n  },\n\n  beforeUnmount() {\n    if (this.snapshotsPollingInterval) {\n      clearInterval(this.snapshotsPollingInterval);\n    }\n    ipcRenderer.removeAllListeners('snapshot');\n  },\n\n  methods: {\n    pollingStart() {\n      this.snapshotsPollingInterval = setInterval(() => {\n        this.$store.dispatch('snapshots/fetch');\n      }, 1500);\n    },\n    escapeHtml,\n  },\n});\n</script>\n\n<template>\n  <div class=\"snapshots\">\n    <div\n      v-if=\"snapshotEvent\"\n      class=\"event\"\n    >\n      <Banner\n        class=\"banner\"\n        :color=\"snapshotEvent.result\"\n        :closable=\"true\"\n        @close=\"snapshotEvent = null\"\n      >\n        <span\n          v-clean-html=\"t(`snapshots.info.${snapshotEvent.type}.${snapshotEvent.result}`,\n                          { snapshot: escapeHtml(snapshotEvent.snapshotName), error: snapshotEvent.error }, true)\"\n          class=\"event-message\"\n        />\n        <span\n          v-clean-html=\"t('snapshots.info.when', { time: snapshotEvent.eventTime })\"\n          class=\"event-message\"\n        />\n      </Banner>\n    </div>\n    <div\n      class=\"cards\"\n      :class=\"{ margin: !snapshotEvent }\"\n    >\n      <div\n        v-for=\"(item) of snapshots\"\n        :key=\"item.name\"\n      >\n        <SnapshotCard\n          class=\"mb-20\"\n          :value=\"item\"\n        />\n      </div>\n      <div v-if=\"isEmpty\">\n        <empty-state\n          class=\"mt-10\"\n          :icon=\"t('snapshots.empty.icon')\"\n          :heading=\"t('snapshots.empty.heading')\"\n          :body=\"t('snapshots.empty.body')\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .event-message {\n    word-wrap: break-word;\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n  .snapshots {\n    > * {\n      padding: 0 5px 0 5px;\n    }\n\n    .event {\n      position: sticky;\n      top: 0;\n      background: var(--body-bg);\n\n      .banner {\n        margin: 0;\n        :deep(.banner__content) {\n          margin-top: 8px;\n          margin-bottom: 15px;\n\n          .banner__content__closer {\n            height: 50px;\n          }\n        }\n      }\n    }\n\n    .cards {\n      &.margin {\n        margin-top: 13px;\n      }\n      overflow-y: auto;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SnapshotsButtonCreate.vue",
    "content": "<template>\n  <button\n    data-test=\"createSnapshotButton\"\n    type=\"button\"\n    class=\"btn btn-xs role-secondary\"\n    @click=\"route\"\n  >\n    {{ t('snapshots.action.create') }}\n  </button>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name: 'snapshots-button-create',\n\n  methods: {\n    route() {\n      this.$router.push({ name: 'snapshots-create' });\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n\n  .btn-xs {\n    min-height: 2.25rem;\n    max-height: 2.25rem;\n    line-height: 0.25rem;\n  }\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/THead.vue",
    "content": "<script>\nimport { Checkbox } from '@rancher/components';\n\nimport { SOME, NONE } from './selection';\n\nimport LabeledSelect from '@pkg/components/form/LabeledSelect';\nimport { AUTO, CENTER, fitOnScreen } from '@pkg/utils/position';\n\nexport default {\n  components: { Checkbox, LabeledSelect },\n  props:      {\n    columns: {\n      type:     Array,\n      required: true,\n    },\n    sortBy: {\n      type:     String,\n      required: true,\n    },\n    defaultSortBy: {\n      type:    String,\n      default: '',\n    },\n    group: {\n      type:    String,\n      default: '',\n    },\n    groupOptions: {\n      type:    Array,\n      default: () => [],\n    },\n    descending: {\n      type:     Boolean,\n      required: true,\n    },\n    hasAdvancedFiltering: {\n      type:     Boolean,\n      required: false,\n    },\n    tableColsOptions: {\n      type:    Array,\n      default: () => [],\n    },\n    tableActions: {\n      type:     Boolean,\n      required: true,\n    },\n    rowActions: {\n      type:     Boolean,\n      required: true,\n    },\n    howMuchSelected: {\n      type:     String,\n      required: true,\n    },\n    checkWidth: {\n      type:    Number,\n      default: 30,\n    },\n    rowActionsWidth: {\n      type:     Number,\n      required: true,\n    },\n    subExpandColumn: {\n      type:    Boolean,\n      default: false,\n    },\n    expandWidth: {\n      type:    Number,\n      default: 30,\n    },\n    labelFor: {\n      type:     Function,\n      required: true,\n    },\n    noRows: {\n      type:    Boolean,\n      default: true,\n    },\n    noResults: {\n      type:    Boolean,\n      default: true,\n    },\n    loading: {\n      type:     Boolean,\n      required: false,\n    },\n  },\n\n  data() {\n    return {\n      tableColsOptionsVisibility: false,\n      tableColsMenuPosition:      null,\n    };\n  },\n\n  watch: {\n    advancedFilteringValues() {\n      // passing different dummy args to make sure update is triggered\n      this.watcherUpdateLiveAndDelayed(true, false);\n    },\n    tableColsOptionsVisibility(neu) {\n      if (neu) {\n        // check if user clicked outside the table cols options box\n        window.addEventListener('click', this.onClickOutside);\n\n        // update filtering options and toggleable cols every time dropdown is open\n        this.$emit('update-cols-options');\n      } else {\n        // unregister click event\n        window.removeEventListener('click', this.onClickOutside);\n      }\n    },\n  },\n  computed: {\n    isAll: {\n      get() {\n        return this.howMuchSelected !== NONE;\n      },\n\n      set(value) {\n        this.$emit('on-toggle-all', value);\n      },\n    },\n    hasAdvGrouping() {\n      return this.group?.length && this.groupOptions?.length;\n    },\n    advGroup: {\n      get() {\n        return this.group || this.advGroup;\n      },\n\n      set(val) {\n        this.$emit('group-value-change', val);\n      },\n    },\n\n    isIndeterminate() {\n      return this.howMuchSelected === SOME;\n    },\n    hasColumnWithSubLabel() {\n      return this.columns.some((col) => col.subLabel);\n    },\n  },\n\n  methods: {\n    changeSort(e, col) {\n      if ( !col.sort ) {\n        return;\n      }\n\n      let desc = false;\n\n      if ( this.sortBy === col.name ) {\n        desc = !this.descending;\n      }\n\n      this.$emit('on-sort-change', col.name, desc);\n    },\n\n    isCurrent(col) {\n      return col.name === this.sortBy;\n    },\n\n    tableColsOptionsClick(ev) {\n      // set menu position\n      const menu = document.querySelector('.table-options-container');\n      const elem = document.querySelector('.table-options-btn');\n\n      this.tableColsMenuPosition = fitOnScreen(menu, ev || elem, {\n        overlapX:  true,\n        fudgeX:    326,\n        fudgeY:    -22,\n        positionX: CENTER,\n        positionY: AUTO,\n      });\n\n      // toggle visibility\n      this.tableColsOptionsVisibility = !this.tableColsOptionsVisibility;\n    },\n\n    onClickOutside(event) {\n      const tableOpts = this.$refs['table-options'];\n\n      if (!tableOpts || tableOpts.contains(event.target)) {\n        return;\n      }\n      this.tableColsOptionsVisibility = false;\n    },\n\n    tableOptionsCheckbox(value, label) {\n      this.$emit('col-visibility-change', {\n        label,\n        value,\n      });\n    },\n\n    tooltip(col) {\n      if (!col.tooltip) {\n        return null;\n      }\n\n      const exists = this.$store.getters['i18n/exists'];\n\n      return exists(col.tooltip) ? this.t(col.tooltip) : col.tooltip;\n    },\n  },\n\n};\n</script>\n\n<template>\n  <thead>\n    <tr :class=\"{ loading, 'top-aligned': hasColumnWithSubLabel }\">\n      <th\n        v-if=\"tableActions\"\n        :width=\"checkWidth\"\n      >\n        <Checkbox\n          v-model:value=\"isAll\"\n          class=\"check\"\n          data-testid=\"sortable-table_check_select_all\"\n          :indeterminate=\"isIndeterminate\"\n          :disabled=\"noRows || noResults\"\n        />\n      </th>\n      <th\n        v-if=\"subExpandColumn\"\n        :width=\"expandWidth\"\n      />\n      <th\n        v-for=\"(col) in columns\"\n        v-show=\"!hasAdvancedFiltering || (hasAdvancedFiltering && col.isColVisible)\"\n        :key=\"col.name\"\n        :align=\"col.align || 'left'\"\n        :width=\"col.width\"\n        :class=\"{ sortable: col.sort, [col.breakpoint]: !!col.breakpoint }\"\n        @click.prevent=\"changeSort($event, col)\"\n      >\n        <div\n          class=\"table-header-container\"\n          :class=\"{ 'not-filterable': hasAdvancedFiltering && !col.isFilter }\"\n        >\n          <div\n            v-clean-tooltip=\"tooltip(col)\"\n            class=\"content\"\n          >\n            <span v-clean-html=\"labelFor(col)\" />\n            <span\n              v-if=\"col.subLabel\"\n              class=\"text-muted\"\n            >\n              {{ col.subLabel }}\n            </span>\n          </div>\n          <div\n            v-if=\"col.sort\"\n            class=\"sort\"\n          >\n            <i\n              v-show=\"hasAdvancedFiltering && !col.isFilter\"\n              v-clean-tooltip=\"t('sortableTable.tableHeader.noFilter')\"\n              class=\"icon icon-info not-filter-icon\"\n            />\n            <span class=\"icon-stack\">\n              <i class=\"icon icon-sort icon-stack-1x faded\" />\n              <i\n                v-if=\"isCurrent(col) && !descending\"\n                class=\"icon icon-sort-down icon-stack-1x\"\n              />\n              <i\n                v-if=\"isCurrent(col) && descending\"\n                class=\"icon icon-sort-up icon-stack-1x\"\n              />\n            </span>\n          </div>\n        </div>\n      </th>\n      <th\n        v-if=\"rowActions && hasAdvancedFiltering && tableColsOptions.length\"\n        :width=\"rowActionsWidth\"\n      >\n        <div\n          ref=\"table-options\"\n          class=\"table-options-group\"\n        >\n          <button\n            aria-haspopup=\"true\"\n            aria-expanded=\"false\"\n            type=\"button\"\n            class=\"btn btn-sm role-multi-action table-options-btn\"\n            @click=\"tableColsOptionsClick\"\n          >\n            <i class=\"icon icon-actions\" />\n          </button>\n          <div\n            v-show=\"tableColsOptionsVisibility\"\n            class=\"table-options-container\"\n            :style=\"tableColsMenuPosition\"\n          >\n            <div\n              v-if=\"hasAdvGrouping\"\n              class=\"table-options-grouping\"\n            >\n              <span class=\"table-options-col-subtitle\">{{ t('sortableTable.tableHeader.groupBy') }}:</span>\n              <LabeledSelect\n                v-model:value=\"advGroup\"\n                class=\"table-options-grouping-select\"\n                :clearable=\"true\"\n                :options=\"groupOptions\"\n                :disabled=\"false\"\n                :searchable=\"false\"\n                mode=\"edit\"\n                :multiple=\"false\"\n                :taggable=\"false\"\n              />\n            </div>\n            <p class=\"table-options-col-subtitle mb-20\">\n              {{ t('sortableTable.tableHeader.show') }}:\n            </p>\n            <ul>\n              <li\n                v-for=\"(col, index) in tableColsOptions\"\n                v-show=\"col.isTableOption\"\n                :key=\"index\"\n                :class=\"{ visible: !col.preventColToggle }\"\n              >\n                <Checkbox\n                  v-show=\"!col.preventColToggle\"\n                  v-model:value=\"col.isColVisible\"\n                  class=\"table-options-checkbox\"\n                  :label=\"col.label\"\n                  @update:value=\"tableOptionsCheckbox($event, col.label)\"\n                />\n              </li>\n            </ul>\n          </div>\n        </div>\n      </th>\n      <th\n        v-else-if=\"rowActions\"\n        :width=\"rowActionsWidth\"\n      />\n    </tr>\n  </thead>\n</template>\n\n  <style lang=\"scss\" scoped>\n    .table-options-group {\n\n      .table-options-btn.role-multi-action {\n        background-color: transparent;\n        border: none;\n        font-size: 18px;\n        &:hover, &:focus {\n          background-color: var(--accent-btn);\n          box-shadow: none;\n        }\n      }\n      .table-options-container {\n        width: 350px;\n        border: 1px solid var(--primary);\n        background-color: var(--body-bg);\n        padding: 20px;\n        z-index: 1;\n\n        .table-options-grouping {\n          display: flex;\n          align-items: center;\n          margin-bottom: 20px;\n\n          span {\n            white-space: nowrap;\n            margin-right: 10px;\n          }\n        }\n\n        ul {\n          list-style: none;\n          margin: 0;\n          padding: 0;\n          max-height: 200px;\n          overflow-y: auto;\n\n          li {\n            margin: 0;\n            padding: 0;\n\n            &.visible {\n              margin: 0 0 10px 0;\n            }\n          }\n        }\n      }\n    }\n\n    .sortable > SPAN {\n      cursor: pointer;\n      user-select: none;\n      white-space: nowrap;\n      &:hover,\n      &:active {\n        text-decoration: underline;\n        color: var(--body-text);\n      }\n    }\n\n    .top-aligned th {\n      vertical-align: top;\n      padding-top: 10px;\n    }\n\n    thead {\n      tr {\n        background-color: var(--sortable-table-header-bg);\n        color: var(--body-text);\n        text-align: left;\n\n        &:not(.loading) {\n          border-bottom: 1px solid var(--sortable-table-top-divider);\n        }\n      }\n    }\n\n    th {\n      padding: 8px 5px;\n      font-weight: normal;\n      border: 0;\n      color: var(--body-text);\n\n      .table-header-container {\n        display: flex;\n        align-items: baseline;\n\n        .content {\n          display: flex;\n          flex-direction: column;\n        }\n\n        &.not-filterable {\n          margin-top: -2px;\n\n          .icon-stack {\n            margin-top: -2px;\n          }\n        }\n\n        .not-filter-icon {\n          font-size: 16px;\n          color: var(--primary);\n          vertical-align: super;\n        }\n      }\n\n      &:first-child {\n        padding-left: 10px;\n      }\n\n      &:last-child {\n        padding-right: 10px;\n      }\n\n      &:not(.sortable) > SPAN {\n        display: block;\n        margin-bottom: 2px;\n      }\n\n      & A {\n        color: var(--body-text);\n      }\n\n      // Aligns with COLUMN_BREAKPOINTS\n      @media only screen and (max-width: map-get($breakpoints, '--viewport-4')) {\n        // HIDE column on sizes below 480px\n        &.tablet, &.laptop, &.desktop {\n          display: none;\n        }\n      }\n      @media only screen and (max-width: map-get($breakpoints, '--viewport-9')) {\n        // HIDE column on sizes below 992px\n        &.laptop, &.desktop {\n          display: none;\n        }\n      }\n      @media only screen and (max-width: map-get($breakpoints, '--viewport-12')) {\n        // HIDE column on sizes below 1281px\n        &.desktop {\n          display: none;\n        }\n      }\n    }\n\n    .icon-stack {\n      width: 12px;\n    }\n\n    .icon-sort {\n      &.faded {\n        opacity: .3;\n      }\n    }\n  </style>\n  <style lang=\"scss\">\n    .table-options-checkbox .checkbox-custom {\n      min-width: 14px;\n    }\n    .table-options-checkbox .checkbox-label {\n      color: var(--body-text);\n    }\n  </style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/actions.js",
    "content": "import debounce from 'lodash/debounce';\n\n// Use a visible display type to reduce flickering\nconst displayType = 'inline-block';\n\nexport default {\n\n  data() {\n    return {\n      bulkActionsClass:            'bulk',\n      bulkActionClass:             'bulk-action',\n      bulkActionsDropdownClass:    'bulk-actions-dropdown',\n      bulkActionAvailabilityClass: 'action-availability',\n\n      hiddenActions: [],\n\n      updateHiddenBulkActions: debounce(this.protectedUpdateHiddenBulkActions, 10),\n    };\n  },\n\n  beforeUnmount() {\n    window.removeEventListener('resize', this.onWindowResize);\n  },\n\n  mounted() {\n    window.addEventListener('resize', this.onWindowResize);\n    this.updateHiddenBulkActions();\n  },\n\n  watch: {\n    selectedRows() {\n      this.updateHiddenBulkActions();\n    },\n    keyedAvailableActions() {\n      this.updateHiddenBulkActions();\n    },\n  },\n\n  computed: {\n    availableActions() {\n      return this.bulkActionsForSelection.filter((act) => !act.external);\n    },\n\n    keyedAvailableActions() {\n      return this.availableActions.map((aa) => aa.action);\n    },\n\n    selectedRowsText() {\n      if (!this.selectedRows.length) {\n        return null;\n      }\n\n      return this.t('sortableTable.actionAvailability.selected', { actionable: this.selectedRows.length });\n    },\n\n    // Shows a tooltip if the bulk action that the user is hovering over cannot be applied to all selected rows\n    actionTooltip() {\n      if (!this.selectedRows.length || !this.actionOfInterest) {\n        return null;\n      }\n\n      const runnableTotal = this.selectedRows.filter(this.canRunBulkActionOfInterest).length;\n\n      if (runnableTotal === this.selectedRows.length) {\n        return null;\n      }\n\n      return this.t('sortableTable.actionAvailability.some', {\n        actionable: runnableTotal,\n        total:      this.selectedRows.length,\n      });\n    },\n  },\n\n  methods: {\n    onWindowResize() {\n      this.updateHiddenBulkActions();\n      this.onScroll();\n    },\n\n    /**\n     * Determine if any actions wrap over to a new line, if so group them into a dropdown instead\n     */\n    protectedUpdateHiddenBulkActions() {\n      if (!this.$refs.container) {\n        return;\n      }\n\n      const actionsContainer = this.$refs.container.querySelector(`.${ this.bulkActionsClass }`);\n      const actionsDropdown = this.$refs.container.querySelector(`.${ this.bulkActionsDropdownClass }`);\n\n      if (!actionsContainer || !actionsDropdown) {\n        return;\n      }\n\n      const actionsContainerWidth = actionsContainer.offsetWidth;\n      const actionsHTMLCollection = this.$refs.container.querySelectorAll(`.${ this.bulkActionClass }`);\n      const actions = Array.from(actionsHTMLCollection || []);\n\n      // Determine if the 'x selected' label should show and it's size\n      const selectedRowsText = this.$refs.container.querySelector(`.${ this.bulkActionAvailabilityClass }`);\n      let selectedRowsTextWidth = 0;\n\n      if (this.selectedRowsText) {\n        if (selectedRowsText) {\n          selectedRowsText.style.display = displayType;\n          selectedRowsTextWidth = selectedRowsText.offsetWidth;\n        } else {\n          selectedRowsText.style.display = 'none;';\n        }\n      }\n\n      this.hiddenActions = [];\n\n      let cumulativeWidth = 0;\n      let showActionsDropdown = false;\n      let totalAvailableWidth = actionsContainerWidth - selectedRowsTextWidth;\n\n      // Loop through all actions to determine if some exceed the available space in the row, if so hide them and instead show in a dropdown\n      for (let i = 0; i < actions.length; i++) {\n        const ba = actions[i];\n\n        ba.style.display = displayType;\n        const actionWidth = ba.offsetWidth;\n\n        cumulativeWidth += actionWidth + 15;\n        if (cumulativeWidth >= totalAvailableWidth) {\n          // There are too many actions so the drop down will be visible.\n          if (!showActionsDropdown) {\n            // If we haven't previously enabled the drop down...\n            actionsDropdown.style.display = displayType;\n            // By showing the drop down some previously visible actions may now be hidden, so start the process again\n            // ... except taking into account the width of drop down width in the available space\n            i = -1;\n            cumulativeWidth = 0;\n            showActionsDropdown = true;\n            totalAvailableWidth = actionsContainerWidth - actionsDropdown.offsetWidth - selectedRowsTextWidth;\n          } else {\n            // Collate the actions in an array and hide in the normal row\n            const id = ba.attributes.getNamedItem('id').value;\n\n            this.hiddenActions.push(this.availableActions.find((aa) => aa.action === id));\n            ba.style.display = 'none';\n          }\n        }\n      }\n\n      if (!showActionsDropdown) {\n        actionsDropdown.style.display = 'none';\n      }\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/advanced-filtering.js",
    "content": "import { ADV_FILTER_ALL_COLS_VALUE, ADV_FILTER_ALL_COLS_LABEL } from './filtering';\n\nconst DEFAULT_ADV_FILTER_COLS_VALUE = ADV_FILTER_ALL_COLS_VALUE;\n\nexport default {\n  props: {\n    /**\n     * Group value\n     * To be used on the THead component when adv filtering is present\n     */\n    group: {\n      type:    String,\n      default: () => '',\n    },\n    /**\n     * Group options\n     * All of the grouping options available to be used on the THead component when adv filtering is present\n     */\n    groupOptions: {\n      type:    Array,\n      default: () => [],\n    },\n    /**\n     * Flag that controls visibility of advanced filtering feature\n     */\n    hasAdvancedFiltering: {\n      type:    Boolean,\n      default: false,\n    },\n    /**\n     * Flag that controls visibility of labels as possible toggleable cols to be displayed on the Sortable Table\n     */\n    advFilterHideLabelsAsCols: {\n      type:    Boolean,\n      default: false,\n    },\n    /**\n     * Flag that prevents filtering by labels\n     */\n    advFilterPreventFilteringLabels: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  data() {\n    return {\n      columnOptions:               [],\n      colOptionsWatcher:           null,\n      advancedFilteringVisibility: false,\n      advancedFilteringValues:     [],\n      advFilterSearchTerm:         null,\n      advFilterSelectedProp:       DEFAULT_ADV_FILTER_COLS_VALUE,\n      advFilterSelectedLabel:      ADV_FILTER_ALL_COLS_LABEL,\n      column:                      null,\n    };\n  },\n\n  mounted() {\n    if (this.hasAdvancedFiltering) {\n      // trigger to first populate the cols options for filters\n      this.updateColsOptions();\n    }\n  },\n\n  watch: {\n    advancedFilteringValues() {\n      // passing different dummy args to make sure update is triggered\n      this.watcherUpdateLiveAndDelayed(true, false);\n    },\n    advancedFilteringVisibility(neu) {\n      if (neu) {\n        // check if user clicked outside the advanced filter box\n        window.addEventListener('click', this.onClickOutside);\n\n        // update filtering options and toggleable cols every time dropdown is open\n        this.updateColsOptions();\n      } else {\n        // unregister click event\n        window.removeEventListener('click', this.onClickOutside);\n      }\n    },\n  },\n\n  computed: {\n    advFilterSelectOptions() {\n      return this.columnOptions.filter((c) => c.isFilter && !c.preventFiltering);\n    },\n\n    advGroupOptions() {\n      return this.groupOptions.map((item) => {\n        return {\n          label: this.t(item.tooltipKey),\n          value: item.value,\n        };\n      });\n    },\n  },\n\n  methods: {\n    handleColsVisibilityAndFiltering(cols) {\n      const allCols = cols;\n\n      this.columnOptions.forEach((advCol) => {\n        if (advCol.isTableOption) {\n          const index = allCols.findIndex((col) => col.name === advCol.name);\n\n          if (index !== -1) {\n            allCols[index].isColVisible = advCol.isColVisible;\n            allCols[index].isFilter = advCol.isFilter;\n          } else {\n            allCols.push(advCol);\n          }\n        }\n      });\n\n      return allCols;\n    },\n    // advanced filtering methods\n    setColsOptions() {\n      let opts = [];\n      const rowLabels = [];\n      const headerProps = [];\n\n      // Filter out any columns that are too heavy to show for large page sizes\n      const filteredHeaders = this.headers.slice().filter((c) => (!c.maxPageSize || (c.maxPageSize && c.maxPageSize >= this.perPage)));\n\n      // add table cols from config (headers)\n      filteredHeaders.forEach((prop) => {\n        const name = prop.name;\n        const label = prop.labelKey ? this.t(`${ prop.labelKey }`) : prop.label;\n        const isFilter = !!((!Object.keys(prop).includes('search') || prop.search));\n        let sortVal = prop.sort;\n        const valueProp = prop.valueProp || prop.value;\n        let value = null;\n        let isColVisible = true;\n\n        if (prop.sort && valueProp) {\n          if (typeof prop.sort === 'string') {\n            sortVal = prop.sort.includes(':') ? [prop.sort.split(':')[0]] : [prop.sort];\n          }\n\n          if (!sortVal.includes(valueProp)) {\n            value = JSON.stringify(sortVal.concat([valueProp]));\n          } else {\n            value = JSON.stringify([valueProp]);\n          }\n        } else if (valueProp) {\n          value = JSON.stringify([valueProp]);\n        } else {\n          value = null;\n        }\n\n        // maintain current visibility of cols if they exist already\n        if (this.columnOptions?.length) {\n          const opt = this.columnOptions.find((colOpt) => colOpt.name === name && colOpt.label === label);\n\n          if (opt) {\n            isColVisible = opt.isColVisible;\n          }\n        }\n\n        headerProps.push({\n          name,\n          label,\n          value,\n          isFilter,\n          isTableOption: true,\n          isColVisible,\n        });\n      });\n\n      // add labels as table cols\n      if (this.rows.length) {\n        this.rows.forEach((row) => {\n          if (row.metadata?.labels && Object.keys(row.metadata?.labels).length) {\n            Object.keys(row.metadata?.labels).forEach((label) => {\n              const res = {\n                name:             label,\n                label,\n                value:            `metadata.labels.${ label }`,\n                isFilter:         true,\n                isTableOption:    true,\n                isColVisible:     false,\n                isLabel:          true,\n                preventFiltering: this.advFilterPreventFilteringLabels,\n                preventColToggle: this.advFilterHideLabelsAsCols,\n              };\n\n              // maintain current visibility of cols if they exist already\n              if (this.columnOptions?.length) {\n                const opt = this.columnOptions.find((colOpt) => colOpt.name === label && colOpt.label === label);\n\n                if (opt) {\n                  res.isColVisible = opt.isColVisible;\n                }\n              }\n\n              if (!rowLabels.filter((row) => row.label === label).length) {\n                rowLabels.push(res);\n              }\n            });\n          }\n        });\n      }\n\n      opts = headerProps.concat(rowLabels);\n\n      // add find on all cols option...\n      if (opts.length) {\n        opts.unshift({\n          name:          ADV_FILTER_ALL_COLS_LABEL,\n          label:         ADV_FILTER_ALL_COLS_LABEL,\n          value:         ADV_FILTER_ALL_COLS_VALUE,\n          isFilter:      true,\n          isTableOption: false,\n        });\n      }\n\n      return opts;\n    },\n    addAdvancedFilter() {\n      // set new advanced filter\n      if (this.advFilterSelectedProp && this.advFilterSearchTerm) {\n        this.advancedFilteringValues.push({\n          prop:  this.advFilterSelectedProp,\n          value: this.advFilterSearchTerm,\n          label: this.advFilterSelectedLabel,\n        });\n\n        this.eventualSearchQuery = this.advancedFilteringValues;\n\n        this.advancedFilteringVisibility = false;\n        this.advFilterSelectedProp = DEFAULT_ADV_FILTER_COLS_VALUE;\n        this.advFilterSelectedLabel = ADV_FILTER_ALL_COLS_LABEL;\n        this.advFilterSearchTerm = null;\n      }\n    },\n    clearAllAdvancedFilters() {\n      this.advancedFilteringValues = [];\n      this.eventualSearchQuery = this.advancedFilteringValues;\n\n      this.advancedFilteringVisibility = false;\n      this.advFilterSelectedProp = DEFAULT_ADV_FILTER_COLS_VALUE;\n      this.advFilterSelectedLabel = ADV_FILTER_ALL_COLS_LABEL;\n      this.advFilterSearchTerm = null;\n    },\n    clearAdvancedFilter(index) {\n      this.advancedFilteringValues.splice(index, 1);\n      this.eventualSearchQuery = this.advancedFilteringValues;\n    },\n    onClickOutside(event) {\n      const advFilterBox = this.$refs['advanced-filter-group'];\n\n      if (!advFilterBox || advFilterBox.contains(event.target)) {\n        return;\n      }\n      this.advancedFilteringVisibility = false;\n    },\n    updateColsOptions() {\n      this.columnOptions = this.setColsOptions();\n    },\n\n    // cols visibility\n    changeColVisibility(colData) {\n      const index = this.columnOptions.findIndex((col) => col.label === colData.label);\n\n      if (index !== -1) {\n        this.columnOptions[index].isColVisible = colData.value;\n      }\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/debug.js",
    "content": "// =========================================================================================\n// Debug helper\n// Adds a bunch of watches to help diagnose computed properties bring re-evaluated\n// =========================================================================================\n\nexport default {\n  watch: {\n    sortFields(neu, old) {\n      console.log('sortFields changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    descending(neu, old) {\n      console.log('descending changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    rows(neu, old) {\n      console.log('rows changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n\n      // console.log('Checking rows');\n\n      // let diff = 0;\n\n      // for (let i=0;i<neu.length;i++) {\n      //   const a = JSON.stringify(neu[i]);\n      //   const b = JSON.stringify(old[i]);\n\n      //   if (a !== b) {\n      //     console.log('rows differ ' + i);\n      //     diff++;\n      //   }\n      // }\n\n      // console.log(diff + ' rows changed');\n    },\n\n    pagingDisplay(neu, old) {\n      console.log('pagingDisplay changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    totalPages(neu, old) {\n      console.log('totalPages changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    pagedRows(neu, old) {\n      console.log('pagedRows changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    arrangedRows(neu, old) {\n      console.log('arrangedRows changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    searchFields(neu, old) {\n      console.log('searchFields changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    filteredRows(neu, old) {\n      console.log('filteredRows changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    groupedRows(neu, old) {\n      console.log('groupedRows changed ------------------------------------------------');\n      console.log(neu.length);\n      console.log(old.length);\n    },\n\n    headers(neu, old) {\n      console.log('headers changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    displayRows(neu, old) {\n      console.log('displayRows changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    groupBy(neu, old) {\n      console.log('groupBy changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    groupSort(neu, old) {\n      console.log('groupSort changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n\n    columns(neu, old) {\n      console.log('columns changed ------------------------------------------------');\n      console.log(neu);\n      console.log(old);\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/filtering.js",
    "content": "import { addObject, addObjects, isArray, removeAt } from '@pkg/utils/array';\nimport { get } from '@pkg/utils/object';\n\nexport const ADV_FILTER_ALL_COLS_VALUE = 'allcols';\nexport const ADV_FILTER_ALL_COLS_LABEL = 'All Columns';\nconst LABEL_IDENTIFIER = ':::islabel';\n\nexport default {\n  data() {\n    return {\n      searchQuery:    null,\n      previousFilter: null,\n      previousResult: null,\n    };\n  },\n\n  computed: {\n    searchFields() {\n      const out = columnsToSearchField(this.columns);\n\n      if ( this.extraSearchFields ) {\n        addObjects(out, this.extraSearchFields);\n      }\n\n      return out;\n    },\n\n    /*\n    subFields: computed('subHeaders.@each.{searchField,name}', 'extraSearchSubFields.[]', function() {\n      let out = headersToSearchField(get(this, 'subHeaders'));\n\n      return out.addObjects(get(this, 'extraSearchSubFields') || []);\n    }),\n    */\n    filteredRows() {\n      if (this.externalPaginationEnabled) {\n        return;\n      }\n\n      // PROP hasAdvancedFiltering comes from Advanced Filtering mixin (careful changing data var there...)\n      if (!this.hasAdvancedFiltering) {\n        return this.handleFiltering();\n      } else {\n        return this.handleAdvancedFiltering();\n      }\n    },\n  },\n\n  methods: {\n    handleAdvancedFiltering() {\n      this.subMatches = null;\n\n      if (this.searchQuery.length) {\n        const out = (this.arrangedRows || []).slice();\n\n        const res = out.filter((row) => {\n          return this.searchQuery.every((f) => {\n            if (f.prop === ADV_FILTER_ALL_COLS_VALUE) {\n              // advFilterSelectOptions comes from Advanced Filtering mixin\n              // remove the All Columns option from the list so that we don't iterate over it\n              const allCols = this.advFilterSelectOptions.slice(1);\n              let searchFields = [];\n\n              allCols.forEach((col) => {\n                if (col.value.includes('[') && col.value.includes(']')) {\n                  searchFields = searchFields.concat(JSON.parse(col.value));\n                } else {\n                  // this means we are on the presence of a label, which should be dealt\n                // carefully because of object path such row.metadata.labels.\"app.kubernetes.io/managed-by\n                  const value = col.isLabel ? `${ col.label }${ LABEL_IDENTIFIER }` : col.value;\n\n                  searchFields.push(value);\n                }\n              });\n\n              return handleStringSearch(searchFields, [f.value], row);\n            } else {\n              if (f.prop.includes('[') && f.prop.includes(']')) {\n                return handleStringSearch(JSON.parse(f.prop), [f.value], row);\n              }\n\n              let prop = f.prop;\n\n              // this means we are on the presence of a label, which should be dealt\n              // carefully because of object path such row.metadata.labels.\"app.kubernetes.io/managed-by\"\n              if (f.prop.includes('metadata.labels')) {\n                prop = `${ f.label }${ LABEL_IDENTIFIER }`;\n              }\n\n              return handleStringSearch([prop], [f.value], row);\n            }\n          });\n        });\n\n        return res;\n      }\n\n      // return arrangedRows array if we don't have anything to search for...\n      return this.arrangedRows;\n    },\n\n    handleFiltering() {\n      const searchText = (this.searchQuery || '').trim().toLowerCase();\n      let out;\n\n      if ( searchText === this.previousFilter && this.previousResult ) {\n        // If the search hasn't changed at all, just return the previous results\n        // since otherwise we get into a loop due to Vue proxying everything.\n        return this.previousResult;\n      }\n\n      if ( searchText && this.previousResult && searchText.startsWith(this.previousFilter) ) {\n        // If the new search is an addition to the last one, we can start with the same set of results as last time\n        // and filter those down, since adding more searchText can only reduce the number of results.\n        out = this.previousResult.slice();\n      } else {\n        this.previousResult = null;\n        out = (this.arrangedRows || []).slice();\n      }\n\n      this.previousFilter = searchText;\n\n      if ( !searchText.length ) {\n        this.subMatches = null;\n        this.previousResult = null;\n\n        return out;\n      }\n\n      const searchFields = this.searchFields;\n      const searchTokens = searchText.split(/\\s*[, ]\\s*/);\n      const subSearch = this.subSearch;\n      const subFields = this.subFields;\n      const subMatches = {};\n\n      for ( let i = out.length - 1; i >= 0; i-- ) {\n        const row = out[i];\n        let hits = 0;\n        let mainFound = true;\n\n        mainFound = handleStringSearch(searchFields, searchTokens, row);\n\n        if ( subFields && subSearch) {\n          const subRows = row[subSearch] || [];\n\n          for ( let k = subRows.length - 1; k >= 0; k-- ) {\n            let subFound = true;\n\n            subFound = handleStringSearch(subFields, searchTokens, row);\n\n            if ( subFound ) {\n              hits++;\n            }\n          }\n\n          subMatches[get(row, this.keyField)] = hits;\n        }\n\n        if ( !mainFound && hits === 0 ) {\n          removeAt(out, i);\n        }\n      }\n\n      this.subMatches = subMatches;\n      this.previousResult = out;\n\n      return out;\n    },\n  },\n\n  watch: {\n    arrangedRows(q) {\n      // The rows changed so the old filter result is no longer useful\n      this.previousResult = null;\n    },\n\n    searchQuery() {\n      this.debouncedPaginationChanged();\n    },\n  },\n};\n\nfunction columnsToSearchField(columns) {\n  const out = [];\n\n  (columns || []).forEach((column) => {\n    const field = column.search;\n\n    if ( field ) {\n      if ( typeof field === 'string' ) {\n        addObject(out, field);\n      } else if ( isArray(field) ) {\n        addObjects(out, field);\n      }\n    } else if ( field === false ) {\n      // Don't add the name\n    } else {\n      // Use value/name as the default\n      addObject(out, column.value || column.name);\n    }\n  });\n\n  return out.filter((x) => !!x);\n}\n\nconst ipLike = /^[0-9a-f\\.:]+$/i;\n\nfunction handleStringSearch(searchFields, searchTokens, row) {\n  for ( let j = 0; j < searchTokens.length; j++ ) {\n    let expect = true;\n    let token = searchTokens[j];\n\n    if ( token.substr(0, 1) === '!' ) {\n      expect = false;\n      token = token.substr(1);\n    }\n\n    if ( token && matches(searchFields, token, row) !== expect ) {\n      return false;\n    }\n\n    return true;\n  }\n}\n\nfunction matches(fields, token, item) {\n  for ( let field of fields ) {\n    if ( !field ) {\n      continue;\n    }\n\n    // some items might not even have metadata.labels or metadata.labels.something... ignore those items. Nothing to filter by\n    if (typeof field !== 'function' &&\n    field.includes(LABEL_IDENTIFIER) &&\n    (!item.metadata.labels || !item.metadata.labels[field.replace(LABEL_IDENTIFIER, '')])) {\n      continue;\n    }\n\n    let modifier;\n    let val;\n\n    if (typeof field === 'function') {\n      val = field(item);\n    } else if (field.includes(LABEL_IDENTIFIER)) {\n      val = item.metadata.labels[field.replace(LABEL_IDENTIFIER, '')];\n    } else {\n      const idx = field.indexOf(':');\n\n      if ( idx > 0 ) {\n        modifier = field.substr(idx + 1);\n        field = field.substr(0, idx);\n      }\n\n      if ( field.includes('.') ) {\n        val = get(item, field);\n      } else {\n        val = item[field];\n      }\n    }\n\n    if ( val === undefined ) {\n      continue;\n    }\n\n    val = (`${ val }`).toLowerCase();\n    if ( !val ) {\n      continue;\n    }\n\n    if ( !modifier ) {\n      if ( val.includes((`${ token }`).toLowerCase()) ) {\n        return true;\n      }\n    } else if ( modifier === 'exact' ) {\n      if ( val === token ) {\n        return true;\n      }\n    } else if ( modifier === 'ip' ) {\n      const tokenMayBeIp = ipLike.test(token);\n\n      if ( tokenMayBeIp ) {\n        const re = new RegExp(`(?:^|\\\\.)${ token }(?:\\\\.|$)`);\n\n        if ( re.test(val) ) {\n          return true;\n        }\n      }\n    } else if ( modifier === 'prefix' ) {\n      if ( val.indexOf(token) === 0) {\n        return true;\n      }\n    }\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/grouping.js",
    "content": "import { get } from '@pkg/utils/object';\n\nexport default {\n  computed: {\n    /**\n     * The group config associated with the selected group\n     */\n    selectedGroupOption() {\n      return this.groupOptions?.find((go) => go.value === this.group);\n    },\n\n    groupedRows() {\n      const groupKey = this.groupBy;\n      const refKey = this.groupRef || this.selectedGroupOption?.groupLabelKey || groupKey;\n\n      if ( !groupKey) {\n        return [{\n          key:  'default',\n          ref:  'default',\n          rows: this.pagedRows,\n        }];\n      }\n\n      const out = [];\n      const map = {};\n\n      for ( const obj of this.pagedRows ) {\n        const key = get(obj, groupKey) || '';\n        const ref = get(obj, refKey);\n        let entry = map[key];\n\n        if ( entry ) {\n          entry.rows.push(obj);\n        } else {\n          entry = {\n            key,\n            ref,\n            rows: [obj],\n          };\n          map[key] = entry;\n          out.push(entry);\n        }\n      }\n\n      return out;\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/index.vue",
    "content": "<script>\nimport { Checkbox } from '@rancher/components';\nimport day from 'dayjs';\nimport debounce from 'lodash/debounce';\nimport isEmpty from 'lodash/isEmpty';\nimport throttle from 'lodash/throttle';\nimport { mapGetters } from 'vuex';\n\nimport THead from './THead';\nimport actions from './actions';\nimport AdvancedFiltering from './advanced-filtering';\nimport filtering from './filtering';\nimport grouping from './grouping';\nimport paging from './paging';\nimport selection from './selection';\nimport sorting from './sorting';\n\nimport ActionDropdown from '@pkg/components/ActionDropdown';\nimport AsyncButton, { ASYNC_BUTTON_STATES } from '@pkg/components/AsyncButton';\nimport { FORMATTERS } from '@pkg/components/SortableTable/sortable-config';\nimport LabeledSelect from '@pkg/components/form/LabeledSelect';\nimport { removeObject } from '@pkg/utils/array';\nimport { getParent } from '@pkg/utils/dom';\nimport { get, clone } from '@pkg/utils/object';\nimport { dasherize, ucFirst } from '@pkg/utils/string';\n\n// Uncomment for table performance debugging\n// import tableDebug from './debug';\n\n// @TODO:\n// Fixed header/scrolling\n\n// Data Flow:\n// rows prop\n// --> sorting.js arrangedRows\n// --> filtering.js handleFiltering()\n// --> filtering.js filteredRows\n// --> paging.js pageRows\n// --> grouping.js groupedRows\n// --> index.vue displayedRows\n\nexport default {\n  name:       'SortableTable',\n  components: {\n    THead, Checkbox, AsyncButton, ActionDropdown, LabeledSelect,\n  },\n  mixins: [\n    filtering,\n    sorting,\n    paging,\n    grouping,\n    selection,\n    actions,\n    AdvancedFiltering,\n    // For table performance debugging - uncomment and uncomment the corresponding import\n    // tableDebug,\n  ],\n\n  props: {\n    headers: {\n      // {\n      //    name:   Name for the column (goes in query param) and for defaultSortBy\n      //    label:  Displayed column header\n      //    sort:   string|array[string] Field name(s) to sort by, default: [name, keyField]\n      //              fields can be suffixed with ':desc' to flip the normal sort order\n      //    search: string|array[string] Field name(s) to search in, default: [name]\n      //    width:  number\n      // }\n      type:     Array,\n      required: true,\n    },\n    rows: {\n      // The array of objects to show\n      type:     Array,\n      required: true,\n    },\n    keyField: {\n      // Field that is unique for each row.\n      type:    String,\n      default: '_key',\n    },\n\n    loading: {\n      type:     Boolean,\n      required: false,\n    },\n\n    /**\n     * Alt Loading - True: Always show table rows and obscure them when `loading`. Intended for use with server-side pagination.\n     *\n     * Alt Loading - False: Hide the table rows when `loading`. Intended when all resources are provided up front.\n     */\n    altLoading: {\n      type:     Boolean,\n      required: false,\n    },\n\n    groupBy: {\n      // Field to group rows by, row[groupBy] must be something that can be a map key\n      type:    String,\n      default: null,\n    },\n    groupRef: {\n      // Object to provide as the reference for rendering the grouping row\n      type:    String,\n      default: null,\n    },\n    groupSort: {\n      // Field to order groups by, defaults to groupBy\n      type:    Array,\n      default: null,\n    },\n\n    defaultSortBy: {\n      // Default field to sort by if none is specified\n      // uses name on headers\n      type:    String,\n      default: null,\n    },\n\n    tableActions: {\n      // Show bulk table actions\n      type:    Boolean,\n      default: true,\n    },\n\n    rowActions: {\n      // Show action dropdown on the end of each row\n      type:    Boolean,\n      default: true,\n    },\n\n    mangleActionResources: {\n      type:    Function,\n      default: null,\n    },\n\n    rowActionsWidth: {\n      // How wide the action dropdown column should be\n      type:    Number,\n      default: 40,\n    },\n\n    search: {\n      // Show search input to filter rows\n      type:    Boolean,\n      default: true,\n    },\n\n    extraSearchFields: {\n      // Additional fields that aren't defined in the headers to search in on each row\n      type:    Array,\n      default: null,\n    },\n\n    subRows: {\n      // If there are sub-rows, your main row must have <tr class=\"main-row\"> to identify it\n      type:    Boolean,\n      default: false,\n    },\n\n    subRowsDescription: {\n      type:    Boolean,\n      default: true,\n    },\n\n    subExpandable: {\n      type:    Boolean,\n      default: false,\n    },\n\n    subExpandColumn: {\n      type:    Boolean,\n      default: false,\n    },\n\n    subSearch: {\n      // A field containing an array of sub-items to also search in for each row\n      type:    String,\n      default: null,\n    },\n\n    subFields: {\n      // Search this list of fields within the items in \"subSearch\" of each row\n      type:    Array,\n      default: null,\n    },\n\n    /**\n     * Show the divider between the thead and tbody.\n     */\n    topDivider: {\n      type:    Boolean,\n      default: true,\n    },\n\n    /**\n     * Show the dividers between rows\n     */\n    bodyDividers: {\n      type:    Boolean,\n      default: false,\n    },\n\n    overflowX: {\n      type:    Boolean,\n      default: false,\n    },\n    overflowY: {\n      type:    Boolean,\n      default: false,\n    },\n\n    /**\n     * If pagination of the data is enabled or not\n     */\n    paging: {\n      type:    Boolean,\n      default: false,\n    },\n\n    /**\n     * What translation key to use for displaying the '1 - 10 of 100 Things' pagination info\n     */\n    pagingLabel: {\n      type:    String,\n      default: 'sortableTable.paging.generic',\n    },\n\n    /**\n     * Additional params to pass to the pagingLabel translation\n     */\n    pagingParams: {\n      type:    Object,\n      default: null,\n    },\n\n    /**\n     * Allows you to override the default preference of the number of\n     * items to display per page. This is used by ./paging.js if you're\n     * looking for a reference.\n     */\n    rowsPerPage: {\n      type:    Number,\n      default: null, // Default comes from the user preference\n    },\n\n    /**\n     * Allows you to override the default translation text of no rows view\n     */\n    noRowsKey: {\n      type:    String,\n      default: 'sortableTable.noRows',\n    },\n\n    /**\n     * Allows you to hide the no rows messaging.\n     */\n    showNoRows: {\n      type:    Boolean,\n      default: true,\n    },\n\n    /**\n     * Allows you to override the default translation text of no search data view\n     */\n    noDataKey: {\n      type:    String,\n      default: 'sortableTable.noData', // i18n-uses sortableTable.noData\n    },\n\n    /**\n     * Allows you to override showing the THEAD section.\n     */\n    showHeaders: {\n      type:    Boolean,\n      default: true,\n    },\n\n    sortGenerationFn: {\n      type:    Function,\n      default: null,\n    },\n\n    /**\n     * The list will always be sorted by these regardless of what the user has selected\n     */\n    mandatorySort: {\n      type:    Array,\n      default: null,\n    },\n\n    /**\n     * Allows you to link to a custom detail page for data that\n     * doesn't have a class model. For example, a receiver configuration\n     * block within an AlertmanagerConfig resource.\n     */\n    getCustomDetailLink: {\n      type:    Function,\n      default: null,\n    },\n\n    /**\n     * Inherited global identifier prefix for tests\n     * Define a term based on the parent component to avoid conflicts on multiple components\n     */\n    componentTestid: {\n      type:    String,\n      default: 'sortable-table',\n    },\n    /**\n     * Allows for the usage of a query param to work for simple filtering (q)\n     */\n    useQueryParamsForSimpleFiltering: {\n      type:    Boolean,\n      default: false,\n    },\n    /**\n     * Manually force the update of live and delayed cells. Change this number to kick off the update\n     */\n    forceUpdateLiveAndDelayed: {\n      type:    Number,\n      default: 0,\n    },\n\n    /**\n     * True if pagination is executed outside of the component\n     */\n    externalPaginationEnabled: {\n      type:    Boolean,\n      default: false,\n    },\n\n    /**\n     * If `externalPaginationEnabled` is true this will be used as the current page\n     */\n    externalPaginationResult: {\n      type:    Object,\n      default: null,\n    },\n  },\n\n  data() {\n    let searchQuery = '';\n    let eventualSearchQuery = '';\n\n    // only allow for filter query param for simple filtering for now...\n    if (!this.hasAdvancedFiltering && this.useQueryParamsForSimpleFiltering && this.$route.query?.q) {\n      searchQuery = this.$route.query?.q;\n      eventualSearchQuery = this.$route.query?.q;\n    }\n\n    return {\n      refreshButtonPhase:         ASYNC_BUTTON_STATES.WAITING,\n      expanded:                   {},\n      searchQuery,\n      eventualSearchQuery,\n      subMatches:                 null,\n      actionOfInterest:           null,\n      loadingDelay:               false,\n      debouncedPaginationChanged: null,\n      /**\n       * This bool controls showing loading state in the DOM; it's proxied from `loading` to avoid blipping the indicator (see usages)\n       */\n      isLoading:                  false,\n    };\n  },\n\n  mounted() {\n    this._loadingDelayTimer = setTimeout(() => {\n      this.loadingDelay = true;\n    }, 200);\n\n    // Add scroll listener to the main element\n    const $main = document.querySelector('main');\n\n    this._onScroll = this.onScroll.bind(this);\n    $main?.addEventListener('scroll', this._onScroll);\n\n    this.debouncedPaginationChanged();\n  },\n\n  beforeUnmount() {\n    clearTimeout(this._scrollTimer);\n    clearTimeout(this._loadingDelayTimer);\n    clearTimeout(this._altLoadingDelayTimer);\n    clearTimeout(this._liveColumnsTimer);\n    clearTimeout(this._delayedColumnsTimer);\n    clearTimeout(this.manualRefreshTimer);\n\n    const $main = document.querySelector('main');\n\n    $main?.removeEventListener('scroll', this._onScroll);\n  },\n\n  watch: {\n    eventualSearchQuery: debounce(function(q) {\n      this.searchQuery = q;\n\n      if (!this.hasAdvancedFiltering && this.useQueryParamsForSimpleFiltering) {\n        const route = {\n          name:   this.$route.name,\n          params: { ...this.$route.params },\n          query:  { ...this.$route.query, q },\n        };\n\n        if (!q && this.$route.query?.q) {\n          route.query = {};\n        }\n\n        this.$router.replace(route);\n      }\n    }, 200),\n\n    descending(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    searchQuery(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    sortFields(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    groupBy(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    namespaces(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    page(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    forceUpdateLiveAndDelayed(neu, old) {\n      this.watcherUpdateLiveAndDelayed(neu, old);\n    },\n\n    // Ensure we update live and delayed columns on first load\n    initialLoad: {\n      handler(neu) {\n        if (neu) {\n          this._didinit = true;\n          this.$nextTick(() => this.updateLiveAndDelayed());\n        }\n      },\n      immediate: true,\n    },\n\n    // this is the flag that indicates that manual refresh data has been loaded\n    // and we should update the deferred cols\n    manualRefreshLoadingFinished: {\n      handler(neu, old) {\n        // this is merely to update the manual refresh button status\n        this.refreshButtonPhase = !neu ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION;\n        if (neu && neu !== old) {\n          this.$nextTick(() => this.updateLiveAndDelayed());\n        }\n      },\n      immediate: true,\n    },\n\n    loading: {\n      handler(neu, old) {\n        // Always ensure the Refresh button phase aligns with loading state (to ensure external phase changes which can then reset the internal phase changed by click)\n        this.refreshButtonPhase = neu ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION;\n\n        if (this.altLoading) {\n          // Delay setting the actual loading indicator. This should avoid flashing up the indicator if the API responds quickly\n          if (neu) {\n            this._altLoadingDelayTimer = setTimeout(() => {\n              this.isLoading = true;\n            }, 200); // this should be above the targeted quick response\n          } else {\n            clearTimeout(this._altLoadingDelayTimer);\n            this.isLoading = false;\n          }\n        } else {\n          this.isLoading = neu;\n        }\n      },\n      immediate: true,\n    },\n  },\n\n  created() {\n    this.debouncedRefreshTableData = debounce(this.refreshTableData, 500);\n    this.debouncedPaginationChanged = debounce(this.paginationChanged, 50);\n  },\n\n  computed: {\n    ...mapGetters({ isTooManyItemsToAutoUpdate: 'resource-fetch/isTooManyItemsToAutoUpdate' }),\n    ...mapGetters({ isManualRefreshLoading: 'resource-fetch/manualRefreshIsLoading' }),\n    namespaces() {\n      return this.$store.getters['activeNamespaceCache'];\n    },\n\n    initialLoad() {\n      return !!(!this.isLoading && !this._didinit && this.rows?.length);\n    },\n\n    manualRefreshLoadingFinished() {\n      const res = !!(!this.isLoading && this._didinit && this.rows?.length && !this.isManualRefreshLoading);\n\n      // Always ensure the Refresh button phase aligns with loading state (regardless of if manualRefreshLoadingFinished has changed or not)\n      this.refreshButtonPhase = !res || this.loading ? ASYNC_BUTTON_STATES.WAITING : ASYNC_BUTTON_STATES.ACTION;\n\n      return res;\n    },\n\n    fullColspan() {\n      let span = 0;\n\n      for ( let i = 0; i < this.columns.length; i++ ) {\n        if (!this.columns[i].hide) {\n          span++;\n        }\n      }\n\n      if ( this.tableActions ) {\n        span++;\n      }\n\n      if ( this.subExpandColumn ) {\n        span++;\n      }\n\n      if ( this.rowActions ) {\n        span++;\n      }\n\n      return span;\n    },\n\n    noResults() {\n      return !!this.searchQuery && this.pagedRows.length === 0;\n    },\n\n    noRows() {\n      return !this.noResults && (this.rows || []).length === 0;\n    },\n\n    showHeaderRow() {\n      return this.search ||\n        this.tableActions ||\n        this.$slots['header-left']?.() ||\n        this.$slots['header-middle']?.() ||\n        this.$slots['header-right']?.();\n    },\n\n    columns() {\n      // Filter out any columns that are too heavy to show for large page sizes\n      const out = this.headers.slice().filter((c) => !c.maxPageSize || (c.maxPageSize && c.maxPageSize >= this.perPage));\n\n      if ( this.groupBy ) {\n        const entry = out.find((x) => x.name === this.groupBy);\n\n        if ( entry ) {\n          removeObject(out, entry);\n        }\n      }\n\n      // If all columns have a width, try to remove it from a column that can be variable (name)\n      const missingWidth = out.find((x) => !x.width);\n\n      if ( !missingWidth ) {\n        const variable = out.find((x) => x.canBeVariable);\n\n        if ( variable ) {\n          const neu = clone(variable);\n\n          delete neu.width;\n\n          out.splice(out.indexOf(variable), 1, neu);\n        }\n      }\n\n      // handle cols visibility and filtering if there is advanced filtering\n      if (this.hasAdvancedFiltering) {\n        const cols = this.handleColsVisibilityAndFiltering(out);\n\n        return cols;\n      }\n\n      return out;\n    },\n\n    // For data-title properties on <td>s\n    dt() {\n      const out = {\n        check:   `Select: `,\n        actions: `Actions: `,\n      };\n\n      this.columns.forEach((col) => {\n        out[col.name] = `${ (col.label || col.name) }:`;\n      });\n\n      return out;\n    },\n\n    classObject() {\n      return {\n        'top-divider':   this.topDivider,\n        'body-dividers': this.bodyDividers,\n        'overflow-y':    this.overflowY,\n        'overflow-x':    this.overflowX,\n        'alt-loading':   this.altLoading && this.isLoading,\n      };\n    },\n\n    // Do we have any live columns?\n    hasLiveColumns() {\n      const liveColumns = this.columns.find((c) => c.formatter?.startsWith('Live') || c.liveUpdates);\n\n      return !!liveColumns;\n    },\n\n    hasDelayedColumns() {\n      const delayedColumns = this.columns.find((c) => c.delayLoading);\n\n      return !!delayedColumns;\n    },\n\n    columnFormatterIDs() {\n      const columnsIds = {};\n\n      this.columns.forEach((c) => {\n        if (c.formatter) {\n          columnsIds[c.formatter] = dasherize(c.formatter);\n        }\n      });\n\n      return columnsIds;\n    },\n\n    // Generate row and column data for easier rendering in the template\n    // ensures we only call methods like `valueFor` once\n    displayRows() {\n      const rows = [];\n      const columnFormatterIDs = this.columnFormatterIDs;\n\n      this.groupedRows.forEach((grp) => {\n        const group = {\n          grp,\n          key:  grp.key,\n          ref:  grp.ref,\n          rows: [],\n        };\n\n        rows.push(group);\n\n        grp.rows.forEach((row) => {\n          const rowData = {\n            row,\n            key:                        this.get(row, this.keyField),\n            showSubRow:                 this.showSubRow(row, this.keyField),\n            canRunBulkActionOfInterest: this.canRunBulkActionOfInterest(row),\n            columns:                    [],\n          };\n\n          group.rows.push(rowData);\n\n          this.columns.forEach((c) => {\n            const value = c.delayLoading ? undefined : this.valueFor(row, c, c.isLabel);\n            let component;\n            let formatted = value;\n            let needRef = false;\n\n            if (Array.isArray(value)) {\n              formatted = value.join(', ');\n            }\n\n            if (c.formatter) {\n              if (FORMATTERS[c.formatter]) {\n                component = FORMATTERS[c.formatter];\n                needRef = true;\n              } else {\n                // Check if we have a formatter from a plugin\n                const pluginFormatter = this.$plugin?.getDynamic('formatters', c.formatter);\n\n                if (pluginFormatter) {\n                  component = pluginFormatter;\n                  needRef = true;\n                }\n              }\n            }\n\n            rowData.columns.push({\n              col:       c,\n              value,\n              formatted,\n              component,\n              needRef,\n              delayed:   c.delayLoading,\n              live:      c.formatter?.startsWith('Live') || c.liveUpdates,\n              label:     this.labelFor(c),\n              dasherize: columnFormatterIDs[c.formatter] || '',\n            });\n          });\n        });\n      });\n\n      return rows;\n    },\n  },\n\n  methods: {\n    refreshTableData() {\n      this.$store.dispatch('resource-fetch/doManualRefresh');\n    },\n    get,\n    dasherize,\n\n    onScroll() {\n      if (this.hasLiveColumns || this.hasDelayedColumns) {\n        clearTimeout(this._liveColumnsTimer);\n        clearTimeout(this._scrollTimer);\n        clearTimeout(this._delayedColumnsTimer);\n        this._scrollTimer = setTimeout(() => {\n          this.updateLiveColumns();\n          this.updateDelayedColumns();\n        }, 300);\n      }\n    },\n\n    watcherUpdateLiveAndDelayed(neu, old) {\n      if (neu !== old) {\n        this.$nextTick(() => this.updateLiveAndDelayed());\n      }\n    },\n\n    updateLiveAndDelayed() {\n      if (this.hasLiveColumns) {\n        this.updateLiveColumns();\n      }\n\n      if (this.hasDelayedColumns) {\n        this.updateDelayedColumns();\n      }\n    },\n\n    updateDelayedColumns() {\n      clearTimeout(this._delayedColumnsTimer);\n\n      if (!this.$refs.column || this.pagedRows.length === 0) {\n        return;\n      }\n\n      const delayedColumns = this.$refs.column.filter((c) => c.startDelayedLoading && !c.__delayedLoading);\n      // We add 100 pixels here - so we will render the delayed columns for a few extra rows below what is visible\n      // This way if you scroll slowly, you won't see the columns being loaded\n      const clientHeight = (window.innerHeight || document.documentElement.clientHeight) + 100;\n\n      let scheduled = 0;\n\n      for (let i = 0; i < delayedColumns.length; i++) {\n        const dc = delayedColumns[i];\n        const y = dc.$el.getBoundingClientRect().y;\n\n        if (y >= 0 && y <= clientHeight) {\n          dc.startDelayedLoading(true);\n          dc.__delayedLoading = true;\n\n          scheduled++;\n\n          // Only update 4 at a time\n          if (scheduled === 4) {\n            this._delayedColumnsTimer = setTimeout(this.updateDelayedColumns, 100);\n\n            return;\n          }\n        }\n      }\n    },\n\n    updateLiveColumns() {\n      clearTimeout(this._liveColumnsTimer);\n\n      if (!this.$refs.column || !this.hasLiveColumns || this.pagedRows.length === 0) {\n        return;\n      }\n\n      const clientHeight = window.innerHeight || document.documentElement.clientHeight;\n      const liveColumns = this.$refs.column.filter((c) => !!c.liveUpdate);\n      const now = day();\n      let next = Number.MAX_SAFE_INTEGER;\n\n      for (let i = 0; i < liveColumns.length; i++) {\n        const column = liveColumns[i];\n        const y = column.$el.getBoundingClientRect().y;\n\n        if (y >= 0 && y <= clientHeight) {\n          const diff = column.liveUpdate(now);\n\n          if (diff < next) {\n            next = diff;\n          }\n        }\n      }\n\n      if (next < 1 ) {\n        next = 1;\n      }\n\n      // Schedule again\n      this._liveColumnsTimer = setTimeout(() => this.updateLiveColumns(), next * 1000);\n    },\n\n    labelFor(col) {\n      if ( col.labelKey ) {\n        return this.t(col.labelKey, undefined, true);\n      } else if ( col.label ) {\n        return col.label;\n      }\n\n      return ucFirst(col.name);\n    },\n\n    valueFor(row, col, isLabel) {\n      if (typeof col.value === 'function') {\n        return col.value(row);\n      }\n\n      if (isLabel) {\n        if (row.metadata?.labels && row.metadata?.labels[col.label]) {\n          return row.metadata?.labels[col.label];\n        }\n\n        return '';\n      }\n\n      // Use to debug table columns using expensive value getters\n      // console.warn(`Performance: Table valueFor: ${ col.name } ${ col.value }`); // eslint-disable-line no-console\n\n      const expr = col.value || col.name;\n\n      if (!expr) {\n        console.error('No path has been defined for this column, unable to get value of cell', col);\n\n        return '';\n      }\n      const out = get(row, expr);\n\n      if ( out === null || out === undefined ) {\n        return '';\n      }\n\n      return out;\n    },\n\n    isExpanded(row) {\n      const key = row[this.keyField];\n\n      return !!this.expanded[key];\n    },\n\n    toggleExpand(row) {\n      const key = row[this.keyField];\n      const val = !this.expanded[key];\n\n      this.expanded[key] = val;\n      this.expanded = { ...this.expanded };\n\n      return val;\n    },\n\n    setBulkActionOfInterest(action) {\n      this.actionOfInterest = action;\n    },\n\n    // Can the action of interest be applied to the specified resource?\n    canRunBulkActionOfInterest(resource) {\n      if ( !this.actionOfInterest || isEmpty(resource?.availableActions) ) {\n        return false;\n      }\n\n      const matchingResourceAction = resource.availableActions?.find((a) => a.action === this.actionOfInterest.action);\n\n      return matchingResourceAction?.enabled;\n    },\n\n    focusSearch() {\n      if ( this.$refs.searchQuery ) {\n        this.$refs.searchQuery.focus();\n        this.$refs.searchQuery.select();\n      }\n    },\n\n    nearestCheckbox() {\n      return document.activeElement.closest('tr.main-row')?.querySelector('.checkbox-custom');\n    },\n\n    focusAdjacent(next = true) {\n      const all = Array.from(this.$el.querySelectorAll('.checkbox-custom'));\n\n      const cur = this.nearestCheckbox();\n      let idx = -1;\n\n      if ( cur ) {\n        idx = all.indexOf(cur) + (next ? 1 : -1 );\n      } else if ( next ) {\n        idx = 1;\n      } else {\n        idx = all.length - 1;\n      }\n\n      if ( idx < 1 ) { // Don't go up to the check all button\n        idx = 1;\n\n        return null;\n      }\n\n      if ( idx >= all.length ) {\n        idx = all.length - 1;\n\n        return null;\n      }\n\n      if ( all[idx] ) {\n        all[idx].focus();\n\n        return all[idx];\n      }\n    },\n\n    focusNext: throttle(function(event, more = false) {\n      const elem = this.focusAdjacent(true);\n      const row = getParent(elem, 'tr');\n\n      if (row?.classList.contains('row-selected')) {\n        return;\n      }\n\n      this.keySelectRow(row, more);\n    }, 50),\n\n    focusPrevious: throttle(function(event, more = false) {\n      const elem = this.focusAdjacent(false);\n      const row = getParent(elem, 'tr');\n\n      if (row?.classList.contains('row-selected')) {\n        return;\n      }\n\n      this.keySelectRow(row, more);\n    }, 50),\n\n    showSubRow(row, keyField) {\n      const hasInjectedSubRows = this.subRows && (!this.subExpandable || this.expanded[get(row, keyField)]);\n      const hasStateDescription = this.subRowsDescription && row.stateDescription;\n\n      return hasInjectedSubRows || hasStateDescription;\n    },\n\n    handleActionButtonClick(i, event) {\n      // Each row in the table gets its own ref with\n      // a number based on its index. If you are using\n      // an ActionMenu that doesn't have a dependency on Vuex,\n      // these refs are useful because you can reuse the\n      // same ActionMenu component on a page with many different\n      // target elements in a list,\n      // so you can still avoid the performance problems that\n      // could result if the ActionMenu was in every row. The menu\n      // will open on whichever target element is clicked.\n      this.$emit('clickedActionButton', {\n        event,\n        targetElement: this.$refs[`actionButton${ i }`][0],\n      });\n    },\n\n    paginationChanged() {\n      if (!this.externalPaginationEnabled) {\n        return;\n      }\n\n      this.$emit('pagination-changed', {\n        page:    this.page,\n        perPage: this.perPage,\n        filter:  {\n          searchFields: this.searchFields,\n          searchQuery:  this.searchQuery,\n        },\n        sort:       this.sortFields,\n        descending: this.descending,\n      });\n    },\n  },\n};\n</script>\n\n<template>\n  <div\n    ref=\"container\"\n    :data-testid=\"componentTestid + '-list-container'\"\n  >\n    <div\n      :class=\"{ titled: $slots.title && $slots.title.length }\"\n      class=\"sortable-table-header\"\n    >\n      <slot name=\"title\" />\n      <div\n        v-if=\"showHeaderRow\"\n        class=\"fixed-header-actions\"\n        :class=\"{ button: !!$slots['header-button'], 'advanced-filtering': hasAdvancedFiltering }\"\n      >\n        <div\n          :class=\"bulkActionsClass\"\n          class=\"bulk\"\n        >\n          <slot name=\"header-left\">\n            <template v-if=\"tableActions\">\n              <button\n                v-for=\"(act) in availableActions\"\n                :id=\"act.action\"\n                :key=\"act.action\"\n                v-clean-tooltip=\"actionTooltip\"\n                type=\"button\"\n                class=\"btn role-primary\"\n                :class=\"{ [bulkActionClass]: true }\"\n                :disabled=\"!act.enabled\"\n                :data-testid=\"componentTestid + '-' + act.action\"\n                @click=\"applyTableAction(act, null, $event)\"\n                @mouseover=\"setBulkActionOfInterest(act)\"\n                @mouseleave=\"setBulkActionOfInterest(null)\"\n              >\n                <i\n                  v-if=\"act.icon\"\n                  :class=\"act.icon\"\n                />\n                <span v-clean-html=\"act.label\" />\n              </button>\n              <ActionDropdown\n                :class=\"bulkActionsDropdownClass\"\n                class=\"bulk-actions-dropdown\"\n                :disable-button=\"!selectedRows.length\"\n                size=\"sm\"\n              >\n                <template #button-content>\n                  <button\n                    ref=\"actionDropDown\"\n                    class=\"btn bg-primary mr-0\"\n                    :disabled=\"!selectedRows.length\"\n                  >\n                    <i class=\"icon icon-gear\" />\n                    <span>{{ t('sortableTable.bulkActions.collapsed.label') }}</span>\n                    <i class=\"ml-10 icon icon-chevron-down\" />\n                  </button>\n                </template>\n                <template #popover-content>\n                  <ul class=\"list-unstyled menu\">\n                    <li\n                      v-for=\"(act, i) in hiddenActions\"\n                      :key=\"i\"\n                      v-close-popper\n                      v-clean-tooltip=\"{\n                        content: actionTooltip,\n                        placement: 'right',\n                      }\"\n                      :class=\"{ disabled: !act.enabled }\"\n                      @click=\"applyTableAction(act, null, $event)\"\n                      @mouseover=\"setBulkActionOfInterest(act)\"\n                      @mouseleave=\"setBulkActionOfInterest(null)\"\n                    >\n                      <i\n                        v-if=\"act.icon\"\n                        :class=\"act.icon\"\n                      />\n                      <span v-clean-html=\"act.label\" />\n                    </li>\n                  </ul>\n                </template>\n              </ActionDropdown>\n              <label\n                v-if=\"selectedRowsText\"\n                :class=\"bulkActionAvailabilityClass\"\n                class=\"action-availability\"\n              >\n                {{ selectedRowsText }}\n              </label>\n            </template>\n          </slot>\n        </div>\n        <div\n          v-if=\"!hasAdvancedFiltering && $slots['header-middle']\"\n          class=\"middle\"\n        >\n          <slot name=\"header-middle\" />\n        </div>\n\n        <div\n          v-if=\"search || hasAdvancedFiltering || isTooManyItemsToAutoUpdate || $slots['header-right']\"\n          class=\"search row\"\n          data-testid=\"search-box-filter-row\"\n        >\n          <ul\n            v-if=\"hasAdvancedFiltering\"\n            class=\"advanced-filters-applied\"\n          >\n            <li\n              v-for=\"(filter, i) in advancedFilteringValues\"\n              :key=\"i\"\n            >\n              <span class=\"label\">{{ `\"${filter.value}\" ${t('sortableTable.in')} ${filter.label}` }}</span>\n              <span\n                class=\"cross\"\n                @click=\"clearAdvancedFilter(i)\"\n              >&#10005;</span>\n              <div class=\"bg\" />\n            </li>\n          </ul>\n          <slot name=\"header-right\" />\n          <AsyncButton\n            v-if=\"isTooManyItemsToAutoUpdate\"\n            class=\"manual-refresh\"\n            mode=\"manual-refresh\"\n            :current-phase=\"refreshButtonPhase\"\n            @click=\"debouncedRefreshTableData\"\n          />\n          <div\n            v-if=\"hasAdvancedFiltering\"\n            ref=\"advanced-filter-group\"\n            class=\"advanced-filter-group\"\n          >\n            <button\n              class=\"btn role-primary\"\n              @click=\"advancedFilteringVisibility = !advancedFilteringVisibility;\"\n            >\n              {{ t('sortableTable.addFilter') }}\n            </button>\n            <div\n              v-show=\"advancedFilteringVisibility\"\n              class=\"advanced-filter-container\"\n            >\n              <input\n                ref=\"advancedSearchQuery\"\n                :value=\"advFilterSearchTerm\"\n                type=\"search\"\n                class=\"advanced-search-box\"\n                :placeholder=\"t('sortableTable.filterFor')\"\n                @input=\"($plainInputEvent) => advFilterSearchTerm = $plainInputEvent.target.value\"\n              >\n              <div class=\"middle-block\">\n                <span>{{ t('sortableTable.in') }}</span>\n                <LabeledSelect\n                  v-model:value=\"advFilterSelectedProp\"\n                  class=\"filter-select\"\n                  :clearable=\"true\"\n                  :options=\"advFilterSelectOptions\"\n                  :disabled=\"false\"\n                  :searchable=\"false\"\n                  mode=\"edit\"\n                  :multiple=\"false\"\n                  :taggable=\"false\"\n                  :placeholder=\"t('sortableTable.selectCol')\"\n                  @selecting=\"(col) => advFilterSelectedLabel = col.label\"\n                />\n              </div>\n              <div class=\"bottom-block\">\n                <button\n                  class=\"btn role-secondary\"\n                  :disabled=\"!advancedFilteringValues.length\"\n                  @click=\"clearAllAdvancedFilters\"\n                >\n                  {{ t('sortableTable.resetFilters') }}\n                </button>\n                <button\n                  class=\"btn role-primary\"\n                  @click=\"addAdvancedFilter\"\n                >\n                  {{ t('sortableTable.add') }}\n                </button>\n              </div>\n            </div>\n          </div>\n          <input\n            v-else-if=\"search\"\n            ref=\"searchQuery\"\n            :value=\"eventualSearchQuery\"\n            type=\"search\"\n            class=\"input-sm search-box\"\n            data-testid=\"search-input\"\n            :placeholder=\"t('sortableTable.search')\"\n            @input=\"($plainInputEvent) => eventualSearchQuery = $plainInputEvent.target.value\"\n          >\n          <slot name=\"header-button\" />\n        </div>\n      </div>\n    </div>\n    <table\n      class=\"sortable-table\"\n      :class=\"classObject\"\n      width=\"100%\"\n    >\n      <THead\n        v-if=\"showHeaders\"\n        :label-for=\"labelFor\"\n        :columns=\"columns\"\n        :group=\"group\"\n        :group-options=\"advGroupOptions\"\n        :has-advanced-filtering=\"hasAdvancedFiltering\"\n        :adv-filter-hide-labels-as-cols=\"advFilterHideLabelsAsCols\"\n        :table-actions=\"tableActions\"\n        :table-cols-options=\"columnOptions\"\n        :row-actions=\"rowActions\"\n        :sub-expand-column=\"subExpandColumn\"\n        :row-actions-width=\"rowActionsWidth\"\n        :how-much-selected=\"howMuchSelected\"\n        :sort-by=\"sortBy\"\n        :default-sort-by=\"_defaultSortBy\"\n        :descending=\"descending\"\n        :no-rows=\"noRows\"\n        :loading=\"isLoading && !loadingDelay\"\n        :no-results=\"noResults\"\n        @on-toggle-all=\"onToggleAll\"\n        @on-sort-change=\"changeSort\"\n        @col-visibility-change=\"changeColVisibility\"\n        @group-value-change=\"(val) => $emit('group-value-change', val)\"\n        @update-cols-options=\"updateColsOptions\"\n      />\n\n      <!-- Don't display anything if we're loading and the delay has yet to pass -->\n      <div v-if=\"isLoading && !loadingDelay\" />\n\n      <tbody v-else-if=\"isLoading && !altLoading\">\n        <slot name=\"loading\">\n          <tr>\n            <td :colspan=\"fullColspan\">\n              <div class=\"data-loading\">\n                <i class=\"icon-spin icon icon-spinner\" />\n                <t\n                  k=\"generic.loading\"\n                  :raw=\"true\"\n                />\n              </div>\n            </td>\n          </tr>\n        </slot>\n      </tbody>\n      <tbody v-else-if=\"noRows\">\n        <slot name=\"no-rows\">\n          <tr class=\"no-rows\">\n            <td :colspan=\"fullColspan\">\n              <t\n                v-if=\"showNoRows\"\n                :k=\"noRowsKey\"\n              />\n            </td>\n          </tr>\n        </slot>\n      </tbody>\n      <tbody v-else-if=\"noResults\">\n        <slot name=\"no-results\">\n          <tr class=\"no-results\">\n            <td\n              :colspan=\"fullColspan\"\n              class=\"text-center\"\n            >\n              <t :k=\"noDataKey\" />\n            </td>\n          </tr>\n        </slot>\n      </tbody>\n      <tbody\n        v-for=\"(groupedRows) in displayRows\"\n        v-else\n        :key=\"groupedRows.key\"\n        :class=\"{ group: groupBy }\"\n      >\n        <slot\n          v-if=\"groupBy\"\n          name=\"group-row\"\n          :group=\"groupedRows\"\n          :full-colspan=\"fullColspan\"\n        >\n          <tr class=\"group-row\">\n            <td :colspan=\"fullColspan\">\n              <slot\n                name=\"group-by\"\n                :group=\"groupedRows.grp\"\n              >\n                <div\n                  v-trim-whitespace\n                  class=\"group-tab\"\n                >\n                  {{ groupedRows.ref }}\n                </div>\n              </slot>\n            </td>\n          </tr>\n        </slot>\n        <template\n          v-for=\"(row, i) in groupedRows.rows\"\n          :key=\"row.row[keyField]\"\n        >\n          <slot\n            name=\"main-row\"\n            :row=\"row.row\"\n          >\n            <slot\n              :name=\"'main-row:' + (row.row.mainRowKey || i)\"\n              :full-colspan=\"fullColspan\"\n            >\n              <!-- The data-cant-run-bulk-action-of-interest attribute is being used instead of :class because\n                because our selection.js invokes toggleClass and :class clobbers what was added by toggleClass if\n                the value of :class changes. -->\n              <tr\n                class=\"main-row\"\n                :data-testid=\"componentTestid + '-' + i + '-row'\"\n                :class=\"{ 'has-sub-row': row.showSubRow }\"\n                :data-node-id=\"row.key\"\n                :data-cant-run-bulk-action-of-interest=\"actionOfInterest && !row.canRunBulkActionOfInterest\"\n              >\n                <td\n                  v-if=\"tableActions\"\n                  class=\"row-check\"\n                  align=\"middle\"\n                >\n                  {{ row.mainRowKey }}<Checkbox\n                    class=\"selection-checkbox\"\n                    :data-node-id=\"row.key\"\n                    :data-testid=\"componentTestid + '-' + i + '-checkbox'\"\n                    :value=\"selectedRows.includes(row.row)\"\n                  />\n                </td>\n                <td\n                  v-if=\"subExpandColumn\"\n                  class=\"row-expand\"\n                  align=\"middle\"\n                >\n                  <i\n                    data-title=\"Toggle Expand\"\n                    :class=\"{\n                      icon: true,\n                      'icon-chevron-right': !expanded[row.row[keyField]],\n                      'icon-chevron-down': !!expanded[row.row[keyField]],\n                    }\"\n                    @click.stop=\"toggleExpand(row.row)\"\n                  />\n                </td>\n                <template\n                  v-for=\"(col, j) in row.columns\"\n                  :key=\"j\"\n                >\n                  <slot\n                    :name=\"'col:' + col.col.name\"\n                    :row=\"row.row\"\n                    :col=\"col.col\"\n                    :dt=\"dt\"\n                    :expanded=\"expanded\"\n                    :row-key=\"row.key\"\n                  >\n                    <td\n                      v-show=\"!hasAdvancedFiltering || (hasAdvancedFiltering && col.col.isColVisible)\"\n                      :key=\"col.col.name\"\n                      :data-title=\"col.col.label\"\n                      :data-testid=\"`sortable-cell-${i}-${j}`\"\n                      :align=\"col.col.align || 'left'\"\n                      :class=\"{ ['col-' + col.dasherize]: !!col.col.formatter, [col.col.breakpoint]: !!col.col.breakpoint, ['skip-select']: col.col.skipSelect }\"\n                      :width=\"col.col.width\"\n                    >\n                      <slot\n                        :name=\"'cell:' + col.col.name\"\n                        :row=\"row.row\"\n                        :col=\"col.col\"\n                        :value=\"col.value\"\n                      >\n                        <component\n                          :is=\"col.component\"\n                          v-if=\"col.component && col.needRef\"\n                          ref=\"column\"\n                          :value=\"col.value\"\n                          :row=\"row.row\"\n                          :col=\"col.col\"\n                          v-bind=\"col.col.formatterOpts\"\n                          :row-key=\"row.key\"\n                          :get-custom-detail-link=\"getCustomDetailLink\"\n                        />\n                        <component\n                          :is=\"col.component\"\n                          v-else-if=\"col.component\"\n                          :value=\"col.value\"\n                          :row=\"row.row\"\n                          :col=\"col.col\"\n                          v-bind=\"col.col.formatterOpts\"\n                          :row-key=\"row.key\"\n                        />\n                        <component\n                          :is=\"col.col.formatter\"\n                          v-else-if=\"col.col.formatter\"\n                          :value=\"col.value\"\n                          :row=\"row.row\"\n                          :col=\"col.col\"\n                          v-bind=\"col.col.formatterOpts\"\n                          :row-key=\"row.key\"\n                        />\n                        <template v-else-if=\"col.value !== ''\">\n                          {{ col.formatted }}\n                        </template>\n                        <template v-else-if=\"col.col.dashIfEmpty\">\n                          <span class=\"text-muted\">&mdash;</span>\n                        </template>\n                      </slot>\n                    </td>\n                  </slot>\n                </template>\n                <td\n                  v-if=\"rowActions\"\n                  align=\"middle\"\n                >\n                  <slot\n                    name=\"row-actions\"\n                    :row=\"row.row\"\n                  >\n                    <button\n                      :id=\"`actionButton+${i}+${(row.row && row.row.name) ? row.row.name : ''}`\"\n                      :ref=\"`actionButton${i}`\"\n                      :data-testid=\"componentTestid + '-' + i + '-action-button'\"\n                      aria-haspopup=\"true\"\n                      aria-expanded=\"false\"\n                      type=\"button\"\n                      class=\"btn btn-sm role-multi-action actions\"\n                      @click=\"handleActionButtonClick(i, $event)\"\n                    >\n                      <i class=\"icon icon-actions\" />\n                    </button>\n                  </slot>\n                </td>\n              </tr>\n            </slot>\n          </slot>\n          <slot\n            v-if=\"row.showSubRow\"\n            name=\"sub-row\"\n            :full-colspan=\"fullColspan\"\n            :row=\"row.row\"\n            :sub-matches=\"subMatches\"\n            :key-field=\"keyField\"\n            :component-testid=\"componentTestid\"\n            :i=\"i\"\n            :on-row-mouse-enter=\"onRowMouseEnter\"\n            :on-row-mouse-leave=\"onRowMouseLeave\"\n          >\n            <tr\n              v-if=\"row.row.stateDescription\"\n              :key=\"row.row[keyField] + '-description'\"\n              :data-testid=\"componentTestid + '-' + i + '-row-description'\"\n              class=\"state-description sub-row\"\n              @mouseenter=\"onRowMouseEnter\"\n              @mouseleave=\"onRowMouseLeave\"\n            >\n              <td\n                v-if=\"tableActions\"\n                class=\"row-check\"\n                align=\"middle\"\n              />\n              <td\n                :colspan=\"fullColspan - (tableActions ? 1 : 0)\"\n                :class=\"{ 'text-error': row.row.stateObj.error }\"\n              >\n                {{ row.row.stateDescription }}\n              </td>\n            </tr>\n          </slot>\n        </template>\n      </tbody>\n    </table>\n    <div\n      v-if=\"showPaging\"\n      class=\"paging\"\n    >\n      <button\n        type=\"button\"\n        class=\"btn btn-sm role-multi-action\"\n        data-testid=\"pagination-first\"\n        :disabled=\"page == 1 || loading\"\n        @click=\"goToPage('first')\"\n      >\n        <i class=\"icon icon-chevron-beginning\" />\n      </button>\n      <button\n        type=\"button\"\n        class=\"btn btn-sm role-multi-action\"\n        data-testid=\"pagination-prev\"\n        :disabled=\"page == 1 || loading\"\n        @click=\"goToPage('prev')\"\n      >\n        <i class=\"icon icon-chevron-left\" />\n      </button>\n      <span>\n        {{ pagingDisplay }}\n      </span>\n      <button\n        type=\"button\"\n        class=\"btn btn-sm role-multi-action\"\n        data-testid=\"pagination-next\"\n        :disabled=\"page == totalPages || loading\"\n        @click=\"goToPage('next')\"\n      >\n        <i class=\"icon icon-chevron-right\" />\n      </button>\n      <button\n        type=\"button\"\n        class=\"btn btn-sm role-multi-action\"\n        data-testid=\"pagination-last\"\n        :disabled=\"page == totalPages || loading\"\n        @click=\"goToPage('last')\"\n      >\n        <i class=\"icon icon-chevron-end\" />\n      </button>\n    </div>\n    <button\n      v-if=\"search\"\n      v-shortkey.once=\"['/']\"\n      class=\"hide\"\n      @shortkey=\"focusSearch()\"\n    />\n    <template v-if=\"tableActions\">\n      <button\n        v-shortkey=\"['j']\"\n        class=\"hide\"\n        @shortkey=\"focusNext($event)\"\n      />\n      <button\n        v-shortkey=\"['k']\"\n        class=\"hide\"\n        @shortkey=\"focusPrevious($event)\"\n      />\n      <button\n        v-shortkey=\"['shift', 'j']\"\n        class=\"hide\"\n        @shortkey=\"focusNext($event, true)\"\n      />\n      <button\n        v-shortkey=\"['shift', 'k']\"\n        class=\"hide\"\n        @shortkey=\"focusPrevious($event, true)\"\n      />\n      <slot name=\"shortkeys\" />\n    </template>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .sortable-table.alt-loading {\n    opacity: 0.5;\n    pointer-events: none;\n  }\n\n  .manual-refresh {\n    height: 40px;\n  }\n  .advanced-filter-group {\n    position: relative;\n    margin-left: 10px;\n    .advanced-filter-container {\n      position: absolute;\n      top: 38px;\n      right: 0;\n      width: 300px;\n      border: 1px solid var(--primary);\n      background-color: var(--body-bg);\n      padding: 20px;\n      z-index: 2;\n\n      .middle-block {\n        display: flex;\n        align-items: center;\n        margin-top: 20px;\n\n        span {\n          margin-right: 20px;\n        }\n\n        button {\n          margin-left: 20px;\n        }\n      }\n\n      .bottom-block {\n        display: flex;\n        align-items: center;\n        margin-top: 40px;\n        justify-content: space-between;\n      }\n    }\n  }\n\n  .advanced-filters-applied {\n    display: inline-flex;\n    margin: 0;\n    padding: 0;\n    list-style: none;\n    max-width: 100%;\n    flex-wrap: wrap;\n    justify-content: flex-end;\n\n    li {\n      margin: 0 20px 10px 0;\n      padding: 2px 5px;\n      border: 1px solid;\n      display: flex;\n      align-items: center;\n      position: relative;\n      height: 20px;\n\n      &:nth-child(4n+1) {\n        border-color: var(--success);\n\n        .bg {\n          background-color: var(--success);\n        }\n      }\n\n      &:nth-child(4n+2) {\n        border-color: var(--warning);\n\n        .bg {\n          background-color: var(--warning);\n        }\n      }\n\n      &:nth-child(4n+3) {\n        border-color: var(--info);\n\n        .bg {\n          background-color: var(--info);\n        }\n      }\n\n      &:nth-child(4n+4) {\n        border-color: var(--error);\n\n        .bg {\n          background-color: var(--error);\n        }\n      }\n\n      .bg {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n       opacity: 0.2;\n        z-index: -1;\n      }\n\n      .label {\n        margin-right: 10px;\n        font-size: 11px;\n      }\n     .cross {\n        font-size: 12px;\n        font-weight: bold;\n        cursor: pointer;\n      }\n    }\n  }\n\n  // Remove colors from multi-action buttons in the table\n  td {\n    .actions.role-multi-action {\n      background-color: transparent;\n      border: none;\n      &:hover, &:focus {\n        background-color: var(--accent-btn);\n        box-shadow: none;\n      }\n    }\n\n    // Aligns with COLUMN_BREAKPOINTS\n    @media only screen and (max-width: map-get($breakpoints, '--viewport-4')) {\n      // HIDE column on sizes below 480px\n      &.tablet, &.laptop, &.desktop {\n        display: none;\n      }\n    }\n    @media only screen and (max-width: map-get($breakpoints, '--viewport-9')) {\n      // HIDE column on sizes below 992px\n      &.laptop, &.desktop {\n        display: none;\n      }\n    }\n    @media only screen and (max-width: map-get($breakpoints, '--viewport-12')) {\n      // HIDE column on sizes below 1281px\n      &.desktop {\n        display: none;\n      }\n    }\n  }\n\n  // Loading indicator row\n  tr td div.data-loading {\n    align-items: center;\n    display: flex;\n    justify-content: center;\n    padding: 20px 0;\n    > i {\n      font-size: 20px;\n      height: 20px;\n      margin-right: 5px;\n      width: 20px;\n    }\n  }\n\n  .search-box {\n    height: 40px;\n    margin-left: 10px;\n    min-width: 180px;\n  }\n</style>\n\n<style lang=\"scss\">\n  //\n  // Important: Almost all selectors in here need to be \">\"-ed together so they\n  // apply only to the current table, not one nested inside another table.\n  //\n\n  $group-row-height: 40px;\n  $group-separation: 40px;\n  $divider-height: 1px;\n\n  $separator: 20;\n  $remove: 100;\n  $spacing: 10px;\n\n  .filter-select .vs__selected-options .vs__selected {\n    text-align: left;\n  }\n\n  .sortable-table {\n    border-collapse: collapse;\n    min-width: 400px;\n    border-radius: 5px 5px 0 0;\n    outline: 1px solid var(--border);\n    overflow: hidden;\n    background: var(--sortable-table-bg);\n    border-radius: 4px;\n\n    &.overflow-x {\n      overflow-x: visible;\n    }\n    &.overflow-y {\n      overflow-y: visible;\n    }\n\n    td {\n      padding: 8px 5px;\n      border: 0;\n\n      &:first-child {\n        padding-left: 10px;\n      }\n\n      &:last-child {\n        padding-right: 10px;\n      }\n\n      &.row-check {\n        padding-top: 12px;\n      }\n    }\n\n    tbody {\n      tr {\n        border-bottom: 1px solid var(--sortable-table-top-divider);\n        background-color: var(--sortable-table-row-bg);\n\n        &.main-row.has-sub-row {\n          border-bottom: 0;\n        }\n\n        // if a main-row is hovered also hover its sibling sub row. note - the reverse is handled in selection.js\n        &.main-row:not(.row-selected):hover + .sub-row {\n          background-color: var(--sortable-table-hover-bg);\n        }\n\n        &:last-of-type {\n          border-bottom: 0;\n        }\n\n        &:hover, &.sub-row-hovered {\n          background-color: var(--sortable-table-hover-bg);\n        }\n\n        &.state-description > td {\n          font-size: 13px;\n          padding-top: 0;\n          overflow-wrap: anywhere;\n        }\n      }\n\n      tr.active-row {\n        color: var(--sortable-table-header-bg);\n      }\n\n      tr.row-selected {\n        background: var(--sortable-table-selected-bg);\n      }\n\n      .no-rows {\n        td {\n          padding: 30px 0;\n          text-align: center;\n        }\n      }\n\n      .no-rows, .no-results {\n        &:hover {\n          background-color: var(--body-bg);\n        }\n      }\n\n      &.group {\n        &:before {\n          content: \"\";\n          display: block;\n          height: 20px;\n          background-color: transparent;\n        }\n      }\n\n      tr.group-row {\n        background-color: initial;\n\n        &:first-child {\n          border-bottom: 2px solid var(--sortable-table-row-bg);\n        }\n\n        &:not(:first-child) {\n          margin-top: 20px;\n        }\n\n        td {\n          padding: 0;\n\n          &:first-of-type {\n            border-left: 1px solid var(--sortable-table-accent-bg);\n          }\n        }\n\n        .group-tab {\n          @include clearfix;\n          height: $group-row-height;\n          line-height: $group-row-height;\n          padding: 0 10px;\n          border-radius: 4px 4px 0px 0px;\n          background-color: var(--sortable-table-row-bg);\n          position: relative;\n          top: 1px;\n          display: inline-block;\n          z-index: z-index('tableGroup');\n          min-width: $group-row-height * 1.8;\n\n          > SPAN {\n            color: var(--sortable-table-group-label);\n          }\n        }\n\n        .group-tab:after {\n          height: $group-row-height;\n          width: 70px;\n          border-radius: 5px 5px 0px 0px;\n          background-color: var(--sortable-table-row-bg);\n          content: \"\";\n          position: absolute;\n          right: -15px;\n          top: 0px;\n          transform: skewX(40deg);\n          z-index: -1;\n        }\n      }\n    }\n  }\n\n  .for-inputs{\n    & TABLE.sortable-table {\n    width: 100%;\n    border-collapse: collapse;\n    margin-bottom: $spacing;\n\n    >TBODY>TR>TD, >THEAD>TR>TH {\n      padding-right: $spacing;\n      padding-bottom: $spacing;\n\n      &:last-of-type {\n        padding-right: 0;\n      }\n    }\n\n    >TBODY>TR:first-of-type>TD {\n      padding-top: $spacing;\n    }\n\n    >TBODY>TR:last-of-type>TD {\n      padding-bottom: 0;\n    }\n  }\n\n    &.edit, &.create, &.clone {\n      TABLE.sortable-table>THEAD>TR>TH {\n      border-color: transparent;\n      }\n    }\n  }\n\n  .sortable-table-header {\n    position: relative;\n    z-index: z-index('fixedTableHeader');\n\n    &.titled {\n      display: flex;\n      align-items: center;\n    }\n  }\n  .fixed-header-actions.button{\n    grid-template-columns: [bulk] auto [middle] min-content [search] minmax(min-content, 350px);\n  }\n\n  .fixed-header-actions {\n    padding: 0 0 20px 0;\n    width: 100%;\n    z-index: z-index('fixedTableHeader');\n    background: transparent;\n    display: grid;\n    grid-template-columns: [bulk] auto [middle] min-content [search] minmax(min-content, 200px);\n    grid-column-gap: 10px;\n\n    &.advanced-filtering {\n      grid-template-columns: [bulk] auto [middle] minmax(min-content, auto) [search] minmax(min-content, auto);\n    }\n\n    .bulk {\n      grid-area: bulk;\n\n      $gap: 10px;\n\n      & > BUTTON {\n        display: none; // Handled dynamically\n      }\n\n      & > BUTTON:not(:last-of-type) {\n        margin-right: $gap;\n      }\n\n      .action-availability {\n        display: none; // Handled dynamically\n        margin-left: $gap;\n        vertical-align: middle;\n        margin-top: 2px;\n      }\n\n      .dropdown-button {\n        $disabled-color: var(--disabled-text);\n        $disabled-cursor: not-allowed;\n        li.disabled {\n          color: $disabled-color;\n          cursor: $disabled-cursor;\n\n          &:hover {\n            color: $disabled-color;\n            background-color: unset;\n            cursor: $disabled-cursor;\n          }\n        }\n      }\n\n      .bulk-action  {\n        .icon {\n          vertical-align: -10%;\n        }\n      }\n    }\n\n    .middle {\n      grid-area: middle;\n      white-space: nowrap;\n\n      .icon.icon-backup.animate {\n        animation-name: spin;\n        animation-duration: 1000ms;\n        animation-iteration-count: infinite;\n        animation-timing-function: linear;\n      }\n\n      @keyframes spin {\n        from {\n          transform:rotate(0deg);\n        }\n        to {\n          transform:rotate(360deg);\n        }\n      }\n    }\n\n    .search {\n      grid-area: search;\n      text-align: right;\n      justify-content: flex-end;\n      margin-top: 1px;\n    }\n\n    .bulk-actions-dropdown {\n      display: none; // Handled dynamically\n\n      .dropdown-button {\n        background-color: var(--primary);\n\n        &:hover {\n          background-color: var(--primary-hover-bg);\n          color: var(--primary-hover-text);\n        }\n\n        > *, .icon-chevron-down {\n          color: var(--primary-text);\n        }\n\n        .button-divider {\n          border-color: var(--primary-text);\n        }\n\n        &.disabled {\n          border-color: var(--disabled-bg);\n\n          .icon-chevron-down {\n            color: var(--disabled-text) !important;\n          }\n\n          .button-divider {\n            border-color: var(--disabled-text);\n          }\n        }\n      }\n    }\n  }\n\n  .paging {\n    margin-top: 10px;\n    text-align: center;\n\n    SPAN {\n      display: inline-block;\n      min-width: 200px;\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/paging.js",
    "content": "import { ROWS_PER_PAGE } from '@pkg/store/prefs';\n\nexport default {\n  computed: {\n    totalRows() {\n      if (this.externalPaginationEnabled) {\n        return this.externalPaginationResult?.count || 0;\n      }\n\n      return this.filteredRows.length;\n    },\n\n    indexFrom() {\n      return Math.max(0, 1 + this.perPage * (this.page - 1));\n    },\n\n    indexTo() {\n      return Math.min(this.totalRows, this.indexFrom + this.perPage - 1);\n    },\n\n    totalPages() {\n      return Math.ceil(this.totalRows / this.perPage );\n    },\n\n    showPaging() {\n      if (!this.paging) {\n        return false;\n      }\n\n      const havePages = this.totalPages > 1;\n\n      if (this.altLoading) {\n        return havePages;\n      }\n\n      return !this.loading && havePages;\n    },\n\n    pagingDisplay() {\n      const opt = {\n        ...(this.pagingParams || {}),\n\n        count: this.totalRows,\n        pages: this.totalPages,\n        from:  this.indexFrom,\n        to:    this.indexTo,\n      };\n\n      return this.$store.getters['i18n/t'](this.pagingLabel, opt);\n    },\n\n    pagedRows() {\n      if (this.externalPaginationEnabled) {\n        return this.rows;\n      } else if ( this.paging ) {\n        return this.filteredRows.slice(this.indexFrom - 1, this.indexTo);\n      } else {\n        return this.filteredRows;\n      }\n    },\n  },\n\n  data() {\n    const perPage = this.getPerPage();\n\n    return { page: 1, perPage };\n  },\n\n  watch: {\n    pagedRows() {\n      // Go to the last page if we end up \"past\" the last page because the table changed\n\n      const from = this.indexFrom;\n      const last = this.totalRows;\n\n      if ( this.totalPages > 0 && this.page > 1 && from > last ) {\n        this.setPage(this.totalPages);\n      }\n    },\n\n    page() {\n      this.debouncedPaginationChanged();\n    },\n\n    perPage() {\n      this.debouncedPaginationChanged();\n    },\n\n  },\n\n  methods: {\n    getPerPage() {\n      // perPage cannot change while the list is displayed\n      let out = this.rowsPerPage || 0;\n\n      if ( out <= 0 ) {\n        out = parseInt(this.$store.getters['prefs/get'](ROWS_PER_PAGE), 10) || 0;\n      }\n\n      // This should ideally never happen, but the preference value could be invalid, so return something...\n      if ( out <= 0 ) {\n        out = 10;\n      }\n\n      return out;\n    },\n\n    setPage(num) {\n      if (this.page === num) {\n        return;\n      }\n\n      this.page = num;\n    },\n\n    goToPage(which) {\n      let page;\n\n      switch (which) {\n      case 'first':\n        page = 1;\n        break;\n      case 'prev':\n        page = Math.max(1, this.page - 1 );\n        break;\n      case 'next':\n        page = Math.min(this.totalPages, this.page + 1 );\n        break;\n      case 'last':\n        page = this.totalPages;\n        break;\n      }\n\n      this.setPage(page);\n    },\n\n    getPageByRow(rowId, getRowId = (x) => x) {\n      const pos = this.filteredRows.map(getRowId).indexOf(rowId);\n\n      if (pos === -1) {\n        return null;\n      }\n\n      return Math.ceil(pos / this.perPage);\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/selection.js",
    "content": "import { filterBy } from '@pkg/utils/array';\nimport { getParent } from '@pkg/utils/dom';\nimport { get } from '@pkg/utils/object';\nimport { isMore, isRange, suppressContextMenu, isAlternate } from '@pkg/utils/platform';\n\nexport const ALL = 'all';\nexport const SOME = 'some';\nexport const NONE = 'none';\n\nexport default {\n  mounted() {\n    const table = this.$el.querySelector('TABLE');\n\n    this._onRowClickBound = this.onRowClick.bind(this);\n    this._onRowMousedownBound = this.onRowMousedown.bind(this);\n    this._onRowContextBound = this.onRowContext.bind(this);\n\n    table.addEventListener('click', this._onRowClickBound);\n    table.addEventListener('mousedown', this._onRowMousedownBound);\n    table.addEventListener('contextmenu', this._onRowContextBound);\n  },\n\n  beforeUnmount() {\n    const table = this.$el.querySelector('TABLE');\n\n    table.removeEventListener('click', this._onRowClickBound);\n    table.removeEventListener('mousedown', this._onRowMousedownBound);\n    table.removeEventListener('contextmenu', this._onRowContextBound);\n  },\n\n  computed: {\n    // Used for the table-level selection check-box to show checked (all selected)/intermediate (some selected)/unchecked (none selected)\n    howMuchSelected() {\n      const total = this.pagedRows.length;\n      const selected = this.selectedRows.length;\n\n      if ( selected >= total && total > 0 ) {\n        return ALL;\n      } else if ( selected > 0 ) {\n        return SOME;\n      }\n\n      return NONE;\n    },\n\n    // NOTE: The logic here could be simplified and made more performant\n    bulkActionsForSelection() {\n      let disableAll = false;\n\n      // pagedRows is all rows in the current page\n      const all = this.pagedRows;\n      const allRows = this.arrangedRows || all;\n      let selected = this.selectedRows;\n\n      // Nothing is selected\n      if ( !this.selectedRows.length ) {\n        // and there are no rows\n        if ( !allRows ) {\n          return [];\n        }\n\n        const firstNode = allRows[0];\n\n        selected = firstNode ? [firstNode] : [];\n        disableAll = true;\n      }\n\n      const map = {};\n\n      // Find and add all the actions for all the nodes so that we know\n      // what all the possible actions are\n      for ( const node of all ) {\n        if (node.availableActions) {\n          for ( const act of node.availableActions ) {\n            if ( act.bulkable ) {\n              _add(map, act, false);\n            }\n          }\n        }\n      }\n\n      // Go through all the selected items and add the actions (which were already identified above)\n      // as available for some (or all) of the selected nodes\n      for ( const node of selected ) {\n        if (node.availableActions) {\n          for ( const act of node.availableActions ) {\n            if ( act.bulkable && act.enabled ) {\n              _add(map, act, false);\n            }\n          }\n        }\n      }\n\n      // If there's no items actually selected, we want to see all the actions\n      // so you know what exists, but have them all be disabled since there's nothing to do them on.\n      const out = _filter(map, disableAll);\n\n      // Enable a bulkaction if some of the selected items can perform the action\n      out.forEach((bulkAction) => {\n        const actionEnabledForSomeSelected = this.selectedRows.some((node) => {\n          const availableActions = node.availableActions || [];\n\n          return availableActions.some((action) => action.action === bulkAction.action && action.enabled);\n        });\n\n        bulkAction.enabled = this.selectedRows.length > 0 && actionEnabledForSomeSelected;\n      });\n\n      return out.sort((a, b) => (b.weight || 0) - (a.weight || 0));\n    },\n  },\n\n  data() {\n    return {\n      // List of selected items in the table\n      selectedRows: [],\n      prevNode:     null,\n    };\n  },\n\n  watch: {\n    // On page change\n    pagedRows() {\n      // When the table contents changes:\n      // - Remove items that are in the selection but no longer in the table.\n\n      const content = this.pagedRows;\n      const toRemove = [];\n\n      for (const node of this.selectedRows) {\n        if (!content.includes(node) ) {\n          toRemove.push(node);\n        }\n      }\n\n      this.update([], toRemove);\n    },\n  },\n\n  methods: {\n    onToggleAll(value) {\n      if ( value ) {\n        this.update(this.pagedRows, []);\n\n        return true;\n      } else {\n        this.update([], this.pagedRows);\n\n        return false;\n      }\n    },\n\n    onRowMousedown(e) {\n      if ( isRange(e) || this.isSelectionCheckbox(e.target) ) {\n        e.preventDefault();\n      }\n    },\n\n    onRowMouseEnter(e) {\n      const tr = e.target.closest('TR');\n\n      if (tr.classList.contains('sub-row')) {\n        const trMainRow = tr.previousElementSibling;\n\n        trMainRow.classList.add('sub-row-hovered');\n      }\n    },\n\n    onRowMouseLeave(e) {\n      const tr = e.target.closest('TR');\n\n      if (tr.classList.contains('sub-row')) {\n        const trMainRow = tr.previousElementSibling;\n\n        trMainRow.classList.remove('sub-row-hovered');\n      }\n    },\n\n    nodeForEvent(e) {\n      const tagName = e.target.tagName;\n      const tgt = e.target;\n      const actionElement = tgt.closest('.actions');\n\n      if ( tgt.classList.contains('select-all-check') ) {\n        return;\n      }\n\n      if ( !actionElement ) {\n        if (\n          tagName === 'A' ||\n          tagName === 'BUTTON' ||\n          getParent(tgt, '.btn')\n        ) {\n          return;\n        }\n      }\n\n      const tgtRow = e.target.closest('TR');\n\n      return this.nodeForRow(tgtRow);\n    },\n\n    nodeForRow(tgtRow) {\n      if ( tgtRow?.classList.contains('separator-row') ) {\n        return;\n      }\n\n      while ( tgtRow && !tgtRow.classList.contains('main-row') ) {\n        tgtRow = tgtRow.previousElementSibling;\n      }\n\n      if ( !tgtRow ) {\n        return;\n      }\n\n      const nodeId = tgtRow.dataset.nodeId;\n\n      if ( !nodeId ) {\n        return;\n      }\n\n      const node = this.pagedRows.find( (x) => get(x, this.keyField) === nodeId );\n\n      return node;\n    },\n\n    async onRowClick(e) {\n      const node = this.nodeForEvent(e);\n      const td = e.target.closest('TD');\n      const skipSelect = td?.classList.contains('skip-select');\n\n      if (skipSelect) {\n        return;\n      }\n      const selection = this.selectedRows;\n      const isCheckbox = this.isSelectionCheckbox(e.target) || td?.classList.contains('row-check');\n      const isExpand = td?.classList.contains('row-expand');\n      const content = this.pagedRows;\n\n      this.$emit('rowClick', e);\n\n      if ( !node ) {\n        return;\n      }\n\n      if ( isExpand ) {\n        this.toggleExpand(node);\n\n        return;\n      }\n\n      const actionElement = e.target.closest('.actions');\n\n      if ( actionElement ) {\n        let resources = [node];\n\n        if ( this.mangleActionResources ) {\n          const i = actionElement.querySelector('i');\n\n          i.classList.remove('icon-actions');\n          i.classList.add('icon-spinner');\n          i.classList.add('icon-spin');\n\n          try {\n            resources = await this.mangleActionResources(resources);\n          } finally {\n            i.classList.remove('icon-spinner');\n            i.classList.remove('icon-spin');\n            i.classList.add('icon-actions');\n          }\n        }\n\n        this.$store.commit(`action-menu/show`, {\n          resources,\n          event: e,\n          elem:  actionElement,\n        });\n\n        return;\n      }\n\n      const isSelected = selection.includes(node);\n      let prevNode = this.prevNode;\n\n      // PrevNode is only valid if it's in the current content\n      if ( !prevNode || !content.includes(prevNode) ) {\n        prevNode = node;\n      }\n\n      if ( isMore(e) ) {\n        this.toggle(node);\n      } else if ( isRange(e) ) {\n        const toToggle = this.nodesBetween(prevNode, node);\n\n        if ( isSelected ) {\n          this.update([], toToggle);\n        } else {\n          this.update(toToggle, []);\n        }\n      } else if ( isCheckbox ) {\n        this.toggle(node);\n      } else {\n        this.update([node], content);\n      }\n\n      this.prevNode = node;\n    },\n\n    async onRowContext(e) {\n      const node = this.nodeForEvent(e);\n\n      if ( suppressContextMenu(e) ) {\n        return;\n      }\n\n      if ( !node ) {\n        return;\n      }\n\n      e.preventDefault();\n      e.stopPropagation();\n\n      this.prevNode = node;\n      const isSelected = this.selectedRows.includes(node);\n\n      if ( !isSelected ) {\n        this.update([node], this.selectedRows.slice());\n      }\n\n      let resources = this.selectedRows;\n\n      if ( this.mangleActionResources ) {\n        resources = await this.mangleActionResources(resources);\n      }\n\n      this.$store.commit(`action-menu/show`, {\n        resources,\n        event: e,\n      });\n    },\n\n    keySelectRow(row, more = false) {\n      const node = this.nodeForRow(row);\n      const content = this.pagedRows;\n\n      if ( !node ) {\n        return;\n      }\n\n      if ( more ) {\n        this.update([node], []);\n      } else {\n        this.update([node], content);\n      }\n\n      this.prevNode = node;\n    },\n\n    isSelectionCheckbox(element) {\n      return element.tagName === 'INPUT' &&\n        element.type === 'checkbox' &&\n        element.closest('.selection-checkbox') !== null;\n    },\n\n    nodesBetween(a, b) {\n      let toToggle = [];\n      const key = this.groupBy;\n\n      if ( key ) {\n        // Grouped has 2 levels to look through\n        const grouped = this.groupedRows;\n\n        let from = this.groupIdx(a);\n        let to = this.groupIdx(b);\n\n        if ( !from || !to ) {\n          return [];\n        }\n\n        // From has to come before To\n        if ( (from.group > to.group) || ((from.group === to.group) && (from.item > to.item)) ) {\n          [from, to] = [to, from];\n        }\n\n        for ( let i = from.group; i <= to.group; i++ ) {\n          const items = grouped[i].rows;\n          let j = (from.group === i ? from.item : 0);\n\n          while ( items[j] && ( i < to.group || j <= to.item )) {\n            toToggle.push(items[j]);\n            j++;\n          }\n        }\n      } else {\n        // Ungrouped is much simpler\n        const content = this.pagedRows;\n        let from = content.indexOf(a);\n        let to = content.indexOf(b);\n\n        [from, to] = [Math.min(from, to), Math.max(from, to)];\n        toToggle = content.slice(from, to + 1);\n      }\n\n      // check if there is already duplicate content selected (selectedRows) on the list to toggle...\n      toToggle = toToggle.filter((item) => !this.selectedRows.includes(item));\n\n      return toToggle;\n    },\n\n    groupIdx(node) {\n      const grouped = this.groupedRows;\n\n      for ( let i = 0; i < grouped.length; i++ ) {\n        const rows = grouped[i].rows;\n\n        for ( let j = 0; j < rows.length; j++ ) {\n          if ( rows[j] === node ) {\n            return {\n              group: i,\n              item:  j,\n            };\n          }\n        }\n      }\n\n      return null;\n    },\n\n    toggle(node) {\n      const add = [];\n      const remove = [];\n\n      if (this.selectedRows.includes(node)) {\n        remove.push(node);\n      } else {\n        add.push(node);\n      }\n\n      this.update(add, remove);\n    },\n\n    update(toAdd, toRemove) {\n      toRemove.forEach((row) => {\n        const index = this.selectedRows.findIndex((r) => r._key === row._key);\n\n        if (index !== -1) {\n          this.selectedRows.splice(index, 1);\n        }\n      });\n\n      if ( toAdd ) {\n        this.selectedRows.push(...toAdd);\n      }\n\n      // Uncheck and check the checkboxes of nodes that have been added/removed\n      if (toRemove.length) {\n        this.$nextTick(() => {\n          for ( let i = 0; i < toRemove.length; i++ ) {\n            this.updateInput(toRemove[i], false, this.keyField);\n          }\n        });\n      }\n\n      if (toAdd.length) {\n        this.$nextTick(() => {\n          for ( let i = 0; i < toAdd.length; i++ ) {\n            this.updateInput(toAdd[i], true, this.keyField);\n          }\n        });\n      }\n\n      this.$nextTick(() => {\n        this.$emit('selection', this.selectedRows);\n      });\n    },\n\n    updateInput(node, on, keyField) {\n      const id = get(node, keyField);\n\n      if ( id ) {\n        // Note: This is looking for the checkbox control for the row\n        const input = this.$el.querySelector(`div[data-checkbox-ctrl][data-node-id=\"${ id }\"]`);\n\n        if ( input && !input.disabled ) {\n          const label = input.querySelector('label');\n\n          if (label) {\n            label.value = on;\n          }\n          let tr = input.closest('tr');\n          let first = true;\n\n          while ( tr && (first || tr.classList.contains('sub-row') ) ) {\n            if (on) {\n              tr.classList.add('row-selected');\n            } else {\n              tr.classList.remove('row-selected');\n            }\n            tr = tr.nextElementSibling;\n            first = false;\n          }\n        }\n      }\n    },\n\n    select(nodes) {\n      nodes.forEach((node) => {\n        const id = get(node, this.keyField);\n        const input = this.$el.querySelector(`label[data-node-id=\"${ id }\"]`);\n\n        input.dispatchEvent(new Event('click'));\n      });\n    },\n\n    applyTableAction(action, args, event) {\n      const opts = { alt: event && isAlternate(event), event };\n\n      // Go through the table selection and filter out those actions that can't run the chosen action\n      const executableSelection = this.selectedRows.filter((row) => {\n        const matchingResourceAction = row.availableActions.find((a) => a.action === action.action);\n\n        return matchingResourceAction?.enabled;\n      });\n\n      _execute(executableSelection, action, args, opts, this);\n\n      this.actionOfInterest = null;\n    },\n\n    clearSelection() {\n      this.update([], this.selectedRows);\n    },\n\n  },\n};\n\n// ---------------------------------------------------------------------\n// --- Helpers that were in selectionStore.js --------------------------\n// ---------------------------------------------------------------------\n\nlet anon = 0;\n\nfunction _add(map, act, incrementCounts = true) {\n  let id = act.action;\n\n  if ( !id ) {\n    id = `anon${ anon }`;\n    anon++;\n  }\n\n  let obj = map[id];\n\n  if ( !obj ) {\n    obj = Object.assign({}, act);\n    map[id] = obj;\n    obj.allEnabled = false;\n  }\n\n  if ( !act.enabled ) {\n    obj.allEnabled = false;\n  } else {\n    obj.anyEnabled = true;\n  }\n\n  if ( incrementCounts ) {\n    obj.available = (obj.available || 0) + (!act.enabled ? 0 : 1 );\n    obj.total = (obj.total || 0) + 1;\n  }\n\n  return obj;\n}\n\nfunction _filter(map, disableAll = false) {\n  const out = filterBy(Object.values(map), 'anyEnabled', true);\n\n  for ( const act of out ) {\n    if ( disableAll ) {\n      act.enabled = false;\n    } else {\n      act.enabled = ( act.available >= act.total );\n    }\n  }\n\n  return out;\n}\n\nfunction _execute(resources, action, args, opts = {}, ctx) {\n  args = args || [];\n\n  // New pattern for extensions - always call invoke\n  if (action.invoke) {\n    const actionOpts = {\n      action,\n      event: opts.event,\n      isAlt: !!opts.alt,\n    };\n\n    return action.invoke.apply(ctx, [actionOpts, resources || [], args]);\n  }\n\n  if ( resources.length > 1 && action.bulkAction && !opts.alt ) {\n    const fn = resources[0][action.bulkAction];\n\n    if ( fn ) {\n      return fn.call(resources[0], resources, ...args);\n    }\n  }\n\n  const promises = [];\n\n  for ( const resource of resources ) {\n    let fn;\n\n    if (opts.alt && action.altAction) {\n      fn = resource[action.altAction];\n    } else {\n      fn = resource[action.action];\n    }\n\n    if ( fn ) {\n      promises.push(fn.apply(resource, args));\n    }\n  }\n\n  return Promise.all(promises);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/sortable-config.ts",
    "content": "// Its quicker to render if we directly supply the components for the formatters\n// rather than just the name of a global component - so create a map of the formatter components\n// NOTE: This is populated by a plugin (formatters.js) to avoid issues with plugins\nexport const FORMATTERS = {};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SortableTable/sorting.js",
    "content": "import { uniq } from '@pkg/utils/array';\nimport { sortBy } from '@pkg/utils/sort';\n\n/**\n * Always sort by something, this is the best guess on properties\n *\n * Can be overridden\n */\nconst DEFAULT_MANDATORY_SORT = ['nameSort', 'id'];\n\nexport default {\n  computed: {\n    sortFields() {\n      let fromGroup = ( this.groupBy ? this.groupSort || this.groupBy : null) || [];\n      let fromColumn = [];\n\n      const column = (this.columns || this.headers).find((x) => x && x.name && x.name.toLowerCase() === this.sortBy.toLowerCase());\n\n      if ( this.sortBy && column && column.sort ) {\n        fromColumn = column.sort;\n      }\n\n      if ( !Array.isArray(fromGroup) ) {\n        fromGroup = [fromGroup];\n      }\n\n      if ( !Array.isArray(fromColumn) ) {\n        fromColumn = [fromColumn];\n      }\n\n      // return the sorting based on grouping, user selection and fallback\n      return uniq([...fromGroup, ...fromColumn].concat(...(this.mandatorySort || DEFAULT_MANDATORY_SORT)));\n    },\n\n    arrangedRows() {\n      if (this.externalPaginationEnabled) {\n        return;\n      }\n\n      let key;\n\n      if ( this.sortGenerationFn ) {\n        key = `${ this.sortGenerationFn.apply(this) }/${ this.rows.length }/${ this.descending }/${ this.sortFields.join(',') }`;\n\n        if ( this.cacheKey === key ) {\n          return this.cachedRows;\n        }\n      }\n\n      const out = sortBy(this.rows, this.sortFields, this.descending);\n\n      if ( key ) {\n        this.cacheKey = key;\n        this.cachedRows = out;\n      }\n\n      return out;\n    },\n  },\n\n  data() {\n    let sortBy = null;\n\n    this._defaultSortBy = this.defaultSortBy;\n\n    // Try to find a reasonable default sort\n    if ( !this._defaultSortBy ) {\n      const markedColumn = this.headers.find((x) => !!x.defaultSort);\n      const nameColumn = this.headers.find( (x) => x.name === 'name');\n\n      if ( markedColumn ) {\n        this._defaultSortBy = markedColumn.name;\n      } else if ( nameColumn ) {\n        // Use the name column if there is one\n        this._defaultSortBy = nameColumn.name;\n      } else {\n        // The first column that isn't state\n        const first = this.headers.filter( (x) => x.name !== 'state' )[0];\n\n        if ( first ) {\n          this._defaultSortBy = first.name;\n        } else {\n          // I give up\n          this._defaultSortBy = 'id';\n        }\n      }\n    }\n\n    // If the sort column doesn't exist or isn't specified, use default\n    if ( !sortBy || !this.headers.find((x) => x.name === sortBy ) ) {\n      sortBy = this._defaultSortBy;\n    }\n\n    return {\n      sortBy,\n      descending: false,\n      cachedRows: null,\n      cacheKey:   null,\n    };\n  },\n\n  methods: {\n    changeSort(sort, desc) {\n      this.sortBy = sort;\n      this.descending = desc;\n\n      // Always go back to the first page when the sort is changed\n      this.setPage(1);\n    },\n  },\n\n  watch: {\n    sortFields() {\n      this.debouncedPaginationChanged();\n    },\n\n    descending() {\n      this.debouncedPaginationChanged();\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/components/StatusBar.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport BackendProgress from '@pkg/components/BackendProgress.vue';\nimport StatusBarItem, { StatusBarItemData } from '@pkg/components/StatusBarItem.vue';\n\ninterface BarItem {\n  name:       string,\n  component?: string,\n  icon:       string,\n  data?:      StatusBarItemData,\n}\n\nexport default defineComponent({\n  name:       'status-bar',\n  components: { BackendProgress, StatusBarItem },\n  computed:   {\n    ...mapGetters('preferences', ['getPreferences']),\n    kubernetesVersion(): string {\n      return this.getPreferences.kubernetes.version;\n    },\n    kubernetesEnabled(): boolean {\n      return this.getPreferences.kubernetes.enabled;\n    },\n    containerEngine(): string {\n      return this.getPreferences.containerEngine.name;\n    },\n    items(): BarItem[] {\n      return [\n        {\n          name: 'version', component: 'Version', icon: 'icon icon-rancher-desktop',\n        }, {\n          name: 'network', component: 'NetworkStatus', icon: 'icon icon-globe',\n        }, {\n          name: 'kubernetesVersion',\n          icon: 'kubernetes-black.svg',\n          data: {\n            label: {\n              bar:     'product.kubernetesVersion',\n              tooltip: 'product.kubernetesVersion',\n            },\n            value: this.kubernetesEnabled ? this.kubernetesVersion : this.t('product.deactivated'),\n          },\n        }, {\n          name: 'containerEngine',\n          icon: 'icon icon-init_container',\n          data: {\n            label: {\n              bar:     'product.containerEngine.abbreviation',\n              tooltip: 'product.containerEngine.fullName',\n            },\n            value: this.containerEngine,\n          },\n        },\n      ];\n    },\n  },\n});\n</script>\n\n<template>\n  <footer>\n    <div class=\"left-column\">\n      <status-bar-item\n        v-for=\"item in items\"\n        :key=\"item.name\"\n        :ref=\"item.name\"\n        :sub-component=\"item.component\"\n        :data=\"item.data\"\n        :icon=\"item.icon\"\n        class=\"status-bar-item\"\n      />\n    </div>\n    <div class=\"right-column\">\n      <BackendProgress class=\"progress\" />\n    </div>\n  </footer>\n</template>\n\n<style scoped lang=\"scss\">\nfooter {\n  align-items: center;\n  display: flex;\n  flex-direction: row;\n  padding: 5px 10px;\n  background-color: var(--footer-bg);\n  font-size: 12px;\n\n  .left-column {\n    display: flex;\n    white-space: nowrap;\n  }\n\n  .right-column {\n    display: flex;\n    justify-content: flex-end;\n    flex: 1;\n  }\n\n  .status-bar-item {\n    padding-right: 18px;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/StatusBarItem.vue",
    "content": "<script lang=\"ts\">\nimport { PropType, Component, defineComponent } from 'vue';\n\nimport NetworkStatus from '@pkg/components/NetworkStatus.vue';\nimport Version from '@pkg/components/Version.vue';\n\nexport interface StatusBarItemData {\n  value: string,\n  label: {\n    tooltip: string,\n    bar:     string,\n  },\n}\n\nexport default defineComponent({\n  name:  'status-bar-item',\n  props: {\n    data: {\n      type:    Object as PropType<StatusBarItemData>,\n      default: null,\n    },\n    subComponent: {\n      type:    String,\n      default: null,\n    },\n    icon: {\n      type:     String,\n      required: true,\n    },\n  },\n  computed: {\n    getSubComponent(): Component | undefined {\n      if (this.subComponent) {\n        return this.subComponent === 'Version' ? Version : NetworkStatus;\n      }\n\n      return undefined;\n    },\n    getTooltip() {\n      return {\n        content:     `<b>${ this.t(this.data.label.tooltip) }</b>: ${ this.data.value }`,\n        html:        true,\n        placement:   'top',\n        popperClass: 'tooltip-footer',\n      };\n    },\n    isSvgIcon(): boolean {\n      return this.icon.endsWith('.svg');\n    },\n    svgIconPath(): string | null {\n      return this.isSvgIcon ? require(`@pkg/assets/images/${ this.icon }`) : null;\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"status-bar-item\">\n    <span\n      v-if=\"data\"\n      v-clean-tooltip=\"getTooltip\"\n    >\n      <img\n        v-if=\"isSvgIcon\"\n        class=\"item-icon icon-svg\"\n        :src=\"svgIconPath\"\n      >\n      <i\n        v-else\n        class=\"item-icon\"\n        :class=\"icon\"\n      />\n      <span\n        class=\"item-label\"\n      >\n        <b>{{ t(data.label.bar) }}:</b>\n      </span>\n      <span\n        class=\"item-value\"\n      >\n        {{ data.value }}\n      </span>\n    </span>\n    <component\n      :is=\"getSubComponent\"\n      v-if=\"subComponent\"\n      :icon=\"icon\"\n      :is-status-bar-item=\"true\"\n    />\n  </div>\n</template>\n\n<style scoped lang=\"scss\">\n.status-bar-item {\n  .item-icon, :deep(.item-icon) {\n    padding-right: 2px;\n    vertical-align: middle;\n    display: none;\n\n    &.icon-svg {\n      width: 14px;\n\n      @media (prefers-color-scheme: dark) {\n          filter: brightness(0) invert(100%) grayscale(1) brightness(2);\n      }\n\n      @media (prefers-color-scheme: light) {\n          filter: brightness(0) grayscale(1) brightness(4);\n      }\n    }\n  }\n\n  @media (max-width: 1000px) {\n    .item-label, :deep(.item-label) {\n      display: none;\n    }\n\n    .item-icon, :deep(.item-icon) {\n      display: inline;\n    }\n  }\n\n  @media (max-width: 900px) {\n    .item-value, :deep(.item-value) {\n      display: none;\n    }\n\n    .item-icon, :deep(.item-icon) {\n      display: inline;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/SystemPreferences.vue",
    "content": "<script>\nimport RdSlider from '@pkg/components/form/RdSlider.vue';\n\nexport default {\n  components: { RdSlider },\n  props:      {\n    // Memory limits\n    memoryInGB: {\n      type:    Number,\n      default: 2,\n    },\n    availMemoryInGB: {\n      type:    Number,\n      default: 0,\n    },\n    minMemoryInGB: {\n      type:    Number,\n      default: 2,\n    },\n    reservedMemoryInGB: {\n      type:    Number,\n      default: 1,\n    },\n    isLockedMemory: {\n      type:    Boolean,\n      default: false,\n    },\n\n    // CPU limits\n    numberCPUs: {\n      type:    Number,\n      default: 2,\n    },\n    availNumCPUs: {\n      type:    Number,\n      default: 0,\n    },\n    minNumCPUs: {\n      type:    Number,\n      default: 2,\n    },\n    reservedNumCPUs: {\n      type:    Number,\n      default: 0,\n    },\n    isLockedCpu: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  computed: {\n    memoryMarks() {\n      return this.makeMarks(this.safeMinMemory, this.availMemoryInGB);\n    },\n    CPUMarks() {\n      return this.makeMarks(this.safeMinCPUs, this.availNumCPUs, 2);\n    },\n    disableMemory() {\n      return this.availMemoryInGB <= this.minMemoryInGB;\n    },\n    disableCPUs() {\n      return this.availNumCPUs <= this.minNumCPUs;\n    },\n    safeMinMemory() {\n      return Math.min(this.minMemoryInGB, this.availMemoryInGB);\n    },\n    safeMinCPUs() {\n      return Math.min(this.minNumCPUs, this.availNumCPUs);\n    },\n    safeMemory() {\n      if (this.memoryInGB < this.safeMinMemory) {\n        return this.safeMinMemory;\n      } else if (this.memoryInGB > this.availMemoryInGB) {\n        return this.availMemoryInGB;\n      } else {\n        return this.memoryInGB;\n      }\n    },\n    safeReservedMemoryInGB() {\n      return Math.min(this.reservedMemoryInGB, this.availMemoryInGB - this.safeMinMemory);\n    },\n    safeCPUs() {\n      if (this.numberCPUs < this.safeMinCPUs) {\n        return this.safeMinCPUs;\n      } else if (this.numberCPUs > this.availNumCPUs) {\n        return this.availNumCPUs;\n      } else {\n        return this.numberCPUs;\n      }\n    },\n  },\n  methods: {\n    processMemory() {\n      // The values here seem to always be in percentage.\n      const percent = x => (x - this.safeMinMemory) * 100 / (this.availMemoryInGB - this.safeMinMemory);\n\n      return [\n        [\n          percent(this.availMemoryInGB - this.safeReservedMemoryInGB),\n          percent(this.availMemoryInGB),\n          {},\n        ],\n      ];\n    },\n    processCPUs() {\n      const percent = x => (x - this.minNumCPUs) * 100 / (this.availNumCPUs - this.minNumCPUs);\n\n      return [\n        [\n          percent(Math.max(0, this.availNumCPUs - this.reservedNumCPUs)),\n          percent(this.availNumCPUs),\n          {},\n        ],\n      ];\n    },\n    updatedVal(value, key = 'memory') {\n      const unit = key === 'memory' ? 'GB' : 'CPUs';\n      let warningMessage = '';\n\n      if (this.hasError(key, value)) {\n        const comparison = (key === 'memory' && value > this.availMemoryInGB) || (key === 'cpu' && value > this.availNumCPUs) ? 'Less' : 'More';\n        const threshold = this.threshold(key, comparison);\n\n        this.$emit('error', key, `${ comparison } than ${ threshold } ${ unit } needs to be allocated to the virtual machine.`);\n\n        return;\n      }\n\n      if (this.hasWarning(key, value)) {\n        warningMessage = `Allocating ${ value } ${ unit } to the virtual machine may cause your host machine to be sluggish.`;\n      }\n\n      this.$emit('warning', key, warningMessage);\n      this.$emit(`update:${ key }`, Number(value));\n    },\n    hasError(key, val) {\n      return (\n        (key === 'memory' && (val > this.availMemoryInGB || val < this.safeMinMemory)) ||\n        (key === 'cpu' && (val > this.availNumCPUs || val < this.safeMinCPUs))\n      );\n    },\n    hasWarning(key, val) {\n      return (\n        (key === 'memory' && val > this.availMemoryInGB - this.safeReservedMemoryInGB) ||\n        (key === 'cpu' && val > this.availNumCPUs - this.reservedNumCPUs)\n      );\n    },\n    threshold(key, val) {\n      if (val === 'Less') {\n        return key === 'memory' ? this.availMemoryInGB : this.availNumCPUs;\n      } else {\n        return key === 'memory' ? this.safeMinMemory : this.safeMinCPUs;\n      }\n    },\n    makeMarks(min, max, mult = 8, steps = 8) {\n      const marks = [...Array(Math.floor(max / mult))]\n        .map((_x, i) => (i + 1) * mult);\n\n      if (!marks.includes(min)) {\n        marks.unshift(min);\n      }\n\n      if (!marks.includes(max)) {\n        marks.push(max);\n      }\n\n      const step = Math.ceil((marks.length - min) / steps);\n\n      return marks\n        .filter((_val, i, arr) => i === 0 || i === arr.length - 1 || !(i % step));\n    },\n  },\n};\n</script>\n\n<template>\n  <div class=\"system-preferences\">\n    <rd-slider\n      id=\"memoryInGBWrapper\"\n      ref=\"memory\"\n      label=\"Memory (GB)\"\n      :value=\"safeMemory\"\n      :min=\"safeMinMemory\"\n      :max=\"availMemoryInGB\"\n      :marks=\"memoryMarks\"\n      :disabled=\"disableMemory\"\n      :process=\"processMemory\"\n      :is-locked=\"isLockedMemory\"\n      @change=\"updatedVal($event, 'memory')\"\n    />\n\n    <rd-slider\n      id=\"numCPUWrapper\"\n      ref=\"cpu\"\n      label=\"# CPUs\"\n      :value=\"safeCPUs\"\n      :min=\"safeMinCPUs\"\n      :max=\"availNumCPUs\"\n      :interval=\"1\"\n      :marks=\"CPUMarks\"\n      :disabled=\"disableCPUs\"\n      :process=\"processCPUs\"\n      :is-locked=\"isLockedCpu\"\n      @change=\"updatedVal($event, 'cpu')\"\n    />\n  </div>\n</template>\n\n<style scoped>\n.system-preferences {\n  display: flex;\n  flex-direction: column;\n  padding-right: 1rem;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Tabbed/RdTabbed.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport Tabbed from '@pkg/components/Tabbed/index.vue';\n\nexport default defineComponent({\n  name:       'rd-tabbed',\n  components: { Tabbed },\n});\n</script>\n\n<template>\n  <tabbed\n    v-bind=\"$attrs\"\n    :use-hash=\"true\"\n    class=\"action-tabs\"\n  >\n    <slot name=\"tabs\" />\n    <template\n      v-if=\"$slots['tab-row-extras']\"\n      #tab-row-extras\n    >\n      <slot name=\"tab-row-extras\" />\n    </template>\n    <slot />\n  </tabbed>\n</template>\n\n<style lang=\"scss\" scoped>\n  .action-tabs {\n    display: flex;\n    flex-direction: column;\n    max-height: 100%;\n\n    :deep(.tabs:focus .tab.active) {\n      text-decoration: none;\n    }\n\n    :deep(.tabs) {\n      border: none;\n      border-bottom: 1px solid var(--border);\n\n      &:focus {\n        outline: none;\n        .tab.active a span {\n          text-decoration: none;\n        }\n      }\n      .tab a:hover {\n        color: var(--link);\n        span {\n          text-decoration: none;\n        }\n      }\n    }\n\n    :deep(.tab-container) {\n      max-height: 100%;\n      overflow: auto;\n      background-color: transparent;\n      &.no-content {\n        border: none;\n      }\n    }\n\n    :deep(li.tab) {\n      margin-right: 0;\n      margin-bottom: -1px;\n      padding-right: 0;\n      border-bottom: 1px solid var(--border);\n\n      &.active {\n        border-color: var(--primary);\n        background-color: transparent;\n\n        a {\n          color: var(--link);\n          text-decoration: none;\n        }\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Tabbed/Tab.vue",
    "content": "<script>\nexport default {\n  inject: ['addTab', 'removeTab', 'sideTabs'],\n\n  props: {\n    label: {\n      default: null,\n      type:    String,\n    },\n    labelKey: {\n      default: null,\n      type:    String,\n    },\n    name: {\n      required: true,\n      type:     String,\n    },\n    tooltip: {\n      default: null,\n      type:    [String, Object],\n    },\n    weight: {\n      default:  0,\n      required: false,\n      type:     Number,\n    },\n    showHeader: {\n      type:    Boolean,\n      default: null, // Default true for side-tabs, false for top.\n    },\n    displayAlertIcon: {\n      type:    Boolean,\n      default: null,\n    },\n    error: {\n      type:    Boolean,\n      default: false,\n    },\n    badge: {\n      default:  0,\n      required: false,\n      type:     Number,\n    },\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  emits: ['active'],\n\n  data() {\n    return { active: null };\n  },\n\n  computed: {\n    labelDisplay() {\n      if ( this.labelKey ) {\n        return this.$store.getters['i18n/t'](this.labelKey);\n      }\n\n      if ( this.label ) {\n        return this.label;\n      }\n\n      return this.name;\n    },\n\n    shouldShowHeader() {\n      if ( this.showHeader !== null ) {\n        return this.showHeader;\n      }\n\n      return this.sideTabs || false;\n    },\n  },\n\n  watch: {\n    active(neu) {\n      if (neu) {\n        this.$emit('active');\n      }\n    },\n  },\n\n  mounted() {\n    this.addTab(this);\n  },\n\n  beforeUnmount() {\n    this.removeTab(this);\n  },\n};\n</script>\n\n<template>\n  <section\n    v-show=\"active\"\n    :id=\"name\"\n    :aria-hidden=\"!active\"\n    role=\"tabpanel\"\n  >\n    <div\n      v-if=\"shouldShowHeader\"\n      class=\"tab-header\"\n    >\n      <h2>\n        {{ labelDisplay }}\n        <i\n          v-if=\"tooltip\"\n          v-clean-tooltip=\"tooltip\"\n          class=\"icon icon-info icon-lg\"\n        />\n      </h2>\n      <slot name=\"tab-header-right\" />\n    </div>\n    <slot v-bind=\"{ active }\" />\n  </section>\n</template>\n\n<style lang=\"scss\" scoped>\n.tab-header {\n  display: flex;\n  justify-content: space-between;\n  margin-bottom: 15px;\n  align-items: center;\n\n  h2 {\n    margin: 0;\n\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Tabbed/index.vue",
    "content": "<script>\nimport findIndex from 'lodash/findIndex';\nimport head from 'lodash/head';\n\nimport { addObject, removeObject, findBy } from '@pkg/utils/array';\nimport { sortBy } from '@pkg/utils/sort';\n\nexport default {\n  name: 'Tabbed',\n\n  emits: ['changed', 'addTab', 'removeTab'],\n\n  props: {\n    defaultTab: {\n      type:    String,\n      default: null,\n    },\n\n    sideTabs: {\n      type:    Boolean,\n      default: false,\n    },\n\n    hideSingleTab: {\n      type:    Boolean,\n      default: false,\n    },\n\n    showTabsAddRemove: {\n      type:    Boolean,\n      default: false,\n    },\n\n    // whether or not to scroll to the top of the new tab on tab change. This is particularly ugly with side tabs\n    scrollOnChange: {\n      type:    Boolean,\n      default: false,\n    },\n\n    useHash: {\n      type:    Boolean,\n      default: true,\n    },\n\n    noContent: {\n      type:    Boolean,\n      default: false,\n    },\n\n    // Remove padding and box-shadow\n    flat: {\n      type:    Boolean,\n      default: false,\n    },\n\n    tabsOnly: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  provide() {\n    const tabs = this.tabs;\n\n    return {\n      sideTabs: this.sideTabs,\n\n      addTab(tab) {\n        const existing = findBy(tabs, 'name', tab.name);\n\n        if ( existing ) {\n          removeObject(tabs, existing);\n        }\n\n        addObject(tabs, tab);\n      },\n\n      removeTab(tab) {\n        removeObject(tabs, tab);\n      },\n    };\n  },\n\n  data() {\n    return {\n      tabs:          [],\n      activeTabName: null,\n    };\n  },\n\n  computed: {\n    // keep the tabs list ordered for dynamic tabs\n    sortedTabs() {\n      return sortBy(this.tabs, ['weight:desc', 'labelDisplay', 'name']);\n    },\n\n    // hide tabs based on tab count IF flag is active\n    hideTabs() {\n      return this.hideSingleTab && this.sortedTabs.length === 1;\n    },\n  },\n\n  watch: {\n    sortedTabs: {\n      handler(tabs) {\n        const {\n          defaultTab,\n          useHash,\n        } = this;\n        const activeTab = tabs.find((t) => t.active);\n\n        const hash = useHash ? this.$route.hash : undefined;\n        const windowHash = useHash ? hash.slice(1) : undefined;\n        const windowHashTabMatch = tabs.find((t) => t.name === windowHash && !t.active);\n        const firstTab = head(tabs) || null;\n\n        if (!activeTab) {\n          if (useHash && windowHashTabMatch) {\n            this.select(windowHashTabMatch.name);\n          } else if (defaultTab && tabs.find((t) => t.name === defaultTab)) {\n            this.select(defaultTab);\n          } else if (firstTab?.name) {\n            this.select(firstTab.name);\n          }\n        }\n      },\n      deep: true,\n    },\n  },\n\n  mounted() {\n    if ( this.useHash ) {\n      window.addEventListener('hashchange', this.hashChange);\n    }\n  },\n\n  unmounted() {\n    if ( this.useHash ) {\n      window.removeEventListener('hashchange', this.hashChange);\n    }\n  },\n\n  methods: {\n    hasIcon(tab) {\n      return tab.displayAlertIcon || (tab.error && !tab.active);\n    },\n    hashChange() {\n      if (!this.scrollOnChange) {\n        const scrollable = document.getElementsByTagName('main')[0];\n\n        if (scrollable) {\n          scrollable.scrollTop = 0;\n        }\n      }\n\n      this.select(this.$route.hash);\n    },\n\n    find(name) {\n      return this.sortedTabs.find((x) => x.name === name );\n    },\n\n    select(name/* , event */) {\n      const { sortedTabs } = this;\n\n      const selected = this.find(name);\n      const hashName = `#${ name }`;\n\n      if ( !selected || selected.disabled) {\n        return;\n      }\n      /**\n       * Exclude logic with URL anchor (hash) for projects without routing logic (vue-router)\n       */\n      if ( this.useHash ) {\n        const currentRoute = this.$router.currentRoute._value;\n        const routeHash = currentRoute.hash;\n\n        if (this.useHash && routeHash !== hashName) {\n          const kurrentRoute = { ...currentRoute };\n\n          kurrentRoute.hash = hashName;\n\n          this.$router.replace(kurrentRoute);\n        }\n      }\n\n      for ( const tab of sortedTabs ) {\n        tab.active = (tab.name === selected.name);\n      }\n\n      this.$emit('changed', { tab: selected, selectedName: selected.name });\n      this.activeTabName = selected.name;\n    },\n\n    selectNext(direction) {\n      const { sortedTabs } = this;\n      const currentIdx = sortedTabs.findIndex((x) => x.active);\n      const nextIdx = getCyclicalIdx(currentIdx, direction, sortedTabs.length);\n      const nextName = sortedTabs[nextIdx].name;\n\n      this.select(nextName);\n\n      this.$nextTick(() => {\n        this.$refs.tablist.focus();\n      });\n\n      function getCyclicalIdx(currentIdx, direction, tabsLength) {\n        const nxt = currentIdx + direction;\n\n        if (nxt >= tabsLength) {\n          return 0;\n        } else if (nxt <= 0) {\n          return tabsLength - 1;\n        } else {\n          return nxt;\n        }\n      }\n    },\n\n    tabAddClicked() {\n      const activeTabIndex = findIndex(this.tabs, (tab) => tab.active);\n\n      this.$emit('addTab', activeTabIndex);\n    },\n\n    tabRemoveClicked() {\n      const activeTabIndex = findIndex(this.tabs, (tab) => tab.active);\n\n      this.$emit('removeTab', activeTabIndex);\n    },\n  },\n};\n</script>\n\n<template>\n  <div\n    :class=\"{ 'side-tabs': !!sideTabs, 'tabs-only': tabsOnly }\"\n    data-testid=\"tabbed\"\n  >\n    <ul\n      v-if=\"!hideTabs\"\n      ref=\"tablist\"\n      role=\"tablist\"\n      class=\"tabs\"\n      :class=\"{ clearfix: !sideTabs, vertical: sideTabs, horizontal: !sideTabs }\"\n      tabindex=\"0\"\n      data-testid=\"tabbed-block\"\n      @keydown.right.prevent=\"selectNext(1)\"\n      @keydown.left.prevent=\"selectNext(-1)\"\n      @keydown.down.prevent=\"selectNext(1)\"\n      @keydown.up.prevent=\"selectNext(-1)\"\n    >\n      <li\n        v-for=\"tab in sortedTabs\"\n        :id=\"tab.name\"\n        :key=\"tab.name\"\n        :data-testid=\"tab.name\"\n        :class=\"{ tab: true, active: tab.active, disabled: tab.disabled, error: (tab.error) }\"\n        role=\"presentation\"\n      >\n        <a\n          :data-testid=\"`btn-${tab.name}`\"\n          :aria-controls=\"'#' + tab.name\"\n          :aria-selected=\"tab.active\"\n          role=\"tab\"\n          @click.prevent=\"select(tab.name, $event)\"\n        >\n          <span>{{ tab.labelDisplay }}</span>\n          <span\n            v-if=\"tab.badge\"\n            class=\"tab-badge\"\n          >{{ tab.badge }}</span>\n          <i\n            v-if=\"hasIcon(tab)\"\n            v-clean-tooltip=\"t('validation.tab')\"\n            class=\"conditions-alert-icon icon-error\"\n          />\n        </a>\n      </li>\n      <li\n        v-if=\"sideTabs && !sortedTabs.length\"\n        class=\"tab disabled\"\n      >\n        <a\n          href=\"#\"\n          @click.prevent\n        >(None)</a>\n      </li>\n      <ul\n        v-if=\"sideTabs && showTabsAddRemove\"\n        class=\"tab-list-footer\"\n      >\n        <li>\n          <button\n            type=\"button\"\n            class=\"btn bg-transparent\"\n            data-testid=\"tab-list-add\"\n            @click=\"tabAddClicked\"\n          >\n            <i class=\"icon icon-plus\" />\n          </button>\n          <button\n            type=\"button\"\n            class=\"btn bg-transparent\"\n            :disabled=\"!sortedTabs.length\"\n            data-testid=\"tab-list-remove\"\n            @click=\"tabRemoveClicked\"\n          >\n            <i class=\"icon icon-minus\" />\n          </button>\n        </li>\n      </ul>\n      <slot name=\"tab-row-extras\" />\n    </ul>\n    <div\n      :class=\"{\n        'tab-container': !!tabs.length || !!sideTabs,\n        'no-content': noContent,\n        'tab-container--flat': !!flat,\n      }\"\n    >\n      <slot />\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.tabs {\n  list-style-type: none;\n  margin: 0;\n  padding: 0;\n\n  &.horizontal {\n    border: solid thin var(--border);\n    border-bottom: 0;\n    display: flex;\n    flex-direction: row;\n\n    + .tab-container {\n      border: solid thin var(--border);\n    }\n\n    .tab.active {\n      border-bottom: solid 2px var(--primary);\n    }\n  }\n\n  &:focus {\n    outline: none;\n\n    & .tab.active a span {\n      text-decoration: underline;\n    }\n  }\n\n  .tab {\n    position: relative;\n    float: left;\n    padding: 0 8px 0 0;\n    cursor: pointer;\n\n    A {\n      display: flex;\n      align-items: center;\n      padding: 10px 15px;\n\n      &:hover {\n        text-decoration: none;\n        span {\n          text-decoration: underline;\n        }\n      }\n    }\n\n    .conditions-alert-icon {\n      color: var(--error);\n      padding-left: 4px;\n    }\n\n    &:last-child {\n      padding-right: 0;\n    }\n\n    &.active {\n      > A {\n        color: var(--primary);\n        text-decoration: none;\n      }\n    }\n\n    &.error {\n      & A > i {\n        color: var(--error);\n      }\n    }\n\n    &.disabled {\n      opacity: 0.5;\n      cursor: not-allowed;\n\n      A {\n        pointer-events: none;\n      }\n    }\n\n    .tab-badge {\n      margin-left: 5px;\n      background-color: var(--link);\n      color: #fff;\n      border-radius: 6px;\n      padding: 1px 7px;\n      font-size: 11px;\n    }\n  }\n}\n\n.tab-container {\n  padding: 20px;\n\n  &.no-content {\n    padding: 0 0 3px 0;\n  }\n\n  // Example case: Tabbed component within a tabbed component\n  &--flat {\n    padding: 0;\n\n    .side-tabs {\n      box-shadow: unset;\n    }\n  }\n}\n\n.tabs-only {\n  margin-bottom: 20px;\n\n  .tab-container {\n    display: none;\n  }\n\n  .tabs {\n    border: 0;\n    border-bottom: 2px solid var(--border);\n  }\n}\n\n.side-tabs {\n  display: flex;\n  box-shadow: 0 0 20px var(--shadow);\n  border-radius: calc(var(--border-radius) * 2);\n  background-color: var(--tabbed-sidebar-bg);\n\n  .tab-container {\n    padding: 20px;\n  }\n\n  & .tabs {\n    width: $sideways-tabs-width;\n    min-width: $sideways-tabs-width;\n    display: flex;\n    flex: 1 0;\n    flex-direction: column;\n\n    // &.vertical {\n    //   .tab.active {\n    //     background-color: var(--tabbed-container-bg);\n    //   }\n    // }\n\n    & .tab {\n      width: 100%;\n      border-left: solid 5px transparent;\n\n      &.toggle A {\n        color: var(--primary);\n      }\n\n      A {\n        color: var(--primary);\n      }\n\n      &.active {\n        background-color: var(--body-bg);\n        border-left: solid 5px var(--primary);\n\n        & A {\n          color: var(--input-label);\n        }\n      }\n\n      &.disabled {\n        background-color: var(--disabled-bg);\n\n        & A {\n          color: var(--disabled-text);\n          text-decoration: none;\n        }\n      }\n    }\n    .tab-list-footer {\n      list-style: none;\n      padding: 0;\n      margin-top: auto;\n\n      li {\n        display: flex;\n        flex: 1;\n\n        .btn {\n          flex: 1 1;\n          display: flex;\n          justify-content: center;\n        }\n\n        button:first-of-type {\n          border-top: solid 1px var(--border);\n          border-right: solid 1px var(--border);\n          border-top-right-radius: 0;\n        }\n        button:last-of-type {\n          border-top: solid 1px var(--border);\n          border-top-left-radius: 0;\n        }\n      }\n    }\n  }\n\n  &\n\n  .tab-container {\n    width: calc(100% - #{$sideways-tabs-width});\n    flex-grow: 1;\n    background-color: var(--body-bg);\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/TelemetryOptIn.vue",
    "content": "<script>\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\n\nexport default {\n  components: { RdCheckbox },\n  props:      {\n    // misc\n    telemetry: {\n      type:    Boolean,\n      default: false,\n    },\n    isTelemetryLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  methods: {\n    toggleTelemetry(value) {\n      this.$emit('updateTelemetry', value);\n    },\n  },\n};\n</script>\n\n<template>\n  <div class=\"system-preferences\">\n    <div class=\"checkbox\">\n      <rd-checkbox\n        :value=\"telemetry\"\n        :is-locked=\"isTelemetryLocked\"\n        label=\"Allow collection of anonymous statistics to help us improve Rancher Desktop\"\n        @update:value=\"toggleTelemetry\"\n      />\n      <p class=\"fineprint\">\n        Send anonymized usage info, error reports, etc. to help improve Rancher Desktop. Your data will not be shared with anyone else, and no information about what specific resources or endpoints you are deploying is included.\n      </p>\n    </div>\n  </div>\n</template>\n\n<style scoped>\np.fineprint {\n  font-size: small;\n  margin-top: 2px;\n  color: var(--input-label);\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/TheTitle.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent, markRaw } from 'vue';\nimport { mapState } from 'vuex';\n\nconst componentCache: Record<string, any> = {};\n\nexport default defineComponent({\n  name: 'the-title',\n  data() {\n    return {\n      isChild:          false,\n      dynamicComponent: null,\n    };\n  },\n  computed: {\n    ...mapState<any, any>(\n      'page',\n      [\n        'title',\n        'description',\n        'action',\n        'icon',\n      ]),\n  },\n  watch: {\n    $route: {\n      immediate: true,\n      handler(current) {\n        this.isChild = current.path.lastIndexOf('/') > 0;\n      },\n    },\n    action: {\n      async handler(componentName) {\n        if (componentName) {\n          componentCache[componentName] ||= (await import(`@pkg/components/${ componentName }.vue`)).default;\n          this.dynamicComponent = markRaw(componentCache[componentName]);\n        }\n      },\n      immediate: true,\n    },\n  },\n  methods: {\n    routeBack() {\n      this.$router.back();\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"title\">\n    <div class=\"title-top\">\n      <transition-group name=\"fade-group\">\n        <button\n          v-if=\"isChild\"\n          key=\"back-btn\"\n          data-test=\"back-btn\"\n          class=\"btn role-link btn-sm btn-back fade-group-item\"\n          type=\"button\"\n          @click=\"routeBack\"\n        >\n          <span\n            class=\"icon icon-chevron-left\"\n          />\n        </button>\n        <h1\n          key=\"mainTitle\"\n          data-test=\"mainTitle\"\n          class=\"fade-group-item\"\n          :class=\"icon ? 'main-title-icon' : ''\"\n        >\n          <span\n            v-if=\"icon\"\n            key=\"mainTitleIcon\"\n            :class=\"icon\"\n          />\n          {{ title }}\n        </h1>\n      </transition-group>\n      <transition\n        name=\"fade\"\n        appear\n      >\n        <div\n          v-if=\"action\"\n          key=\"actions\"\n          class=\"actions fade-actions\"\n        >\n          <component :is=\"dynamicComponent\" />\n        </div>\n      </transition>\n    </div>\n    <hr>\n    <div\n      v-show=\"description\"\n      class=\"description\"\n    >\n      {{ description }}\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .title {\n    padding: 20px 20px 0 20px;\n  }\n\n  .title-top{\n    display: flex;\n  }\n\n  .btn-back {\n    height: 27px;\n    font-weight: bolder;\n    font-size: 1.5em;\n  }\n\n  .btn-back:focus {\n    outline: none;\n    box-shadow: none;\n    background: var(--input-focus-bg);\n  }\n\n  .fade-group-item {\n    display: inherit;\n    transition: all 0.25s ease-out;\n  }\n\n  .fade-actions {\n    transition: opacity 0.25s ease-out;\n  }\n\n  .fade-group-item-enter-from, .fade-group-leave-to\n  {\n    opacity: 0;\n  }\n\n  .fade-group-leave-active, .fade-group-item-enter-active {\n    position: absolute;\n  }\n\n  .fade-enter, .fade-leave-to {\n    opacity: 0;\n  }\n\n  .fade-active {\n    transition: all 0.25s ease-in;\n  }\n\n  .main-title-icon {\n    display: flex;\n    gap: 0.5rem;\n\n    span {\n      font-size: 30px;\n      color: var(--primary);\n    }\n  }\n\n  .actions {\n    margin-left: auto;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/TroubleshootingLineItem.vue",
    "content": "<script lang=\"ts\">\nimport { Card } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nexport default defineComponent({\n  name:       'troubleshooting-line-item',\n  components: { Card },\n});\n</script>\n<template>\n  <card\n    class=\"troubleshooting-line-item\"\n    :show-highlight-border=\"false\"\n  >\n    <template #title>\n      <slot name=\"title\" />\n    </template>\n    <template #body>\n      <slot name=\"description\" />\n      <span class=\"options\">\n        <slot name=\"options\" />\n      </span>\n    </template>\n    <template #actions>\n      <slot name=\"actions\" />\n    </template>\n  </card>\n</template>\n\n<style lang=\"scss\" scoped>\n  .options {\n    margin-top: 0.5rem;\n  }\n\n  // Override card styles from @rancher/components, we can remove this once the component gets refactor.\n  .card-container {\n    border-radius: 0;\n    box-shadow: none;\n    margin: 0;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/UpdateStatus.vue",
    "content": "<template>\n  <div>\n    <div class=\"version\">\n      <version />\n      <rd-checkbox\n        v-if=\"updatePossible\"\n        v-model:value=\"updatesEnabled\"\n        class=\"updatesEnabled\"\n        label=\"Check for updates automatically\"\n        :is-locked=\"autoUpdateLocked\"\n      />\n    </div>\n    <card\n      v-if=\"hasUpdate\"\n      ref=\"updateInfo\"\n      :show-highlight-border=\"false\"\n    >\n      <template #title>\n        <div class=\"type-title\">\n          <h3>Update Available</h3>\n        </div>\n      </template>\n      <template #body>\n        <div ref=\"updateStatus\">\n          <p>\n            {{ statusMessage }}\n          </p>\n          <p\n            v-if=\"updateReady\"\n            class=\"update-notification\"\n          >\n            Restart the application to apply the update.\n          </p>\n        </div>\n        <details\n          v-if=\"detailsMessage\"\n          class=\"release-notes\"\n        >\n          <summary>Release Notes</summary>\n          <div\n            ref=\"releaseNotes\"\n            v-html=\"detailsMessage\"\n          />\n        </details>\n      </template>\n      <template #actions>\n        <button\n          v-if=\"updateReady\"\n          ref=\"applyButton\"\n          class=\"btn role-secondary\"\n          :disabled=\"applying\"\n          @click=\"applyUpdate\"\n        >\n          {{ applyMessage }}\n        </button>\n        <span v-else />\n      </template>\n    </card>\n    <card\n      v-else-if=\"unsupportedUpdateAvailable\"\n      :show-highlight-border=\"false\"\n    >\n      <template #title>\n        <div class=\"type-title\">\n          <h3>Latest Version Not Supported</h3>\n        </div>\n      </template>\n      <template #body>\n        <p>\n          A newer version of Rancher Desktop is available, but not supported on your system.\n        </p>\n        <br>\n        <p>\n          For more information please see\n          <a href=\"https://docs.rancherdesktop.io/getting-started/installation\">the installation documentation</a>.\n        </p>\n      </template>\n      <template #actions>\n        <div />\n      </template>\n    </card>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport * as Components from '@rancher/components';\nimport DOMPurify from 'dompurify';\nimport { marked } from 'marked';\nimport { defineComponent } from 'vue';\n\nimport Version from '@pkg/components/Version.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport { UpdateState } from '@pkg/main/update';\n\nimport type { PropType } from 'vue';\n\nconst { Card } = (Components as any).default ?? Components;\n\nexport default defineComponent({\n  name:       'update-status',\n  components: {\n    Version, Card, RdCheckbox,\n  },\n\n  props: {\n    enabled: {\n      type:    Boolean,\n      default: false,\n    },\n    updateState: {\n      type:    Object as PropType<UpdateState | null>,\n      default: null,\n    },\n    locale: {\n      type:    String,\n      default: undefined,\n    },\n    isAutoUpdateLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  data() {\n    return { applying: false };\n  },\n\n  computed: {\n    updatesEnabled: {\n      get(): boolean {\n        return this.enabled;\n      },\n      set(value: boolean) {\n        // We emit an event, but _don't_ set the prop here; we let the containing\n        // page update our prop instead.\n        this.$emit('enabled', value);\n      },\n    },\n\n    updatePossible(): boolean {\n      return !!this.updateState?.configured;\n    },\n\n    hasUpdate(): boolean {\n      return this.updatesEnabled && !!this.updateState?.available;\n    },\n\n    updateReady(): boolean {\n      return this.hasUpdate && !!this.updateState?.downloaded && !this.updateState?.error;\n    },\n\n    statusMessage(): string {\n      if (this.updateState?.error) {\n        return 'There was an error checking for updates.';\n      }\n      if (!this.updateState?.info) {\n        return '';\n      }\n\n      const { info, progress } = this.updateState;\n      const prefix = `An update to version ${ info.version } is available`;\n\n      if (!progress) {\n        return `${ prefix }.`;\n      }\n\n      const percent = Math.floor(progress.percent);\n      const speed = Intl.NumberFormat(this.locale, {\n        style:       'unit',\n        unit:        'byte-per-second',\n        unitDisplay: 'narrow',\n        notation:    'compact',\n      }).format(progress.bytesPerSecond);\n\n      return `${ prefix }; downloading... (${ percent }%, ${ speed })`;\n    },\n\n    detailsMessage(): string | undefined {\n      const markdown = this.updateState?.info?.releaseNotes;\n\n      if (typeof markdown !== 'string') {\n        return undefined;\n      }\n      // Here's the explanation of the following unorthodox typecast:\n      // The signature of `marked.marked` is, with version 11:\n      // marked(src: string, options?: MarkedOptions): string | Promise<string>\n      // It returns a Promise<string> if `options.async` is true; otherwise, a string.\n      const unsanitized = marked(markdown) as string;\n\n      return DOMPurify.sanitize(unsanitized, { USE_PROFILES: { html: true } });\n    },\n\n    applyMessage(): string {\n      return this.applying ? 'Applying update...' : 'Restart Now';\n    },\n\n    unsupportedUpdateAvailable(): boolean {\n      return !this.hasUpdate && !!this.updateState?.info?.unsupportedUpdateAvailable;\n    },\n\n    autoUpdateLocked(): boolean {\n      return this.isAutoUpdateLocked;\n    },\n  },\n\n  methods: {\n    applyUpdate() {\n      this.applying = true;\n      this.$emit('apply');\n    },\n  },\n});\n\n</script>\n\n<style lang=\"scss\" scoped>\n  .version {\n    display: flex;\n    justify-content: space-between\n  }\n  .update-notification {\n    font-weight: 900;\n  }\n  .release-notes > summary {\n    margin: 1em;\n  }\n  .release-notes > div {\n    margin-left: 2em;\n    margin-right: 1em;\n  }\n</style>\n\n<style lang=\"scss\">\n  .release-notes p {\n    margin: 1em 0px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/Version.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nexport default defineComponent({\n  name:  'version',\n  props: {\n    icon: {\n      type:    String,\n      default: '',\n    },\n    isStatusBarItem: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  data() {\n    return { version: this.t('product.versionChecking') };\n  },\n  computed: {\n    getTooltip() {\n      return {\n        content:     `<b>${ this.t('product.version') }</b>: ${ this.version }`,\n        html:        true,\n        placement:   'top',\n        popperClass: 'tooltip-footer',\n      };\n    },\n  },\n  mounted() {\n    ipcRenderer.on('get-app-version', (event, version) => {\n      this.version = version;\n    });\n    ipcRenderer.send('get-app-version');\n  },\n});\n</script>\n\n<template>\n  <span\n    v-clean-tooltip=\"isStatusBarItem ? getTooltip : {}\"\n    class=\"versionInfo\"\n  >\n    <i\n      v-if=\"icon\"\n      class=\"item-icon\"\n      :class=\"icon\"\n    />\n    <span\n      class=\"item-label\"\n    >\n      <b>{{ t('product.version') }}:</b>\n    </span>\n    <span\n      class=\"item-value\"\n    >\n      {{ version }}\n    </span>\n  </span>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/WSLIntegration.vue",
    "content": "<template>\n  <section class=\"wsl-integrations\">\n    <h3\n      v-if=\"description\"\n      v-text=\"description\"\n    />\n    <div\n      v-for=\"item of integrationsList\"\n      :key=\"item.name\"\n      :data-test=\"`item-${item.name}`\"\n    >\n      <checkbox\n        :value=\"item.value\"\n        :label=\"item.name\"\n        :disabled=\"item.disabled\"\n        :description=\"item.description\"\n        @update:value=\"toggleIntegration(item.name, $event)\"\n      />\n    </div>\n  </section>\n</template>\n\n<script lang=\"ts\">\nimport { Checkbox } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport type { PropType } from 'vue';\n\nexport default defineComponent({\n  name:       'wsl-integration',\n  components: { Checkbox },\n\n  props: {\n    title: {\n      type:    String,\n      default: 'System Integration',\n    },\n    description: {\n      type:    String,\n      default: '',\n    },\n    integrations: {\n      type:    Object as PropType<Record<string, boolean | string>>,\n      default: () => ({} as Record<string, boolean | string>),\n    },\n  },\n\n  data() {\n    return {\n      name: 'wsl-integration',\n      /**\n       * A mapping to temporarily disable a selection while work happens\n       * asynchronously, to prevent the user from retrying to toggle too quickly.\n       */\n      busy: {} as Record<string, boolean>,\n    };\n  },\n\n  computed: {\n    integrationsList() {\n      const results: { name: string, value: boolean, disabled: boolean, description: string }[] = [];\n\n      for (const [name, value] of Object.entries(this.integrations)) {\n        if (typeof value === 'boolean') {\n          if (value === this.busy[name]) {\n            this.$delete(this.busy, name);\n          }\n          results.push({\n            name, value, disabled: name in this.busy, description: '',\n          });\n        } else {\n          results.push({\n            name, value: false, disabled: true, description: value,\n          });\n          this.$delete(this.busy, name);\n        }\n      }\n\n      return results.sort((x, y) => x.name.localeCompare(y.name));\n    },\n  },\n\n  methods: {\n    toggleIntegration(name: string, value: boolean) {\n      this.busy.name = value;\n      this.$emit('integration-set', name, value);\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .wsl-integrations {\n    display: flex;\n    flex-direction: column;\n    gap: 1rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/__tests__/BackendProgress.spec.ts",
    "content": "import { jest } from '@jest/globals';\nimport { shallowMount } from '@vue/test-utils';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\ninterface Progress {\n  current:         number;\n  max:             number;\n  description?:    string;\n  transitionTime?: Date;\n}\n\nfunction wrap(props: Record<string, any>) {\n  return shallowMount(BackendProgress, { propsData: props });\n}\n\nconst progress: Progress = { current: 0, max: 0 };\nlet callback: (event: Event | undefined, progress: Progress) => void = () => {};\n\nmockModules({\n  '@pkg/utils/ipcRenderer': {\n    ipcRenderer: {\n      on(name: string, cb: typeof callback) {\n        expect(name).toEqual('k8s-progress');\n        callback = cb;\n      },\n      invoke(name: string) {\n        expect(name).toEqual('k8s-progress');\n        return Promise.resolve(progress);\n      },\n    },\n  },\n});\n\nconst { default: BackendProgress } = await import('../BackendProgress.vue');\n\ndescribe('BackendProgress', () => {\n  beforeAll(() => {\n    jest.useFakeTimers();\n  });\n  describe('progress', () => {\n    describe('elapsed with max', () => {\n      const testCases: [number, number, string][] = [\n        [0, 100, '100 left'],\n        [512, 1_024, '0.5K left'],\n        [513, 1_024, '511 left'],\n        [32_768, 65_536, '32K left'],\n        [158_334_976, 681_574_400, '499M left'],\n        [0, 681_574_400, '0.6G left'], // 650 MiB\n        [681_574_400, 25_025_314_816, '23G left'],\n        [25_025_314_816, 2_199_023_255_552, '2T left'],\n        [0, 4_503_599_627_370_496, '4096T left'], // Past the scale\n      ];\n      for (const [current, max, expected] of testCases) {\n        it(`should show ${ current }/${ max } as ${ expected }`, async() => {\n          const wrapper = wrap({});\n\n          Object.assign(progress, { current, max, transitionTime: new Date() });\n          callback?.(undefined, progress);\n          jest.advanceTimersByTime(1_000); // We delay rendering by half a second\n          await wrapper.vm.$nextTick(); // Force Vue to update\n          expect(wrapper.get('.duration').text()).toEqual(expected);\n        });\n      }\n    });\n    describe('elapsed duration', () => {\n      const testCases: [number, string][] = [\n        [1, '1s'],\n        [59, '59s'],\n        [60, '1m'],\n        [60 * 60 - 1, '59m59s'],\n        [60 * 60, '1h'],\n        [2 * 60 * 60 + 42, '2h42s'], // Skip zero in the middle\n        [24 * 60 * 60 - 1, '23h59m59s'],\n        [24 * 60 * 60, '1d'],\n        [30 * 24 * 60 * 60, '30d'], // Don't have units past days\n        [366 * 24 * 60 * 60, '366d'],\n      ];\n      for (const [duration, expected] of testCases) {\n        it(`should show ${ duration } as ${ expected }`, async() => {\n          const wrapper = wrap({});\n\n          // We need transitionTime to be non-zero; so we start at time=1s, and\n          // mock Date.now() to be duration + 1 second.\n          Object.assign(progress, { max: -1, transitionTime: 1 });\n          jest.spyOn(Date, 'now').mockReturnValue((duration + 1) * 1_000);\n          callback?.(undefined, progress);\n          jest.advanceTimersByTime(1_000); // We delay rendering by half a second\n          await wrapper.vm.$nextTick(); // Force Vue to update\n          expect(wrapper.get('.duration').text()).toEqual(expected);\n        });\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/__tests__/PreferencesButton.spec.ts",
    "content": "import { shallowMount } from '@vue/test-utils';\n\nimport PreferencesButton from '../Preferences/ButtonOpen.vue';\n\ndescribe('Preferences/ButtonOpen.vue', () => {\n  it(`renders a button`, () => {\n    const wrapper = shallowMount(PreferencesButton);\n\n    expect(wrapper.find('button').classes()).toStrictEqual(['btn', 'role-secondary', 'btn-icon-text']);\n  });\n\n  it(`emits 'open-preferences' on click`, async() => {\n    const wrapper = shallowMount(PreferencesButton);\n\n    await wrapper.get('button').trigger('click');\n\n    expect(wrapper.emitted('open-preferences')).toHaveLength(1);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/__tests__/StatusBar.spec.ts",
    "content": "import { jest } from '@jest/globals';\nimport { shallowMount, VueWrapper } from '@vue/test-utils';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nmockModules({ electron: undefined });\n\nconst { default: StatusBar } = await import('@pkg/components/StatusBar.vue');\nconst { default: StatusBarItem } = await import('@pkg/components/StatusBarItem.vue');\n\ndescribe('StatusBar.vue', () => {\n  let wrapper: VueWrapper<InstanceType<typeof StatusBar>>;\n\n  beforeEach(() => {\n    wrapper = shallowMount(StatusBar, {\n      computed: {\n        ...StatusBar.computed,\n        getPreferences: jest.fn().mockReturnValue({\n          kubernetes:      { version: '1.27.7', enabled: true },\n          containerEngine: { name: 'containerd' },\n        }),\n      },\n      mocks: { t: jest.fn() },\n    });\n  });\n\n  it('contains four items', () => {\n    expect(wrapper.findAllComponents(StatusBarItem).length).toBe(4);\n  });\n\n  it('should contain Rancher Desktop version item', () => {\n    const props = wrapper.getComponent({ ref: 'version' }).props();\n\n    expect(props.data).toBeFalsy();\n    expect(props.icon).toBeTruthy();\n    expect(props.subComponent).toBe('Version');\n  });\n\n  it('should contain network status item', () => {\n    const props = wrapper.getComponent({ ref: 'network' }).props();\n\n    expect(props.data).toBeFalsy();\n    expect(props.icon).toBeTruthy();\n    expect(props.subComponent).toBe('NetworkStatus');\n  });\n\n  it('should contain kubernetes version item', () => {\n    const props = wrapper.getComponent({ ref: 'kubernetesVersion' }).props();\n\n    expect(props.data.label.bar).toBe('product.kubernetesVersion');\n    expect(props.data.value).toBe('1.27.7');\n    expect(props.icon).toBeTruthy();\n    expect(props.subComponent).toBeFalsy();\n  });\n\n  it('should contain container engine item', () => {\n    const props = wrapper.getComponent({ ref: 'containerEngine' }).props();\n\n    expect(props.data.label.bar).toBe('product.containerEngine.abbreviation');\n    expect(props.data.value).toBe('containerd');\n    expect(props.icon).toBeTruthy();\n    expect(props.subComponent).toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/__tests__/SystemPreferences.spec.js",
    "content": "import { mount } from '@vue/test-utils';\nimport deepmerge from 'deepmerge';\n\nimport SystemPreferences from '../SystemPreferences.vue';\n\nfunction createWrappedPage(props) {\n  return mount(SystemPreferences, {\n    props,\n    global: {\n      directives: { tooltip: {} },\n    },\n  });\n}\n\nconst baseProps = {\n  memoryInGB:         4,\n  numberCPUs:         5,\n  availMemoryInGB:    8,\n  availNumCPUs:       6,\n  minMemoryInGB:      2,\n  minNumCPUs:         1,\n  reservedMemoryInGB: 3,\n  reservedNumCPUs:    1,\n};\n\ndescribe('SystemPreferences.vue', () => {\n  it('accepts valid data', () => {\n    const wrapper = createWrappedPage(baseProps);\n\n    expect(wrapper.props().memoryInGB).toBe(4);\n    expect(wrapper.props().numberCPUs).toBe(5);\n    expect(wrapper.props().availMemoryInGB).toBe(8);\n    expect(wrapper.props().availNumCPUs).toBe(6);\n\n    const slider1 = wrapper.find('#memoryInGBWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider1.exists()).toBeFalsy();\n    const slider2 = wrapper.find('#numCPUWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider2.exists()).toBeFalsy();\n\n    const div1 = wrapper.find('#memoryInGBWrapper');\n    const span1 = div1.find('div.vue-slider div.vue-slider-dot');\n\n    expect(span1.exists()).toBeTruthy();\n    expect(span1.attributes('aria-valuemin')).toEqual('2');\n    expect(span1.attributes('aria-valuenow')).toEqual('4');\n    expect(span1.attributes('aria-valuemax')).toEqual('8');\n\n    const div2 = wrapper.find('#numCPUWrapper');\n    const span2 = div2.find('div.vue-slider div.vue-slider-dot');\n\n    expect(span2.exists()).toBeTruthy();\n    expect(span2.attributes('aria-valuemin')).toEqual('1');\n    expect(span2.attributes('aria-valuenow')).toEqual('5');\n    expect(span2.attributes('aria-valuemax')).toEqual('6');\n    expect(span2.attributes('aria-valuemin')).toEqual('1');\n    expect(span2.attributes('aria-valuenow')).toEqual('5');\n    expect(span2.attributes('aria-valuemax')).toEqual('6');\n  });\n\n  it('sets correct defaults and is enabled', () => {\n    const minimalProps = deepmerge(baseProps, {});\n\n    delete minimalProps.memoryInGB;\n    delete minimalProps.numberCPUs;\n    const wrapper = createWrappedPage(minimalProps);\n\n    expect(wrapper.props().memoryInGB).toBe(2);\n    expect(wrapper.props().numberCPUs).toBe(2);\n    const slider1 = wrapper.find('#memoryInGBWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider1.exists()).toBeFalsy();\n    const slider2 = wrapper.find('#numCPUWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider2.exists()).toBeFalsy();\n\n    const div1 = wrapper.find('#memoryInGBWrapper');\n    const span1 = div1.find('div.vue-slider div.vue-slider-dot');\n\n    expect(span1.exists()).toBe(true);\n    expect(span1.attributes('aria-valuemin')).toEqual('2');\n    expect(span1.attributes('aria-valuenow')).toEqual('2');\n    expect(span1.attributes('aria-valuemax')).toEqual('8');\n\n    const div2 = wrapper.find('#numCPUWrapper');\n    const span2 = div2.find('div.vue-slider div.vue-slider-dot');\n\n    expect(span2.exists()).toBe(true);\n    expect(span2.attributes('aria-valuemin')).toEqual('1');\n    expect(span2.attributes('aria-valuenow')).toEqual('2');\n    expect(span2.attributes('aria-valuemax')).toEqual('6');\n  });\n\n  // Note that k8s.vue should adjust these values so we don't see this\n  it('disables widgets when no options are possible', () => {\n    const minimalProps = {\n      memoryInGB:      4,\n      numberCPUs:      1,\n      availMemoryInGB: 2,\n      availNumCPUs:    1,\n    };\n    const wrapper = createWrappedPage(minimalProps);\n    const slider1 = wrapper.find('#memoryInGBWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider1.exists()).toBeTruthy();\n    expect(slider1.find('div.vue-slider-rail div.vue-slider-dot.vue-slider-dot-disabled').exists()).toBeTruthy();\n\n    const slider2 = wrapper.find('#numCPUWrapper div.vue-slider.vue-slider-disabled');\n\n    expect(slider2.exists()).toBeTruthy();\n    expect(slider2.find('div.vue-slider-rail div.vue-slider-dot.vue-slider-dot-disabled').exists()).toBeTruthy();\n  });\n\n  it('marks reserved resources', () => {\n    const wrapper = createWrappedPage(baseProps);\n    const memory = wrapper.findComponent({ ref: 'memory' });\n\n    // min 2 reserved 3 total 8, so total width = 6, marked section is 50% to 100%\n    expect(memory.find('.vue-slider-process').element.style.left).toEqual('50%');\n    expect(memory.find('.vue-slider-process').element.style.width).toEqual('50%');\n    const cpu = wrapper.findComponent({ ref: 'cpu' });\n\n    // min 1 reserved 1 total 6, so total width = 5, marked section is 80% to 100%\n    expect(cpu.find('.vue-slider-process').element.style.left).toEqual('80%');\n    expect(cpu.find('.vue-slider-process').element.style.width).toEqual('20%');\n  });\n\n  describe('throw on console.error', () => {\n    class VueSliderError extends Error {\n    }\n\n    let origError;\n\n    beforeAll(() => {\n      origError = console.error;\n      console.error = (...args) => {\n        throw new VueSliderError(...args);\n      };\n    });\n    afterAll(() => {\n      console.error = origError;\n    });\n\n    const checkForError = (func, expectedMessage) => {\n      expect(func).toThrow(new VueSliderError(expectedMessage));\n    };\n\n    it('the sliders detect invalid values', async() => {\n      const wrapper = createWrappedPage(baseProps);\n\n      const div1 = wrapper.getComponent('#memoryInGBWrapper');\n      const slider1 = div1.getComponent({ ref: 'slider' });\n      const span1 = slider1.get('div.vue-slider-dot');\n      const slider1vm = slider1.vm;\n\n      for (let i = 2; i <= baseProps.availMemoryInGB; i++) {\n        await slider1vm.setValue(i);\n        expect(span1.attributes('aria-valuenow')).toEqual(i.toString());\n        expect(slider1vm.getValue()).toBe(i);\n      }\n      checkForError(() => {\n        slider1vm.setValue(1);\n      }, '[VueSlider error]: The \"value\" must be greater than or equal to the \"min\".');\n\n      checkForError(() => {\n        slider1vm.setValue(baseProps.availMemoryInGB + 1);\n      }, '[VueSlider error]: The \"value\" must be less than or equal to the \"max\".');\n\n      const div2 = wrapper.getComponent('#numCPUWrapper');\n      const slider2 = div2.getComponent({ ref: 'slider' });\n      const slider2vm = slider2.vm;\n      const span2 = slider2.get('div.vue-slider-dot');\n\n      for (let i = 1; i <= baseProps.availNumCPUs; i++) {\n        await slider2vm.setValue(i);\n        expect(span2.attributes('aria-valuenow')).toEqual(i.toString());\n        expect(slider2vm.getValue()).toBe(i);\n      }\n\n      checkForError(() => {\n        slider2vm.setValue(0);\n      }, '[VueSlider error]: The \"value\" must be greater than or equal to the \"min\".');\n\n      checkForError(() => {\n        slider2vm.setValue(baseProps.availNumCPUs + 1);\n      }, '[VueSlider error]: The \"value\" must be less than or equal to the \"max\".');\n    });\n  });\n\n  it('emits events', async() => {\n    const wrapper = createWrappedPage(baseProps);\n\n    const div1 = wrapper.getComponent('#memoryInGBWrapper');\n    const slider1 = div1.getComponent({ ref: 'slider' });\n    const slider1vm = slider1.vm;\n\n    await slider1vm.setValue(3);\n    const updateMemoryEmitter = wrapper.emitted()['update:memory'];\n\n    expect(updateMemoryEmitter).toBeTruthy();\n    expect(updateMemoryEmitter.length).toBe(1);\n    expect(updateMemoryEmitter[0]).toEqual([3]);\n    await slider1vm.setValue(5);\n    expect(updateMemoryEmitter.length).toBe(2);\n    expect(updateMemoryEmitter[0]).toEqual([3]);\n    expect(updateMemoryEmitter[1]).toEqual([5]);\n\n    const div2 = wrapper.getComponent('#numCPUWrapper');\n    const slider2 = div2.getComponent({ ref: 'slider' });\n    const slider2vm = slider2.vm;\n\n    await slider2vm.setValue(2);\n    const updateCPUEmitter = wrapper.emitted()['update:cpu'];\n\n    expect(updateCPUEmitter).toBeTruthy();\n    expect(updateCPUEmitter.length).toBe(1);\n    expect(updateCPUEmitter[0]).toEqual([2]);\n    await slider2vm.setValue(4);\n    expect(updateCPUEmitter.length).toBe(2);\n    expect(updateCPUEmitter[0]).toEqual([2]);\n    expect(updateCPUEmitter[1]).toEqual([4]);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/__tests__/UpdateStatus.spec.ts",
    "content": "import { jest } from '@jest/globals';\nimport { mount } from '@vue/test-utils';\nimport FloatingVue from 'floating-vue';\n\nimport type { UpdateState } from '@pkg/main/update';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nmockModules({\n  '@pkg/utils/ipcRenderer': {\n    ipcRenderer: {\n      on:   jest.fn(),\n      send: jest.fn(),\n    },\n  },\n  electron: undefined,\n});\n\nconst { default: UpdateStatus } = await import('../UpdateStatus.vue');\n\nfunction wrap(props: typeof UpdateStatus['$props']) {\n  return mount(UpdateStatus, {\n    props,\n    global: {\n      mocks:   { t: jest.fn() },\n      stubs:   {\n        T:          { template: '<span> {{ k }} </span>' },\n        RdCheckbox: { template: '<input type=\"checkbox\">' },\n        Version:    { template: '<span />' },\n      },\n    },\n    plugins: [FloatingVue],\n  });\n}\n\ndescribe('UpdateStatus.vue', () => {\n  describe('update visibility', () => {\n    it('shows updates when available', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: { available: true, downloaded: true },\n      });\n\n      expect(wrapper.findComponent({ ref: 'updateInfo' }).exists()).toBeTruthy();\n    });\n\n    it('hides updates when disabled', () => {\n      const wrapper = wrap({\n        enabled:     false,\n        updateState: { available: true, downloaded: true } as UpdateState,\n      });\n\n      expect(wrapper.findComponent({ ref: 'updateInfo' }).exists()).toBeFalsy();\n    });\n\n    it('hides when no updates are available', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: { available: false, downloaded: false } as UpdateState,\n      });\n\n      expect(wrapper.findComponent({ ref: 'updateInfo' }).exists()).toBeFalsy();\n    });\n  });\n\n  describe('update status', () => {\n    it('displays error correctly', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true, error: new Error('hello'), downloaded: true,\n        } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'updateStatus' }).text())\n        .toEqual('There was an error checking for updates.');\n      expect(wrapper.element.querySelector('.update-notification'))\n        .toBeFalsy();\n    });\n\n    it('hides when there is nothing to display', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: { available: true } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'updateStatus' }).text())\n        .toEqual('');\n    });\n\n    it('shows when an update is available', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true, downloaded: true, info: { version: 'v1.2.3' },\n        } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'updateStatus' }).text().replace(/\\s+/g, ' '))\n        .toEqual('An update to version v1.2.3 is available. Restart the application to apply the update.');\n\n      expect(wrapper.get({ ref: 'applyButton' }).attributes()).not.toHaveProperty('disabled');\n    });\n\n    it('does not allow applying again', async() => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true, downloaded: true, info: { version: 'v1.2.3' },\n        } as UpdateState,\n      });\n\n      await wrapper.setData({ applying: true });\n      expect(wrapper.get({ ref: 'applyButton' }).attributes()).toHaveProperty('disabled');\n    });\n\n    it('shows download progress', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          configured: true,\n          available:  true,\n          downloaded: false,\n          info:       {\n            version:                    'v1.2.3',\n            files:                      [],\n            path:                       '',\n            sha512:                     '',\n            releaseDate:                '',\n            nextUpdateTime:             12345,\n            unsupportedUpdateAvailable: false,\n          },\n          progress: {\n            percent:        12.34,\n            bytesPerSecond: 1234567,\n            total:          0,\n            delta:          0,\n            transferred:    0,\n          },\n        } as UpdateState,\n        locale: 'en',\n      });\n\n      expect(wrapper.get({ ref: 'updateStatus' }).text())\n        .toMatch(/^An update to version v1\\.2\\.3 is available; downloading... \\(12%, 1\\.2MB\\/s(?:ec\\.?)?\\)$/);\n      expect(wrapper.find({ ref: 'applyButton' }).exists()).toBeFalsy();\n    });\n  });\n\n  describe('release notes', () => {\n    it('should not be displayed if there are none', () => {\n      const wrapper = wrap({ enabled: true, updateState: { info: { version: 'v1.2.3' } } as UpdateState });\n\n      expect(wrapper.find({ ref: 'releaseNotes' }).exists()).toBeFalsy();\n    });\n\n    it('should render plain text', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true,\n          info:      { version: 'v1.2.3', releaseNotes: 'hello' },\n        } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'releaseNotes' }).text())\n        .toEqual('hello');\n    });\n\n    it('should render markdown', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true,\n          info:      { version: 'v1.2.3', releaseNotes: '**hello**' },\n        } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'releaseNotes' }).html())\n        .toContain('<strong>hello</strong>');\n    });\n\n    it('should not support scripting', () => {\n      const wrapper = wrap({\n        enabled:     true,\n        updateState: {\n          available: true,\n          info:      {\n            version:      'v1.2.3',\n            releaseNotes: 'hello<script>alert(1)</script><img onload=\"alert(2)\">',\n          },\n        } as UpdateState,\n      });\n\n      expect(wrapper.get({ ref: 'releaseNotes' }).html())\n        .not.toContain('alert');\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/LabeledBadge.vue",
    "content": "<script lang=\"ts\">\n\nimport * as Components from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport type { PropType } from 'vue';\n\nconst { BadgeState } = (Components as any).default ?? Components;\n\nexport default defineComponent({\n  name:       'labeled-badge',\n  components: { BadgeState },\n  props:      {\n    color: {\n      type: String as PropType<'bg-primary' | 'bg-default' | 'bg-darker' | 'bg-success' | 'bg-info' | 'bg-warning'\n        | 'bg-error'>,\n      default: 'bg-darker',\n    },\n    text: {\n      type:     String,\n      required: true,\n    },\n  },\n});\n</script>\n\n<template>\n  <badge-state\n    class=\"badge\"\n    :color=\"color\"\n    :label=\"text\"\n  />\n</template>\n\n<style lang=\"scss\" scoped>\n  .badge {\n    line-height: initial;\n    letter-spacing: initial;\n    font-size: 10px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/LabeledSelect.vue",
    "content": "<script>\nimport { LabeledTooltip } from '@rancher/components';\n\nimport LabeledSelectPagination from '@pkg/components/form/labeled-select-utils/labeled-select-pagination';\nimport CompactInput from '@pkg/mixins/compact-input';\nimport LabeledFormElement from '@pkg/mixins/labeled-form-element';\nimport VueSelectOverrides from '@pkg/mixins/vue-select-overrides';\nimport { LABEL_SELECT_NOT_OPTION_KINDS } from '@pkg/types/components/labeledSelect';\nimport { get } from '@pkg/utils/object';\nimport { onClickOption, calculatePosition } from '@pkg/utils/select';\n\n// In theory this would be nicer as LabeledSelect/index.vue, however that would break a lot of places where we import this (which includes extensions)\n\nexport default {\n  name: 'LabeledSelect',\n\n  components: { LabeledTooltip },\n  mixins:     [\n    CompactInput,\n    LabeledFormElement,\n    VueSelectOverrides,\n    LabeledSelectPagination,\n  ],\n\n  emits: ['on-open', 'on-close', 'selecting', 'update:validation'],\n\n  props: {\n    appendToBody: {\n      default: true,\n      type:    Boolean,\n    },\n    clearable: {\n      default: false,\n      type:    Boolean,\n    },\n    disabled: {\n      default: false,\n      type:    Boolean,\n    },\n    required: {\n      default: false,\n      type:    Boolean,\n    },\n    hoverTooltip: {\n      default: true,\n      type:    Boolean,\n    },\n    loading: {\n      default: false,\n      type:    Boolean,\n    },\n    localizedLabel: {\n      default: false,\n      type:    Boolean,\n    },\n    optionKey: {\n      default: null,\n      type:    String,\n    },\n    optionLabel: {\n      default: 'label',\n      type:    String,\n    },\n    placement: {\n      default: null,\n      type:    String,\n    },\n    reduce: {\n      default: (e) => {\n        if (e && typeof e === 'object' && e.value !== undefined) {\n          return e.value;\n        }\n\n        return e;\n      },\n      type: Function,\n    },\n    selectable: {\n      default: (opt) => {\n        if ( opt ) {\n          if ( opt.disabled || LABEL_SELECT_NOT_OPTION_KINDS.includes(opt.kind) || opt.loading ) {\n            return false;\n          }\n        }\n\n        return true;\n      },\n      type: Function,\n    },\n    status: {\n      default: null,\n      type:    String,\n    },\n    tooltip: {\n      default: null,\n      type:    [String, Object],\n    },\n    value: {\n      default: null,\n      type:    [String, Object, Number, Array, Boolean],\n    },\n    options: {\n      type:    Array,\n      default: () => ([]),\n    },\n    closeOnSelect: {\n      type:    Boolean,\n      default: true,\n    },\n    noOptionsLabelKey: {\n      type:    String,\n      default: 'labelSelect.noOptions.empty',\n    },\n  },\n\n  data() {\n    return {\n      selectedVisibility: 'visible',\n      shouldOpen:         true,\n    };\n  },\n\n  computed: {\n    hasLabel() {\n      return this.isCompact ? false : !!this.label || !!this.labelKey || !!this.$slots.label;\n    },\n\n    hasGroupIcon() {\n      // Required for option.icon. Note that we only apply if paginating as well (there might be 2 x performance issues with 2k entries. one to iterate through this list, the other with conditional class per entry in dom)\n      return this.canPaginate ? !!this._options.find((o) => o.kind === 'group' && !!o.icon) : false;\n    },\n\n    _options() {\n      // If we're paginated show the page as provided by `paginate`. See label-select-pagination mixin\n      return this.canPaginate ? this.page : this.options;\n    },\n  },\n\n  methods: {\n    // resizeHandler = in mixin\n    focusSearch() {\n      const blurredAgo = Date.now() - this.blurred;\n\n      if (!this.focused && blurredAgo < 250) {\n        return;\n      }\n\n      this.$nextTick(() => {\n        const el = this.$refs['select-input']?.searchEl;\n\n        if (el) {\n          el.focus();\n        }\n      });\n    },\n\n    onFocus() {\n      this.selectedVisibility = 'hidden';\n      this.onFocusLabeled();\n    },\n\n    onBlur() {\n      this.selectedVisibility = 'visible';\n      this.onBlurLabeled();\n    },\n\n    onOpen() {\n      this.$emit('on-open');\n      this.resizeHandler();\n    },\n\n    onClose() {\n      this.$emit('on-close');\n    },\n\n    getOptionLabel(option) {\n      if (!option) {\n        return;\n      }\n\n      if (this.$attrs['get-option-label']) {\n        return this.$attrs['get-option-label'](option);\n      }\n      if (get(option, this.optionLabel)) {\n        if (this.localizedLabel) {\n          const label = get(option, this.optionLabel);\n\n          return this.$store.getters['i18n/t'](label) || label;\n        } else {\n          return get(option, this.optionLabel);\n        }\n      } else {\n        return option;\n      }\n    },\n\n    positionDropdown(dropdownList, component, { width }) {\n      calculatePosition(dropdownList, component, width, this.placement);\n    },\n\n    get,\n\n    onClickOption(option, event) {\n      onClickOption.call(this, option, event);\n    },\n\n    dropdownShouldOpen(instance, forceOpen = false) {\n      const { noDrop, mutableLoading } = instance;\n      const { open } = instance;\n      const shouldOpen = this.shouldOpen;\n\n      if (forceOpen) {\n        instance.open = true;\n\n        return true;\n      }\n\n      if (shouldOpen === false) {\n        this.shouldOpen = true;\n        instance.closeSearchOptions();\n      }\n\n      return noDrop ? false : open && shouldOpen && !mutableLoading;\n    },\n\n    onSearch(newSearchString) {\n      if (this.canPaginate) {\n        this.setPaginationFilter(newSearchString);\n      } else {\n        if (newSearchString) {\n          this.dropdownShouldOpen(this.$refs['select-input'], true);\n        }\n      }\n    },\n\n    getOptionKey(opt) {\n      if (this.optionKey) {\n        return get(opt, this.optionKey);\n      }\n\n      return this.getOptionLabel(opt);\n    },\n  },\n};\n</script>\n\n<template>\n  <div\n    ref=\"select\"\n    class=\"labeled-select\"\n    :class=\"{\n      disabled: isView || disabled,\n      focused,\n      [mode]: true,\n      [status]: status,\n      taggable: $attrs.taggable,\n      taggable: $attrs.multiple,\n      hoverable: hoverTooltip,\n      'compact-input': isCompact,\n      'no-label': !hasLabel,\n    }\"\n    @click=\"focusSearch\"\n    @focus=\"focusSearch\"\n  >\n    <div\n      :class=\"{ 'labeled-container': true, raised, empty, [mode]: true }\"\n      :style=\"{ border: 'none' }\"\n    >\n      <label v-if=\"hasLabel\">\n        <t\n          v-if=\"labelKey\"\n          :k=\"labelKey\"\n        />\n        <template v-else-if=\"label\">{{ label }}</template>\n\n        <span\n          v-if=\"requiredField\"\n          class=\"required\"\n        >*</span>\n      </label>\n    </div>\n    <v-select\n      ref=\"select-input\"\n      v-bind=\"$attrs\"\n      class=\"inline\"\n      :append-to-body=\"appendToBody\"\n      :calculate-position=\"positionDropdown\"\n      :class=\"{ 'no-label': !(label || '').length }\"\n      :clearable=\"clearable\"\n      :disabled=\"isView || disabled || loading\"\n      :get-option-key=\"getOptionKey\"\n      :get-option-label=\"(opt) => getOptionLabel(opt)\"\n      :label=\"optionLabel\"\n      :options=\"_options\"\n      :map-keydown=\"mappedKeys\"\n      :placeholder=\"placeholder\"\n      :reduce=\"(x) => reduce(x)\"\n      :filterable=\"isFilterable\"\n      :searchable=\"isSearchable\"\n      :selectable=\"selectable\"\n      :model-value=\"value != null && !loading ? value : ''\"\n      :dropdown-should-open=\"dropdownShouldOpen\"\n\n      @update:model-value=\"$emit('selecting', $event); $emit('update:value', $event)\"\n      @search:blur=\"onBlur\"\n      @search:focus=\"onFocus\"\n      @search=\"onSearch\"\n      @open=\"onOpen\"\n      @close=\"onClose\"\n      @option:selected=\"$emit('selecting', $event)\"\n    >\n      <template #option=\"option\">\n        <template v-if=\"option.kind === 'group'\">\n          <div class=\"vs__option-kind-group\">\n            <i\n              v-if=\"option.icon\"\n              class=\"icon\"\n              :class=\"{ [option.icon]: true }\"\n            />\n            <b>{{ getOptionLabel(option) }}</b>\n            <div v-if=\"option.badge\">\n              {{ option.badge }}\n            </div>\n          </div>\n        </template>\n        <template v-else-if=\"option.kind === 'divider'\">\n          <hr>\n        </template>\n        <template v-else-if=\"option.kind === 'highlighted'\">\n          <div class=\"option-kind-highlighted\">\n            {{ option.label }}\n          </div>\n        </template>\n        <div\n          v-else\n          class=\"vs__option-kind\"\n          :class=\"{ 'has-icon': hasGroupIcon }\"\n          @mousedown=\"(e) => onClickOption(option, e)\"\n        >\n          {{ getOptionLabel(option) }}\n          <i\n            v-if=\"option.error\"\n            class=\"icon icon-warning pull-right\"\n            style=\"font-size: 20px;\"\n          />\n        </div>\n      </template>\n      <!-- Pass down templates provided by the caller -->\n      <template\n        v-for=\"(_, slot) of $slots\"\n        :key=\"slot\"\n        #[slot]=\"scope\"\n      >\n        <slot\n          :name=\"slot\"\n          v-bind=\"scope\"\n        />\n      </template>\n\n      <template #list-footer>\n        <div\n          v-if=\"canPaginate && totalResults\"\n          class=\"pagination-slot\"\n        >\n          <div class=\"load-more\">\n            <i\n              v-if=\"paginating\"\n              class=\"icon icon-spinner icon-spin\"\n            />\n            <div v-else>\n              <a\n                v-if=\"canLoadMore\"\n                @click=\"loadMore\"\n              > {{ t('labelSelect.pagination.more') }}</a>\n            </div>\n          </div>\n\n          <div class=\"count\">\n            {{ optionCounts }}\n          </div>\n        </div>\n      </template>\n      <template #no-options=\"{ search }\">\n        <div class=\"no-options-slot\">\n          <div\n            v-if=\"paginating\"\n            class=\"paginating\"\n          >\n            <i class=\"icon icon-spinner icon-spin\" />\n          </div>\n          <template v-else-if=\"search\">\n            {{ t('labelSelect.noOptions.noMatch') }}\n          </template>\n          <template v-else>\n            {{ t(noOptionsLabelKey) }}\n          </template>\n        </div>\n      </template>\n    </v-select>\n    <i\n      v-if=\"loading\"\n      class=\"icon icon-spinner icon-spin icon-lg\"\n    />\n    <LabeledTooltip\n      v-if=\"tooltip && !focused\"\n      :hover=\"hoverTooltip\"\n      :value=\"tooltip\"\n      :status=\"status\"\n    />\n    <LabeledTooltip\n      v-if=\"!!validationMessage\"\n      :hover=\"hoverTooltip\"\n      :value=\"validationMessage\"\n    />\n  </div>\n</template>\n\n<style lang='scss' scoped>\n\n.labeled-select {\n  position: relative;\n  // Prevent namespace field from wiggling or changing\n  // height when it is toggled from a LabeledInput to a\n  // LabeledSelect.\n  padding-bottom: 1px;\n\n  &.no-label.compact-input {\n    :deep() .vs__actions:after {\n      top: -2px;\n    }\n\n    .labeled-container {\n      padding: 5px 0 1px 10px;\n    }\n  }\n\n  &.no-label:not(.compact-input) {\n    height: $input-height;\n    padding-top: 4px;\n\n    :deep() .vs__actions:after {\n      top: 0;\n    }\n  }\n\n  .icon-spinner {\n    position: absolute;\n    left: calc(50% - .5em);\n    top: calc(50% - .5em);\n  }\n\n  .labeled-container {\n    // Make LabeledSelect and LabeledInput the same height so they\n    // don't wiggle when you toggle between them.\n    padding: 7px 0 0 $input-padding-sm;\n    padding: $input-padding-sm 0 0 $input-padding-sm;\n\n    label {\n      margin: 0;\n    }\n\n    .selected {\n      background-color: transparent;\n    }\n  }\n\n  &.view {\n    &.labeled-input {\n      .labeled-container {\n        padding: 0;\n      }\n    }\n  }\n\n  &.taggable.compact-input {\n    min-height: $unlabeled-input-height;\n    :deep() .vs__selected-options {\n      padding-top: 8px !important;\n    }\n  }\n\n  &.taggable:not(.compact-input) {\n    min-height: $input-height;\n    :deep() .vs__selected-options {\n      // Need to adjust margin when there is a label in the control to add space between the label and the tags\n      margin-top: 0px;\n    }\n  }\n\n  &:not(.taggable) {\n    :deep() .vs__selected-options {\n      // Ensure whole select is clickable to close the select when open\n      .vs__selected {\n        width: 100%;\n      }\n    }\n  }\n\n  &.taggable {\n    :deep() .vs__selected-options {\n      padding: 3px 0;\n      .vs__selected {\n        border-color: var(--accent-btn);\n        height: 20px;\n        min-height: unset !important;\n        padding: 0 0 0 7px !important;\n\n        > button {\n          height: 20px;\n          line-height: 14px;\n        }\n\n        > button:hover {\n          background-color: var(--primary);\n          border-radius: 0;\n\n          &::after {\n            color: #fff;\n          }\n        }\n      }\n    }\n  }\n\n  :deep() .vs__selected-options {\n    margin-top: -5px;\n  }\n\n  :deep() .v-select:not(.vs--single) {\n    .vs__selected-options {\n      padding: 5px 0;\n    }\n  }\n\n  :deep() .vs__actions {\n    &:after {\n      position: relative;\n      top: -10px;\n    }\n  }\n\n  :deep() .v-select.vs--open {\n    .vs__dropdown-toggle {\n      color: var(--outline) !important;\n    }\n  }\n\n  :deep() &.disabled {\n    .labeled-container,\n    .vs__dropdown-toggle,\n    input,\n    label {\n      cursor: not-allowed;\n    }\n  }\n\n  .no-label :deep() {\n    &.v-select:not(.vs--single) {\n      min-height: 33px;\n    }\n\n    &.selected {\n      padding-top: 8px;\n      padding-bottom: 9px;\n      position: relative;\n      max-height: 2.3em;\n      overflow: hidden;\n    }\n\n    .vs__selected-options {\n      padding: 8px 0 7px 0;\n    }\n  }\n}\n\n$icon-size: 18px;\n\n// This represents the drop down area. Note - it might be attached to body and NOT the parent label select div\n.vs__dropdown-menu {\n\n  // Styling for individual options\n  .vs__dropdown-option .vs__option-kind {\n    &-group {\n      display: flex;\n      align-items: center;\n\n      i { // icon\n        width: $icon-size;\n      }\n\n      > b { // group label\n        flex: 1;\n      }\n\n      > div { // badge\n        background-color: var(--primary);\n        border-radius: 4px;\n        color: var(--primary-text);\n        font-size: 12px;\n        height: 18px;\n        line-height: 18px;\n        margin-top: 1px;\n        padding: 0 10px;\n      }\n    }\n\n    &.has-icon {\n      padding-left: $icon-size;\n    }\n  }\n\n    &.has-icon .vs__option-kind div{\n    padding-left: $icon-size;\n  }\n\n  .pagination-slot {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    position: relative;\n    margin-top: 5px;\n\n    .load-more {\n      display: flex;\n      align-items: center;\n      height: 19px;\n\n      a {\n        cursor: pointer;\n      }\n    }\n\n    .count {\n      position: absolute;\n      right: 10px;\n    }\n  }\n\n  .no-options-slot .paginating {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n}\n\n// Styling for option highlighted\n.vs__dropdown-option {\n  > .option-kind-highlighted {\n    color: var(--dropdown-highlight-text);\n\n    &:hover {\n      color: var(--dropdown-hover-text);\n    }\n  }\n\n  &.vs__dropdown-option--selected,\n  &.vs__dropdown-option--highlight {\n    > .option-kind-highlighted {\n      color: var(--dropdown-hover-text);\n    }\n  }\n}\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/LabeledTooltip.vue",
    "content": "<script>\nexport default {\n  props: {\n    value: {\n      type:    [String, Object],\n      default: null,\n    },\n\n    status: {\n      type:    String,\n      default: 'error',\n    },\n\n    hover: {\n      type:    Boolean,\n      default: true,\n    },\n  },\n};\n</script>\n\n<template>\n  <div\n    ref=\"container\"\n    class=\"labeled-tooltip\"\n    :class=\"{ [status]: true, hoverable: hover }\"\n  >\n    <template v-if=\"hover\">\n      <i\n        v-tooltip=\"value.content ? { ...{ content: value.content, classes: [`tooltip-${status}`] }, ...value } : value\"\n        :class=\"{ hover: !value }\"\n        class=\"icon icon-info-circle status-icon\"\n      />\n    </template>\n    <template v-else>\n      <i\n        :class=\"{ hover: !value }\"\n        class=\"icon icon-info-circle status-icon\"\n      />\n      <div\n        v-if=\"value\"\n        class=\"tooltip\"\n        x-placement=\"bottom\"\n      >\n        <div class=\"tooltip-arrow\" />\n        <div class=\"tooltip-inner\">\n          {{ value }}\n        </div>\n      </div>\n    </template>\n  </div>\n</template>\n\n<style lang='scss'>\n.labeled-tooltip {\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n\n    &.hoverable {\n      height: 0%;\n    }\n\n     .status-icon {\n         position:  absolute;\n         right: 30px;\n         top: $input-padding-lg;\n         font-size: 20px;\n         z-index: z-index(hoverOverContent);\n\n     }\n\n    .tooltip {\n        position: absolute;\n        width: calc(100% + 2px);\n        top: calc(100% + 6px);\n\n        .tooltip-arrow {\n            right: 30px;\n        }\n\n        .tooltip-inner {\n            padding: 10px;\n        }\n    }\n\n    @mixin tooltipColors($color) {\n        .status-icon {\n            color: $color;\n        }\n        .tooltip {\n            .tooltip-inner {\n                color: var(--input-bg);\n                background: $color;\n                border-color: $color;\n            }\n\n            .tooltip-arrow {\n                border-bottom-color: $color;\n                &:after {\n                    border: none;\n                }\n            }\n        }\n    }\n\n    &.error {\n        @include tooltipColors(var(--error));\n    }\n\n    &.warning {\n        @include tooltipColors(var(--warning));\n    }\n\n    &.success {\n        @include tooltipColors(var(--success));\n    }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/RdCheckbox.vue",
    "content": "<script lang=\"ts\">\nimport * as Components from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport TooltipIcon from '@pkg/components/form/TooltipIcon.vue';\n\nconst { Checkbox } = (Components as any).default ?? Components;\n\nexport default defineComponent({\n  name:         'rd-checkbox',\n  components:   { TooltipIcon, Checkbox },\n  inheritAttrs: false,\n  props:        {\n    isExperimental: {\n      type:    Boolean,\n      default: false,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n    tooltip: {\n      type:    String,\n      default: null,\n    },\n    labelKey: {\n      type:    String,\n      default: null,\n    },\n    label: {\n      type:    String,\n      default: null,\n    },\n    tooltipKey: {\n      type:    String,\n      default: null,\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"rd-checkbox-container\">\n    <checkbox\n      v-bind=\"$attrs\"\n      class=\"checkbox\"\n      :disabled=\"$attrs.disabled || isLocked\"\n    >\n      <template #label>\n        <slot name=\"label\">\n          <t\n            v-if=\"labelKey\"\n            :k=\"labelKey\"\n            :raw=\"true\"\n          />\n          <template v-else-if=\"label\">\n            {{ label }}\n          </template>\n          <i\n            v-if=\"tooltipKey\"\n            v-clean-tooltip=\"t(tooltipKey)\"\n            class=\"checkbox-info icon icon-info icon-lg\"\n          />\n          <i\n            v-else-if=\"tooltip\"\n            v-clean-tooltip=\"tooltip\"\n            class=\"checkbox-info icon icon-info icon-lg\"\n          />\n        </slot>\n        <slot name=\"after\">\n          <tooltip-icon\n            v-if=\"isExperimental\"\n            class=\"tooltip-icon\"\n          />\n          <i\n            v-if=\"isLocked\"\n            v-tooltip=\"{\n              content: tooltip || t('preferences.locked.tooltip', undefined, true),\n              placement: 'right',\n            }\"\n            class=\"icon icon-lock\"\n          />\n        </slot>\n      </template>\n    </checkbox>\n    <div class=\"checkbox-below\">\n      <slot name=\"below\" />\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.checkbox :deep(.checkbox-outer-container-description) {\n  font-size: 11px;\n}\n.tooltip-icon {\n  margin-left: 0.25rem;\n}\n.checkbox-below {\n  margin-left: 19px;\n  font-size: 11px;\n  &:empty {\n    display: none;\n  }\n}\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/RdFieldset.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\n\nimport LabeledBadge from '@pkg/components/form/LabeledBadge.vue';\nimport TooltipIcon from '@pkg/components/form/TooltipIcon.vue';\n\n/**\n * Groups several controls as well as labels\n */\nexport default defineComponent({\n  name:       'rd-fieldset',\n  components: { TooltipIcon, LabeledBadge },\n  props:      {\n    legendText: {\n      type:    String,\n      default: '',\n    },\n    badgeText: {\n      type:    String,\n      default: '',\n    },\n    legendTooltip: {\n      type:    String,\n      default: '',\n    },\n    isExperimental: {\n      type:    Boolean,\n      default: false,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  computed: {\n    lockedTooltip() {\n      const legendTooltip = this.legendTooltip ? ` <br><br> ${ this.legendTooltip }` : '';\n\n      return `${ this.t('preferences.locked.tooltip') }${ legendTooltip }`;\n    },\n  },\n});\n</script>\n\n<template>\n  <fieldset class=\"rd-fieldset\">\n    <legend>\n      <slot name=\"legend\">\n        <span>{{ legendText }}</span>\n        <labeled-badge\n          v-if=\"badgeText\"\n          :text=\"badgeText\"\n        />\n        <tooltip-icon\n          v-if=\"isExperimental\"\n        />\n        <i\n          v-if=\"isLocked\"\n          v-clean-tooltip=\"{\n            content: lockedTooltip,\n            html: true,\n            placement: 'right',\n          }\"\n          class=\"icon icon-lock\"\n        />\n        <i\n          v-else-if=\"legendTooltip\"\n          v-clean-tooltip=\"{\n            content: legendTooltip,\n            html: true,\n          }\"\n          class=\"icon icon-info-circle icon-lg\"\n        />\n      </slot>\n    </legend>\n    <slot\n      name=\"default\"\n      :is-locked=\"isLocked\"\n    >\n      <!-- Slot content -->\n    </slot>\n  </fieldset>\n</template>\n\n<style lang=\"scss\" scoped>\n  .rd-fieldset {\n    margin: 0;\n    padding: 0;\n    border: none;\n\n    legend {\n      font-size: 1rem;\n      color: inherit;\n      line-height: 1.5rem;\n      padding-bottom: 0.5rem;\n\n      > * {\n        margin-right: 0.25rem;\n      }\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/RdSlider.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport VueSlider from 'vue-3-slider-component';\n\nimport RdInput from '@pkg/components/RdInput.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\n\nexport default defineComponent({\n  name:       'rd-slider',\n  components: {\n    VueSlider, RdFieldset, RdInput,\n  },\n  props: {\n    label: {\n      type:    String,\n      default: '',\n    },\n    value: {\n      type:     Number,\n      required: true,\n    },\n    min: {\n      type:     Number,\n      required: true,\n    },\n    max: {\n      type:     Number,\n      required: true,\n    },\n    interval: {\n      type:    Number,\n      default: 1,\n    },\n    marks: {\n      type:     Array,\n      required: true,\n    },\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n    process: {\n      type:     Function,\n      required: true,\n    },\n    isLocked: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n  methods: {\n    updatedVal(value: string) {\n      this.$emit('change', value);\n    },\n  },\n});\n</script>\n\n<template>\n  <rd-fieldset\n    :legend-text=\"label\"\n    :is-locked=\"isLocked\"\n  >\n    <div class=\"rd-slider\">\n      <rd-input\n        type=\"number\"\n        class=\"slider-input\"\n        :value=\"value\"\n        :is-locked=\"isLocked\"\n        @input=\"updatedVal($event.target.value)\"\n      >\n        <template #after>\n          <div class=\"empty-content\" />\n        </template>\n      </rd-input>\n      <vue-slider\n        ref=\"slider\"\n        class=\"rd-slider-rail\"\n        :model-value=\"value\"\n        :min=\"min\"\n        :max=\"max\"\n        :interval=\"interval\"\n        :marks=\"marks\"\n        :tooltip=\"'none'\"\n        :disabled=\"disabled || isLocked\"\n        :process=\"process\"\n        @change=\"updatedVal($event)\"\n      />\n    </div>\n  </rd-fieldset>\n</template>\n\n<style lang=\"scss\" scoped>\n.rd-fieldset {\n  width: 100%;\n}\n\n.rd-slider {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  gap: 2rem;\n  margin-bottom: 1rem;\n}\n\n.rd-slider-rail {\n  flex-grow: 1;\n}\n\n.labeled-input .vue-slider {\n  margin: 2em 1em;\n  flex: 1;\n}\n\n.vue-slider :deep(.vue-slider-rail) {\n  background-color: var(--progress-bg);\n}\n\n.vue-slider :deep(.vue-slider-mark-step) {\n  background-color: var(--checkbox-tick-disabled);\n  opacity: 0.5;\n}\n\n.vue-slider :deep(.vue-slider-dot-handle) {\n  background-color: var(--scrollbar-thumb);\n  box-shadow: 0.5px 0.5px 2px 1px var(--darker);\n}\n\n@media screen and (prefers-color-scheme: dark) {\n  .vue-slider :deep(.vue-slider-dot-handle) {\n    background-color: var(--checkbox-tick-disabled);\n  }\n}\n\n.vue-slider :deep(.vue-slider-process) {\n  background-color: var(--error);\n}\n\n.slider-input, .slider-input:focus, .slider-input:hover {\n  max-width: 6rem;\n}\n\n.empty-content {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/SplitButton.vue",
    "content": "<!--\n  - Split Button, as found in Windows:\n  -\n  - Normal State:          Click on the dropdown button (expanded):\n  - +-------------+---+    +-------------+---+\n  - | Button Text | v |    | Button Text | v |\n  - +-------------+---+    +-------------+---+\n  -                        | Option 1        |\n  -                        | Option 2        |\n  -                        +-----------------+\n  -\n  -->\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport type { PropType } from 'vue';\n\ninterface Option {\n  /** The label to display as the option. */\n  label: string;\n  /** The value that will be emitted on @input */\n  id:    string;\n  /** Optional icon */\n  icon?: string;\n}\n\nexport default defineComponent({\n  name:  'split-button',\n  props: {\n    /** The main button text */\n    label: {\n      type:    String,\n      default: '',\n    },\n    /**\n     * The dropdown options.\n     * If the item is a string, then the label will be emitted.\n     */\n    options: {\n      type:    Array as PropType<(Option | string)[]>,\n      default: () => [],\n    },\n    /** The value to emit when the main button is clicked. */\n    value: {\n      type:    String,\n      default: '',\n    },\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  data() {\n    return {\n      /** Whether the popup is open */\n      showing:    false,\n      suppressed: false,\n    };\n  },\n\n  computed: {\n    computedOptions(): Option[] {\n      return this.options.map((option) => {\n        if (typeof (option) === 'string') {\n          return { label: option, id: option };\n        }\n\n        return option;\n      });\n    },\n  },\n\n  methods: {\n    /**\n     * Because everything is inside the top <button>, we need to suppress any\n     * click events that are fired on it when its children (e.g. the dropdown) are\n     * clicked.  Call this to do so.\n     * @returns true if suppression is active.\n     */\n    suppress(): boolean {\n      if (this.suppressed) {\n        return true;\n      }\n      this.suppressed = true;\n      Promise.resolve().then(() => (this.suppressed = false));\n\n      return false;\n    },\n\n    show() {\n      this.showing = !this.disabled;\n      this.$nextTick(() => this.popupFocus(1));\n    },\n\n    hide() {\n      // Call suppress here, in case user clicked on .background to close the popup.\n      this.suppress();\n      this.showing = false;\n      (this.$el as HTMLElement).focus();\n    },\n\n    execute(id?: string) {\n      if (this.suppress()) {\n        return;\n      }\n\n      this.$emit('input', typeof id === 'undefined' ? this.value : id);\n      this.hide();\n    },\n\n    popupUp(event: KeyboardEvent) {\n      const elem = event.target as Element | null;\n      const prev = elem?.previousElementSibling as HTMLElement | null;\n\n      prev?.focus();\n    },\n\n    popupDown(event: KeyboardEvent) {\n      const elem = event.target as Element | null;\n      const next = elem?.nextElementSibling as HTMLElement | null;\n\n      next?.focus();\n    },\n\n    popupFocus(n: number) {\n      if (n < 0) {\n        n += this.computedOptions.length + 1;\n      }\n      const elem = this.$el.querySelector(`.menu > li:nth-child(${ n })`) as HTMLElement | null;\n\n      elem?.focus();\n    },\n\n    popupHover(event: MouseEvent) {\n      (event.target as HTMLElement | null)?.focus();\n    },\n\n    popupTrigger(event: KeyboardEvent) {\n      const newEvent = new MouseEvent('click');\n\n      event.target?.dispatchEvent(newEvent);\n    },\n  },\n});\n</script>\n\n<template>\n  <button\n    class=\"btn split-button\"\n    :disabled=\"disabled\"\n    @click.self=\"execute()\"\n    @keyup.esc=\"hide\"\n  >\n    {{ label }}\n    <button\n      v-if=\"computedOptions.length > 0\"\n      ref=\"indicator\"\n      class=\"indicator icon-btn icon icon-chevron-down role-multi-action\"\n      :aria-expanded=\"showing\"\n      @click=\"show\"\n    />\n    <div\n      v-if=\"showing\"\n      class=\"background\"\n      @click=\"hide\"\n      @contextmenu.prevent\n    />\n    <ul\n      v-if=\"showing\"\n      class=\"list-unstyled menu\"\n    >\n      <li\n        v-for=\"opt in computedOptions\"\n        :key=\"opt.id\"\n        role=\"menuitem\"\n        tabindex=\"0\"\n        @click.stop=\"execute(opt.id)\"\n        @keydown.home.prevent=\"popupFocus(1)\"\n        @keydown.end.prevent=\"popupFocus(-1)\"\n        @keydown.arrow-up.prevent=\"popupUp\"\n        @keydown.arrow-down.prevent=\"popupDown\"\n        @keypress.space.prevent=\"popupTrigger\"\n        @keypress.enter.prevent=\"popupTrigger\"\n        @mouseover=\"popupHover\"\n      >\n        <i\n          v-if=\"opt.icon\"\n          :class=\"{ icon: true, [`icon-${opt.icon}`]: true }\"\n        />\n        <span v-text=\"opt.label\" />\n      </li>\n    </ul>\n  </button>\n</template>\n\n<style lang=\"scss\" scoped>\n  /* $btn-padding copied from _button.scss */\n  $btn-padding: 21px;\n\n  .split-button {\n    position: relative;\n    /* Remove the right padding so that the split button goes all the way */\n    padding-right: 0;\n\n    &.role-secondary {\n      .indicator {\n        border-left: 1px solid var(--primary);\n      }\n    }\n  }\n  .indicator {\n    margin-left: math.div($btn-padding, 2);\n    padding: 0 math.div($btn-padding, 2);\n    background: transparent;\n    border: none;\n    border-top-left-radius: 0;\n    border-bottom-left-radius: 0;\n    box-shadow: none;\n    &:focus::before {\n      outline: 1px dashed;\n    }\n  }\n  .menu {\n    @include list-unstyled;\n\n    position: absolute;\n    margin-left: 0px - $btn-padding - /* border */ 1px;\n    z-index: z-index('dropdownContent');\n\n    color: var(--dropdown-text);\n    background-color: var(--dropdown-bg);\n    border: 1px solid var(--dropdown-border);\n    border-radius: var(--border-radius);\n    box-shadow: 0 5px 20px var(--shadow);\n    /* Hide overflow to clip out the corners from border-radius */\n    overflow: hidden;\n\n    li {\n      margin: 0;\n      padding: 0 1em;\n      &:focus {\n        background-color: var(--dropdown-hover-bg);\n        color: var(--dropdown-hover-text);\n      }\n      .icon {\n        display: unset;\n      }\n    }\n  }\n\n  .background {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    opacity: 0;\n    z-index: z-index('dropdownOverlay');\n  }\n  </style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/TextAreaAutoGrow.vue",
    "content": "<script>\nimport $ from 'jquery';\nimport debounce from 'lodash/debounce';\n\nimport { _EDIT, _VIEW } from '@pkg/config/query-params';\n\nexport default {\n  inheritAttrs: false,\n\n  props: {\n    mode: {\n      type:    String,\n      default: _EDIT,\n    },\n\n    minHeight: {\n      type:    Number,\n      default: 35,\n    },\n    maxHeight: {\n      type:    Number,\n      default: 200,\n    },\n    placeholder: {\n      type:    String,\n      default: '',\n    },\n    spellcheck: {\n      type:    Boolean,\n      default: true,\n    },\n\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n  },\n\n  data() {\n    return {\n      curHeight: this.minHeight,\n      overflow:  'hidden',\n    };\n  },\n\n  computed: {\n    isDisabled() {\n      return this.disabled || this.mode === _VIEW;\n    },\n\n    style() {\n      // This sets the height to one-line for SSR pageload so that it's already right\n      // (unless the input is long)\n      return `height: ${ this.curHeight }px; overflow: ${ this.overflow };`;\n    },\n  },\n\n  watch: {\n    $attrs: {\n      deep: true,\n      handler() {\n        this.queueResize();\n      },\n    },\n  },\n\n  created() {\n    this.queueResize = debounce(this.autoSize, 100);\n  },\n\n  mounted() {\n    $(this.$refs.ta).css('height', `${ this.curHeight }px`);\n    this.$nextTick(() => {\n      this.autoSize();\n    });\n  },\n\n  methods: {\n    onInput(val) {\n      this.$emit('input', val);\n      this.queueResize();\n    },\n\n    focus() {\n      this.$refs.ta.focus();\n    },\n\n    autoSize() {\n      const el = this.$refs.ta;\n\n      if ( !el ) {\n        return;\n      }\n\n      const $el = $(el);\n\n      $el.css('height', '1px');\n\n      const border = parseInt($el.css('borderTopWidth'), 10) || 0 + parseInt($el.css('borderBottomWidth'), 10) || 0;\n      const neu = Math.max(this.minHeight, Math.min(el.scrollHeight + border, this.maxHeight));\n\n      $el.css('overflowY', (el.scrollHeight > neu ? 'auto' : 'hidden'));\n      $el.css('height', `${ neu }px`);\n\n      this.curHeight = neu;\n    },\n  },\n};\n</script>\n\n<template>\n  <textarea\n    ref=\"ta\"\n    :disabled=\"isDisabled\"\n    :style=\"style\"\n    :placeholder=\"placeholder\"\n    class=\"no-resize no-ease\"\n    v-bind=\"$attrs\"\n    :spellcheck=\"spellcheck\"\n    @paste=\"$emit('paste', $event)\"\n    @input=\"onInput($event.target.value)\"\n    @focus=\"$emit('focus', $event)\"\n    @blur=\"$emit('blur', $event)\"\n  />\n</template>\n\n<style lang='scss' scoped>\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/TooltipIcon.vue",
    "content": "<script lang=\"ts\">\nexport default {\n  props: {\n    icon: {\n      type:    String,\n      default: 'icon-flask',\n    },\n    tooltipText: {\n      type:    String,\n      default: 'prefs.experimental',\n    },\n    tooltipPlacement: {\n      type:    String,\n      default: 'right',\n    },\n  },\n};\n</script>\n\n<template>\n  <i\n    v-tooltip=\"{\n      content: t(tooltipText, undefined, true),\n      placement: tooltipPlacement,\n    }\"\n    :class=\"`icon ${icon}`\"\n  />\n</template>\n\n<style lang=\"scss\" scoped>\n  .icon {\n    font-size: 0.75rem;\n    background-color: var(--icon-circle);\n    border-radius: 50%;\n    text-align: center;\n    padding: 0.25rem;\n    width: 1.25rem;\n    height: 1.25rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/__tests__/SplitButton.spec.ts",
    "content": "import { mount } from '@vue/test-utils';\n\nimport SplitButton from '../SplitButton.vue';\n\nfunction wrap(props: Record<string, any>) {\n  return mount(SplitButton, { props });\n}\n\ndescribe('SplitButton.vue', () => {\n  it('should have the correct label', () => {\n    const wrapper = wrap({ label: 'hello' });\n\n    expect(wrapper.get('button').text()).toEqual('hello');\n  });\n\n  it('should not have dropdown if no options given', () => {\n    const wrapper = wrap({});\n\n    expect(wrapper.find({ ref: 'indicator' }).exists()).toBeFalsy();\n    expect(wrapper.find('ul').exists()).toBeFalsy();\n  });\n\n  it('should accept click and emit the correct value', async() => {\n    const wrapper = wrap({ value: 'yes' });\n\n    await wrapper.trigger('click');\n    expect(wrapper.emitted('input')?.flat() ?? []).toContain('yes');\n  });\n\n  it('should not work when disabled', async() => {\n    const wrapper = wrap({ value: 'yes', disabled: true });\n\n    await wrapper.trigger('click');\n    expect(wrapper.emitted('input')).toBeUndefined();\n  });\n\n  describe('dropdown handling', () => {\n    let wrapper: ReturnType<typeof wrap>;\n\n    beforeEach(async() => {\n      wrapper = wrap({\n        value:   'top',\n        options: ['hello', 'world', { id: 'lorem', icon: 'sun' }, 'ipsum'],\n      });\n      wrapper.element.ownerDocument.firstElementChild?.appendChild(wrapper.element);\n      await wrapper.get({ ref: 'indicator' }).trigger('click');\n    });\n\n    afterEach(() => {\n      wrapper.element.parentElement?.removeChild(wrapper.element);\n      wrapper.unmount();\n    });\n\n    it('should generate a dropdown', () => {\n      expect(wrapper.findAll('ul li').length).toBeGreaterThan(0);\n    });\n\n    it('supports icons', () => {\n      const icon = wrapper.find('ul li:nth-child(3) i');\n\n      expect(icon).not.toBeNull();\n      expect(icon.element.classList).toContain('icon');\n      expect(icon.element.classList).toContain('icon-sun');\n    });\n\n    it('should trigger on click', async() => {\n      const item = wrapper.findAll('ul li').filter(w => w.text() === 'hello')[0];\n\n      await item.trigger('click');\n      expect(Object.keys(wrapper.emitted())).toContain('input');\n      expect(wrapper.emitted('input')?.flat() ?? []).not.toContain('top');\n      expect(wrapper.emitted('input')?.flat() ?? []).toContain('hello');\n    });\n\n    it('should trigger on enter', async() => {\n      const item = wrapper.findAll('ul li').filter(w => w.text() === 'hello')[0];\n\n      await item.trigger('keypress.enter');\n      expect(Object.keys(wrapper.emitted())).toContain('input');\n      expect(wrapper.emitted('input')?.flat() ?? []).not.toContain('top');\n      expect(wrapper.emitted('input')?.flat() ?? []).toContain('hello');\n    });\n\n    it('should trigger on space', async() => {\n      const item = wrapper.findAll('ul li').filter(w => w.text() === 'hello')[0];\n\n      await item.trigger('keypress.space');\n      expect(Object.keys(wrapper.emitted())).toContain('input');\n      expect(wrapper.emitted('input')?.flat() ?? []).not.toContain('top');\n      expect(wrapper.emitted('input')?.flat() ?? []).toContain('hello');\n    });\n\n    it('should focus the first element by default', () => {\n      const options = wrapper.findAll('ul li');\n      const firstOption = options.filter(w => w.text() === 'hello')[0];\n      const document = wrapper.element.ownerDocument;\n\n      expect(document.activeElement).toBe(firstOption.element);\n    });\n\n    it('should focus on mouse over', async() => {\n      const options = wrapper.findAll('ul li');\n      const secondOption = options.filter(w => w.text() === 'world')[0];\n      const document = wrapper.element.ownerDocument;\n\n      expect(document.activeElement).not.toBe(secondOption.element);\n      await secondOption.trigger('mouseover');\n      expect(document.activeElement).toBe(secondOption.element);\n    });\n\n    it('should respond to arrow-up key', async() => {\n      const options = wrapper.findAll('ul li');\n      const document = wrapper.element.ownerDocument;\n      const lastOption = options[options.length - 1];\n      const secondLastOption = options[options.length - 2];\n\n      await lastOption.trigger('mouseover');\n      expect(document.activeElement).toBe(lastOption.element);\n      await lastOption.trigger('keydown', { key: 'ArrowUp' });\n      expect(document.activeElement).toBe(secondLastOption.element);\n    });\n\n    it('should respond to arrow-down key', async() => {\n      const options = wrapper.findAll('ul li');\n      const document = wrapper.element.ownerDocument;\n      const firstOption = options[0];\n      const secondOption = options[1];\n\n      expect(document.activeElement).toBe(firstOption.element);\n      await firstOption.trigger('keydown', { key: 'ArrowDown' });\n      expect(document.activeElement).toBe(secondOption.element);\n    });\n\n    it('should respond to home key', async() => {\n      const options = wrapper.findAll('ul li');\n      const document = wrapper.element.ownerDocument;\n      const firstOption = options[0];\n      const secondOption = options[1];\n\n      await secondOption.trigger('mouseover');\n      expect(document.activeElement).toBe(secondOption.element);\n      await secondOption.trigger('keydown', { key: 'Home' });\n      expect(document.activeElement).toBe(firstOption.element);\n    });\n\n    it('should respond to end key', async() => {\n      const options = wrapper.findAll('ul li');\n      const document = wrapper.element.ownerDocument;\n      const firstOption = options[0];\n      const lastOption = options[options.length - 1];\n\n      expect(document.activeElement).not.toBe(lastOption.element);\n      await firstOption.trigger('keydown', { key: 'End' });\n      expect(document.activeElement).toBe(lastOption.element);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/components/form/labeled-select-utils/labeled-select-pagination.ts",
    "content": "import { debounce } from 'lodash';\nimport { PropType, defineComponent } from 'vue';\nimport { ComputedOptions, MethodOptions } from 'vue/types/v3-component-options';\n\nimport { LabelSelectPaginateFn, LABEL_SELECT_NOT_OPTION_KINDS, LABEL_SELECT_KINDS } from '@pkg/types/components/labeledSelect';\n\ninterface Props {\n  paginate?: LabelSelectPaginateFn\n}\n\ninterface Data {\n  currentPage: number,\n  search:      string,\n  pageSize:    number,\n\n  page:         any[],\n  pages:        number,\n  totalResults: number,\n\n  paginating: boolean,\n\n  debouncedRequestPagination: Function\n}\n\ninterface Computed extends ComputedOptions {\n  canPaginate: () => boolean,\n\n  canLoadMore: () => boolean,\n\n  optionsInPage: () => number,\n\n  optionCounts: () => string,\n}\n\ninterface Methods extends MethodOptions {\n  loadMore:            () => void\n  setPaginationFilter: (filter: string) => void\n  requestPagination:   () => Promise<any>;\n}\n\n/**\n * 'mixin' to provide pagination support to LabeledSelect\n */\nexport default defineComponent<Props, any, Data, Computed, Methods>({\n  props: {\n    paginate: {\n      default: null,\n      type:    Function as PropType<LabelSelectPaginateFn>,\n    },\n\n    inStore: {\n      type:    String,\n      default: 'cluster',\n    },\n\n    /**\n     * Resource to show\n     */\n    resourceType: {\n      type:    String,\n      default: null,\n    },\n  },\n\n  data(): Data {\n    return {\n      // Internal\n      currentPage: 1,\n      search:      '',\n      pageSize:    10,\n      pages:       0,\n\n      debouncedRequestPagination: debounce(this.requestPagination, 700),\n\n      // External\n      page:         [],\n      totalResults: 0,\n      paginating:   false,\n    };\n  },\n\n  async mounted() {\n    if (this.canPaginate) {\n      await this.requestPagination();\n    }\n  },\n\n  computed: {\n    canPaginate() {\n      return !!this.paginate && !!this.resourceType && this.$store.getters[`${ this.inStore }/paginationEnabled`](this.resourceType);\n    },\n\n    canLoadMore() {\n      return this.pages > this.currentPage;\n    },\n\n    optionsInPage() {\n      // Number of genuine options (not groups, dividers, etc)\n      return this.canPaginate\n        ? this._options.filter((o: any) => {\n          return o.kind !== LABEL_SELECT_KINDS.NONE && !LABEL_SELECT_NOT_OPTION_KINDS.includes(o.kind);\n        }).length\n        : 0;\n    },\n\n    optionCounts() {\n      if (!this.canPaginate || this.optionsInPage === this.totalResults) {\n        return '';\n      }\n\n      return this.$store.getters['i18n/t']('labelSelect.pagination.counts', {\n        count:      this.optionsInPage,\n        totalCount: this.totalResults,\n      });\n    },\n  },\n\n  methods: {\n    loadMore() {\n      this.currentPage++;\n      this.requestPagination();\n    },\n\n    setPaginationFilter(filter: string) {\n      this.paginating = true; // Do this before debounce\n      this.currentPage = 1;\n      this.search = filter;\n      this.debouncedRequestPagination(true);\n    },\n\n    async requestPagination(resetPage = false) {\n      this.paginating = true;\n      const paginate: LabelSelectPaginateFn = this.paginate as LabelSelectPaginateFn; // Checking is done via prop\n\n      const {\n        page,\n        pages,\n        total,\n      } = await paginate({\n        resetPage,\n        pageContent: this.page || [],\n        page:        this.currentPage,\n        filter:      this.search,\n        pageSize:    this.pageSize,\n      });\n\n      this.page = page;\n      this.pages = pages || 0;\n      this.totalResults = total || 0;\n\n      this.paginating = false;\n    },\n  },\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/config/__tests__/commandLineOptions.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\n\nimport { jest } from '@jest/globals';\nimport _ from 'lodash';\n\nimport * as settings from '@pkg/config/settings';\nimport { TransientSettings } from '@pkg/config/transientSettings';\nimport clone from '@pkg/utils/clone';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\nimport { RecursiveKeys } from '@pkg/utils/typeUtils';\n\nmockModules({ '@pkg/utils/logging': undefined });\n\nconst { getObjectRepresentation, LockedFieldError, updateFromCommandLine } = await import('@pkg/config/commandLineOptions');\n\ndescribe('commandLineOptions', () => {\n  let prefs: settings.Settings;\n  let origPrefs: settings.Settings;\n  let lockedSettings: settings.LockedSettingsType = {};\n\n  beforeEach(() => {\n    jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { });\n    prefs = _.merge({}, settings.defaultSettings, {\n      application:     { telemetry: { enabled: true } },\n      containerEngine: {\n        allowedImages: {\n          enabled:  false,\n          patterns: [],\n        },\n        name: settings.ContainerEngine.MOBY,\n      },\n      kubernetes: {\n        version: '1.29.5',\n        port:    6443,\n        enabled: true,\n        options: {\n          traefik: true,\n          flannel: false,\n        },\n      },\n      portForwarding: { includeKubernetesServices: false },\n    });\n    origPrefs = clone(prefs);\n    lockedSettings = { };\n  });\n\n  describe('updateFromCommandLine', () => {\n    describe('with locked fields', () => {\n      let enabledOptionChange: string;\n      let enabledOptionSame: string;\n      let lockedFields: settings.LockedSettingsType;\n\n      beforeEach(() => {\n        lockedFields = { containerEngine: { allowedImages: { enabled: true } } };\n        enabledOptionChange = `--containerEngine.allowedImages.enabled=${ !prefs.containerEngine.allowedImages.enabled }`;\n        enabledOptionSame = `--containerEngine.allowedImages.enabled=${ prefs.containerEngine.allowedImages.enabled }`;\n      });\n\n      test('disallows changing allowedImages.enabled when locked', () => {\n        expect(() => {\n          updateFromCommandLine(prefs, lockedFields, [enabledOptionChange]);\n        }).toThrow(LockedFieldError);\n      });\n\n      test(\"doesn't complain when not changing fields\", () => {\n        expect(() => {\n          updateFromCommandLine(prefs, lockedFields, [enabledOptionSame]);\n        }).not.toThrow();\n      });\n    });\n\n    test('no command-line args should leave prefs unchanged', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, []);\n\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('one option with embedded equal sign should change only one value', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.version=1.29.6']);\n\n      expect(newPrefs.kubernetes.version).toBe('1.29.6');\n      newPrefs.kubernetes.version = origPrefs.kubernetes.version;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('one option over two args should change only one value', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.version', '1.29.7']);\n\n      expect(newPrefs.kubernetes.version).toBe('1.29.7');\n      newPrefs.kubernetes.version = origPrefs.kubernetes.version;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('boolean option to true should change only that value', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.options.flannel=true']);\n\n      expect(origPrefs.kubernetes.options.flannel).toBeFalsy();\n      expect(newPrefs.kubernetes.options.flannel).toBeTruthy();\n      newPrefs.kubernetes.options.flannel = false;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('boolean option set to implicit true should change only that value', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.options.flannel']);\n\n      expect(origPrefs.kubernetes.options.flannel).toBeFalsy();\n      expect(newPrefs.kubernetes.options.flannel).toBeTruthy();\n      newPrefs.kubernetes.options.flannel = false;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('boolean option set to false should change only that value', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.options.traefik=false']);\n\n      expect(origPrefs.kubernetes.options.traefik).toBeTruthy();\n      expect(newPrefs.kubernetes.options.traefik).toBeFalsy();\n      newPrefs.kubernetes.options.traefik = true;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('changes specified options', () => {\n      const optionsByPlatform: Record<string, (string | [string, string])[]> = {\n        win32: [\n          '--experimental.virtualMachine.proxy.enabled',\n        ],\n        '!win32': [\n          '--application.adminAccess',\n          ['--application.pathManagementStrategy', 'rcfiles'],\n          '--virtualMachine.memoryInGB',\n          '--virtualMachine.numberCPUs',\n        ],\n        '*': [\n          '--application.debug',\n          '--application.telemetry.enabled',\n          '--application.updater.enabled',\n          '--application.autoStart',\n          '--application.startInBackground',\n          '--application.hideNotificationIcon',\n          '--application.window.quitOnClose',\n          '--containerEngine.allowedImages.enabled',\n          ['--containerEngine.name', 'containerd'],\n          '--kubernetes.port',\n          '--kubernetes.enabled',\n          '--kubernetes.options.traefik',\n          '--kubernetes.options.flannel',\n          '--portForwarding.includeKubernetesServices',\n          '--images.showAll',\n          ['--images.namespace', 'mangos'],\n          '--diagnostics.showMuted',\n        ],\n      };\n\n      for (const platform in optionsByPlatform) {\n        const options: (string | [string, string])[] = optionsByPlatform[platform as 'win32' | 'linux' | 'darwin' | '*'];\n\n        for (const entry of options) {\n          let option: string;\n          let newValue: string | boolean | number | undefined;\n\n          if (Array.isArray(entry)) {\n            option = entry[0];\n            newValue = entry[1];\n          } else {\n            option = entry;\n          }\n          const accessor = option.substring(2);\n          const oldValue: string | boolean | number | undefined = _.get(origPrefs, accessor);\n\n          expect(oldValue).not.toBeUndefined();\n          if (newValue === undefined) {\n            switch (typeof oldValue) {\n            case 'boolean':\n              newValue = !oldValue;\n              break;\n            case 'number':\n              newValue = oldValue + 1;\n              break;\n            default:\n              expect(['boolean', 'number']).toContain(typeof oldValue);\n            }\n          }\n          const newOption = `${ option }=${ newValue }`;\n          let expectUpdateToPass: boolean;\n\n          if (platform === '*') {\n            expectUpdateToPass = true;\n          } else if (platform === '!win32') {\n            // These aren't supported on Windows\n            expectUpdateToPass = os.platform() !== 'win32';\n          } else {\n            // And these are platform-specific\n            expectUpdateToPass = platform === os.platform();\n          }\n          if (expectUpdateToPass) {\n            const newPrefs = updateFromCommandLine(prefs, lockedSettings, [newOption]);\n\n            expect(_.get(newPrefs, accessor)).toEqual(newValue);\n            _.set(newPrefs, accessor, oldValue);\n            expect(newPrefs).toEqual(origPrefs);\n          } else {\n            expect(() => {\n              updateFromCommandLine(prefs, lockedSettings, [newOption]);\n            }).toThrow(`Changing field \"${ accessor }\" via the API isn't supported`);\n          }\n        }\n      }\n    });\n\n    test('nothing after an = should set target to empty string', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, ['--images.namespace=']);\n\n      expect(origPrefs.images.namespace).not.toBe('');\n      expect(newPrefs.images.namespace).toBe('');\n      newPrefs.images.namespace = origPrefs.images.namespace;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('should change several values (and no others)', () => {\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, [\n        '--kubernetes.options.traefik=false',\n        '--application.telemetry.enabled=false',\n        '--portForwarding.includeKubernetesServices=true',\n        '--containerEngine.name=containerd',\n        '--kubernetes.port', '6444',\n      ]);\n\n      expect(newPrefs.kubernetes.options.traefik).toBeFalsy();\n      expect(newPrefs.application.telemetry.enabled).toBeFalsy();\n      expect(newPrefs.portForwarding.includeKubernetesServices).toBeTruthy();\n      expect(newPrefs.containerEngine.name).toBe('containerd');\n      expect(newPrefs.kubernetes.port).toBe(6444);\n\n      newPrefs.kubernetes.options.traefik = true;\n      newPrefs.application.telemetry.enabled = true;\n      newPrefs.portForwarding.includeKubernetesServices = false;\n      newPrefs.containerEngine.name = settings.ContainerEngine.MOBY;\n      newPrefs.kubernetes.port = 6443;\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('should ignore non-option arguments', () => {\n      const arg = 'doesnt.start.with.dash.dash=some-value';\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, [arg]);\n\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('should ignore an unrecognized option', () => {\n      const arg = '--kubernetes.zipperhead';\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, [arg]);\n\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('should ignore leading options and arguments', () => {\n      const args = ['--kubernetes.zipperhead', '--another.unknown.option', 'its.argument', '--dont.know.what.this.is.either'];\n      const newPrefs = updateFromCommandLine(prefs, lockedSettings, args);\n\n      expect(TransientSettings.value.noModalDialogs).toEqual(false);\n      expect(newPrefs).toEqual(origPrefs);\n    });\n\n    test('should complain about an unrecognized option after a recognized one', () => {\n      const args = ['--ignore.this.one', '--kubernetes.enabled', '--complain.about.this'];\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, args);\n      }).toThrow(`Can't evaluate command-line argument ${ args[2] } -- no such entry in current settings`);\n    });\n\n    test('should complain about non-options after recognizing an option', () => {\n      const args = ['--kubernetes.enabled', 'doesnt.start.with.dash.dash=some-value'];\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, args);\n      }).toThrow(`Unexpected argument '${ args[1] }'`);\n    });\n\n    test('should refuse to overwrite a non-leaf node', () => {\n      const arg = '--kubernetes.options';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, [arg, '33']);\n      }).toThrow(`Can't overwrite existing setting ${ arg }`);\n    });\n\n    test('should complain about a missing string value', () => {\n      const arg = '--kubernetes.version';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, [arg]);\n      }).toThrow(`No value provided for option ${ arg }`);\n    });\n\n    test('should complain about a missing numeric value', () => {\n      const arg = '--virtualMachine.memoryInGB';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, ['--kubernetes.version', '1.2.3', arg]);\n      }).toThrow(`No value provided for option ${ arg }`);\n    });\n\n    test('should complain about a non-boolean value', () => {\n      const arg = '--kubernetes.enabled';\n      const value = 'nope';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, [`${ arg }=${ value }`]);\n      }).toThrow(`Can't evaluate ${ arg }=${ value } as boolean`);\n    });\n\n    test('should complain about a non-numeric value', () => {\n      const arg = '--kubernetes.port';\n      const value = 'angeles';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, [`${ arg }=${ value }`]);\n      }).toThrow(`Can't evaluate ${ arg }=${ value } as number: SyntaxError: Unexpected token 'a', \\\"angeles\\\" is not valid JSON`);\n    });\n\n    test('should complain about type mismatches', () => {\n      const optionList = [\n        ['--virtualMachine.memoryInGB', 'true', 'boolean', 'number'],\n        ['--kubernetes.enabled', '7', 'number', 'boolean'],\n      ];\n\n      for (const [arg, finalValue, currentType, desiredType] of optionList) {\n        expect(() => {\n          updateFromCommandLine(prefs, lockedSettings, [`${ arg }=${ finalValue }`]);\n        }).toThrow(`Type of '${ finalValue }' is ${ currentType }, but current type of ${ arg.substring(2) } is ${ desiredType } `);\n      }\n    });\n  });\n\n  describe('--no-modal-dialogs', () => {\n    test('sets the value accordingly', () => {\n      TransientSettings.update({ noModalDialogs: false });\n      updateFromCommandLine(prefs, lockedSettings, ['--no-modal-dialogs']);\n      expect(TransientSettings.value.noModalDialogs).toBeTruthy();\n      TransientSettings.update({ noModalDialogs: false });\n      updateFromCommandLine(prefs, lockedSettings, ['--no-modal-dialogs=true']);\n      expect(TransientSettings.value.noModalDialogs).toBeTruthy();\n      updateFromCommandLine(prefs, lockedSettings, ['--no-modal-dialogs=false']);\n      expect(TransientSettings.value.noModalDialogs).toBeFalsy();\n    });\n\n    test('complains about an invalid argument', () => {\n      const arg = '--no-modal-dialogs=42';\n\n      expect(() => {\n        updateFromCommandLine(prefs, lockedSettings, [arg]);\n      }).toThrow(`Invalid associated value for ${ arg }: must be unspecified (set to true), true or false`);\n    });\n  });\n\n  describe('getObjectRepresentation', () => {\n    test('handles more than 2 dots', () => {\n      expect(getObjectRepresentation('a.b.c.d' as RecursiveKeys<settings.Settings>, 3))\n        .toMatchObject({ a: { b: { c: { d: 3 } } } });\n    });\n    test('handles 2 dots', () => {\n      expect(getObjectRepresentation('a.b.c' as RecursiveKeys<settings.Settings>, false))\n        .toMatchObject({ a: { b: { c: false } } });\n    });\n    test('handles 1 dot', () => {\n      expect(getObjectRepresentation('first.last' as RecursiveKeys<settings.Settings>, 'middle'))\n        .toMatchObject({ first: { last: 'middle' } });\n    });\n    test('handles 0 dots', () => {\n      expect(getObjectRepresentation('version', 4))\n        .toMatchObject({ version: 4 });\n    });\n    test('complains about an invalid accessor', () => {\n      expect(() => {\n        getObjectRepresentation('application.' as RecursiveKeys<settings.Settings>, 4);\n      }).toThrow(\"Unrecognized command-line option ends with a dot ('.')\");\n    });\n    test('complains about an empty-string accessor', () => {\n      expect(() => {\n        getObjectRepresentation('' as RecursiveKeys<settings.Settings>, 4);\n      }).toThrow(\"Invalid command-line option: can't be the empty string.\");\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/config/__tests__/settings.spec.ts",
    "content": "/** @jest-environment node */\n/* eslint object-curly-newline: [\"error\", {\"consistent\": true}] */\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { jest } from '@jest/globals';\nimport _ from 'lodash';\nimport plist from 'plist';\n\nimport * as settings from '../settings';\nimport * as settingsImpl from '../settingsImpl';\n\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport paths from '@pkg/utils/paths';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nclass FakeFSError extends Error {\n  public message = '';\n  public code = '';\n  constructor(message: string, code: string) {\n    super(message);\n    this.message = message;\n    this.code = code;\n  }\n}\n\nenum ProfileTypes {\n  None = 'none',\n  Unlocked = 'unlocked',\n  Locked = 'locked',\n}\n\nconst actualSyncReader = fs.readFileSync;\nconst modules = mockModules({\n  fs: {\n    ...fs,\n    readFileSync: jest.spyOn(fs, 'readFileSync'),\n  },\n});\n\nconst { readDeploymentProfiles } = await import('@pkg/main/deploymentProfiles');\n\ndescribe('settings', () => {\n  describe('merge', () => {\n    test('merges plain objects', () => {\n      const input = {\n        a: 1,\n        b: {\n          c: 2, d: 3, e: { f: 4 },\n        },\n      };\n      const changes = { a: 10, b: { c: 20, e: { } } };\n      const result = settingsImpl.merge(input, changes);\n\n      expect(result).toEqual({\n        a: 10,\n        b: {\n          c: 20, d: 3, e: { f: 4 },\n        },\n      },\n      );\n    });\n    test('replaces arrays of primitives', () => {\n      const input = {\n        a: [1, 2, 3, 4, 5], b: 3, c: 5,\n      };\n      const changes = { a: [1, 3, 5, 7], b: 4 };\n      const result = settingsImpl.merge(input, changes);\n\n      expect(result).toEqual({\n        a: [1, 3, 5, 7], b: 4, c: 5,\n      });\n    });\n    test('removes values set to undefined', () => {\n      const input = { a: 1, b: { c: 3, d: 4 } };\n      const changes = { b: { c: undefined } };\n      const result = settingsImpl.merge(input, changes);\n\n      expect(result).toEqual({ a: 1, b: { d: 4 } });\n    });\n    test('returns merged settings', () => {\n      const input = { a: 1 };\n      const changes = { a: 2 };\n      const result = settingsImpl.merge(input, changes);\n\n      expect(result).toBe(input);\n      expect(input).toEqual({ a: 2 });\n    });\n  });\n\n  const fullDefaults = {\n    version:     settings.CURRENT_SETTINGS_VERSION,\n    debug:       true,\n    application: {\n      adminAccess:            false,\n      pathManagementStrategy: 'rcfiles',\n      window:                 { quitOnClose: true },\n      extensions:             {\n        installed: {\n          bellingham: 'A',\n          seattle:    'B',\n          olympia:    'C',\n          winthrop:   'D',\n        },\n      },\n    },\n    containerEngine: {\n      allowedImages: {\n        enabled:  true,\n        patterns: [],\n      },\n      mobyStorageDriver: 'auto',\n      name:              'moby',\n    },\n    kubernetes: {\n      version: '1.29.15',\n      enabled: true,\n    },\n    WSL: {\n      integrations: {\n        kingston: false,\n        napanee:  false,\n        yarker:   true,\n        weed:     true,\n      },\n    },\n    portForwarding: { includeKubernetesServices: false },\n    diagnostics:    {\n      showMuted:   false,\n      locked:      true,\n      mutedChecks: {\n        montreal:          true,\n        'riviere du loup': false,\n        magog:             false,\n      },\n    },\n    ignorableTestSettings: {\n      testTitle:  'test-title',\n      testStruct: {\n        title:     'tests-struct',\n        subStruct: {\n          title:  'sub-title',\n          locked: true,\n          subvar: 'sub-var',\n        },\n      },\n    },\n  };\n\n  const jsonProfile = JSON.stringify(fullDefaults);\n  const plistProfile = plist.build(fullDefaults);\n  const unlockedProfile = {\n    version:         11,\n    ignoreThis:      { soups: ['beautiful', 'vichyssoise'] },\n    containerEngine: { name: 'moby' },\n    kubernetes:      { version: '1.25.9' },\n  };\n  const lockedProfile = {\n    version:         11,\n    ignoreThis:      { soups: ['beautiful', 'vichyssoise'] },\n    containerEngine: {\n      allowedImages: {\n        enabled:  true,\n        patterns: ['nginx', 'alpine'],\n      },\n    },\n    kubernetes: { version: '1.25.9' },\n  };\n  const unlockedJSONProfile = JSON.stringify(unlockedProfile);\n  const lockedJSONProfile = JSON.stringify(lockedProfile);\n  const unlockedPlistProfile = plist.build(unlockedProfile);\n  const lockedPlistProfile = plist.build(lockedProfile);\n\n  // Check structural breakage in this file.\n  const brokenJSONProfile = jsonProfile.slice(0, jsonProfile.length / 2);\n  const brokenPlistProfile = plistProfile.slice(0, plistProfile.length / 2);\n\n  // TODO: Figure out how to implement this on Windows as well\n  const describeNotWindows = process.platform === 'win32' ? describe.skip : describe;\n\n  describeNotWindows('profiles', () => {\n    const lockedAccessors = ['containerEngine.allowedImages.enabled', 'containerEngine.allowedImages.patterns'];\n\n    beforeEach(() => {\n      jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { });\n      settingsImpl.clearSettings();\n    });\n    afterEach(() => {\n      modules.fs.readFileSync.mockRestore();\n    });\n\n    /**\n     * This mocker is used to intercept a call to `fs.readFileSync` and return a specified profile text,\n     * depending on how the mocker is configured.\n     *\n     * The mocker can do four different things when it's triggered via a call to `fs.readFileSync`:\n     *\n     * 1. Return the actual text of the requested file\n     * 2. Throw an ENOENT exception\n     * 3. Return a pre-determined default profile\n     * 4. Return a pre-determined locked-field profile\n     *\n     * The mocker always does option 1 for any file it doesn't care about\n     * The mocker always does option 2 for settings.json to trigger use of any default profile\n     * For requests for profile files, the mocker looks at its main arguments\n     * `useSystemProfile`, and `usePersonalProfile` to determine whether it should return some\n     * predetermined text or throw an ENOENT exception. For example, if `useSystemProfile` is `ProfileTypes.None`,\n     * then any requests for a system profile file should trigger an ENOENT exception.\n     *\n     * This is why `createMocker(ProfileTypes.None, ProfileTypes.None) will throw an exception when asked for\n     * the text of any profile.\n     *\n     * And if we want to verify that the system handles invalid files correctly, the `typeToCorrupt` field\n     * is used to indicate whether to corrupt a defaults or locked-fields profile. Corrupting involves returning\n     * the first half of the predefined text, which causes both json and plist parsers to throw an exception.\n     *\n     * On Linux, system files are (currently) `/etc/rancher-desktop/{defaults,locked}.json`,\n     * while the user files are  `~/.config/rancher-desktop.{defaults,locked}.json`\n     * The alternate system location is `/usr/etc/rancher-desktop/{defaults,locked}.json`.\n     *\n     * macOS plist files:\n     * User: `~/Library/Preferences/io.rancherdesktop.profile.{defaults,locked}.plist`\n     * System: `/Library/Managed Preferences/io.rancherdesktop.profile.{defaults,locked}.plist`\n     * AltSystem: `/Library/Preferences/io.rancherdesktop.profile.{defaults,locked}.plist`\n     *\n     * A request for the AltSystem profile gets the same response as a request for the System profile.\n     *\n     * @param useSystemProfile: what to do when a system profile is requested\n     * @param usePersonalProfile:  what to do when a user profile is requested\n     * @param typeToCorrupt: if 'defaults', when a defaults profile is requested, just return the first half of the text.\n     *                       ... similar if it's 'locked'\n     */\n    function createMocker(useSystemProfile: ProfileTypes, usePersonalProfile: ProfileTypes, typeToCorrupt?: 'defaults' | 'locked'): (inputPath: any, unused: any) => any {\n      return (inputPath: any, unused: any): any => {\n        const profilePaths = [paths.deploymentProfileUser, paths.deploymentProfileSystem, paths.altDeploymentProfileSystem];\n        if (!profilePaths.some(p => inputPath.startsWith(p))) {\n          return actualSyncReader(inputPath, unused);\n        }\n        const action = inputPath.startsWith(paths.deploymentProfileUser) ? usePersonalProfile : useSystemProfile;\n\n        if (action === ProfileTypes.None || inputPath === path.join(paths.config, 'settings.json')) {\n          throw new FakeFSError(`File ${ inputPath } not found`, 'ENOENT');\n        }\n        const pathInfo = path.parse(inputPath);\n\n        if (!['.json', '.plist'].includes(pathInfo.ext)) {\n          return actualSyncReader(inputPath, unused);\n        }\n\n        if (pathInfo.base.endsWith('defaults.json')) {\n          return typeToCorrupt === 'defaults' ? brokenJSONProfile : jsonProfile;\n        }\n        if (pathInfo.base.endsWith('defaults.plist')) {\n          return typeToCorrupt === 'defaults' ? brokenPlistProfile : plistProfile;\n        }\n        switch (action) {\n        case ProfileTypes.Unlocked:\n          // These are effectively empty profiles because the validator removes all the fields.\n          // No need to sometimes emulate corruption and return only the first half of the data;\n          // this is done when requesting locked fields below.\n          if (pathInfo.base.endsWith('locked.json')) {\n            return unlockedJSONProfile;\n          } else if (pathInfo.base.endsWith('locked.plist')) {\n            return unlockedPlistProfile;\n          }\n          break;\n        case ProfileTypes.Locked:\n          if (pathInfo.base.endsWith('locked.json')) {\n            return typeToCorrupt === 'locked' ? brokenJSONProfile : lockedJSONProfile;\n          } else if (pathInfo.base.endsWith('locked.plist')) {\n            return typeToCorrupt === 'locked' ? brokenPlistProfile : lockedPlistProfile;\n          }\n        }\n        throw new Error(\"Shouldn't get here.\");\n      };\n    }\n\n    describe('validation', () => {\n      function invalidProfileMessage(basename: string) {\n        if (process.platform === 'darwin') {\n          return new RegExp(`Error loading plist file .*/io.rancherdesktop.profile.${ basename }.plist`);\n        }\n\n        return new RegExp(`Error parsing deployment profile from .*/\\\\.config/rancher-desktop.${ basename }.json: SyntaxError: Unterminated string in JSON at position`);\n      }\n      test('complains about invalid default values', async() => {\n        modules.fs.readFileSync\n          .mockImplementation(createMocker(ProfileTypes.None, ProfileTypes.Unlocked, 'defaults'));\n        await expect(readDeploymentProfiles()).rejects.toThrow(invalidProfileMessage('defaults'));\n      });\n      test('complains about invalid locked values', async() => {\n        modules.fs.readFileSync\n          .mockImplementation(createMocker(ProfileTypes.None, ProfileTypes.Locked, 'locked'));\n        await expect(readDeploymentProfiles()).rejects.toThrow(invalidProfileMessage('locked'));\n      });\n    });\n\n    describe('locked fields', () => {\n      function verifyAllFieldsAreLocked(lockedFields: settings.LockedSettingsType) {\n        for (const acc of lockedAccessors) {\n          expect(_.get(lockedFields, acc)).toBeTruthy();\n        }\n      }\n\n      function verifyAllFieldsAreUnlocked(lockedFields: settings.LockedSettingsType) {\n        for (const acc of lockedAccessors) {\n          expect(_.get(lockedFields, acc)).toBeFalsy();\n        }\n      }\n\n      describe('when there is no profile', () => {\n        beforeEach(() => {\n          modules.fs.readFileSync\n            .mockImplementation(createMocker(ProfileTypes.None, ProfileTypes.None));\n        });\n        test('all fields are unlocked', async() => {\n          const profiles = await readDeploymentProfiles();\n\n          settingsImpl.createSettings(profiles);\n          settingsImpl.updateLockedFields(profiles.locked);\n          verifyAllFieldsAreUnlocked(settingsImpl.getLockedSettings());\n        });\n      });\n      describe('when there is a profile', () => {\n        describe('all possible situations of (system,user) x (locked,unlocked)', () => {\n          const testCases: { system: ProfileTypes, user: ProfileTypes, shouldLock: boolean, msg: string }[] = [];\n\n          for (const system of Object.values(ProfileTypes)) {\n            for (const user of Object.values(ProfileTypes)) {\n              let shouldLock = system === ProfileTypes.Locked;\n\n              if (system === ProfileTypes.None) {\n                shouldLock = user === ProfileTypes.Locked;\n              }\n\n              const msg = shouldLock ? 'should lock' : 'should not lock';\n\n              testCases.push({\n                system, user, shouldLock, msg,\n              });\n            }\n          }\n          test.each(testCases)('system profile $system user profile $user $msg',\n            async({ system, user, shouldLock }) => {\n              modules.fs.readFileSync\n                .mockImplementation(createMocker(system, user));\n              const profiles = await readDeploymentProfiles();\n\n              settingsImpl.createSettings(profiles);\n              settingsImpl.updateLockedFields(profiles.locked);\n              if (shouldLock) {\n                verifyAllFieldsAreLocked(settingsImpl.getLockedSettings());\n              } else {\n                verifyAllFieldsAreUnlocked(settingsImpl.getLockedSettings());\n              }\n            });\n        });\n        describe('check profile reading', () => {\n          it('preserves hash-like settings', async() => {\n            modules.fs.readFileSync\n              .mockImplementation(createMocker(ProfileTypes.None, ProfileTypes.Unlocked));\n            const profiles = await readDeploymentProfiles();\n            const expectedDefaults = _.omit(fullDefaults, ['debug', 'ignorableTestSettings', 'diagnostics.locked']);\n            const expected: RecursivePartial<settings.Settings> = {\n              version:         settings.CURRENT_SETTINGS_VERSION,\n              containerEngine: {\n                name: settings.ContainerEngine.MOBY,\n              },\n              experimental: {\n              },\n              kubernetes: {\n                version: '1.25.9',\n              },\n            };\n\n            expect(profiles.locked).toEqual(expected);\n            expect(profiles.defaults).toEqual(expectedDefaults);\n          });\n        });\n      });\n    });\n  });\n\n  describe('lockableFields', () => {\n    test('flattens an object with only allowed-image settings', () => {\n      const lockedSettings = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: [\"Shouldn't see this\"],\n          },\n        },\n      };\n      const expectedLockedFields = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: true,\n          },\n        },\n      };\n      const calculatedLockedFields = settingsImpl.determineLockedFields(lockedSettings);\n\n      expect(calculatedLockedFields).toEqual(expectedLockedFields);\n    });\n    test('flattens a complex object', () => {\n      const lockedSettings = {\n        virtualMachine: {\n          memoryInGB: 2,\n          numberCPUs: 2,\n        },\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: [\"Shouldn't see this\"],\n          },\n        },\n        kubernetes: { version: '1.2.3' },\n      };\n      const expectedLockedFields = {\n        virtualMachine: {\n          memoryInGB: true,\n          numberCPUs: true,\n        },\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: true,\n          },\n        },\n        kubernetes: { version: true },\n      };\n      const calculatedLockedFields = settingsImpl.determineLockedFields(lockedSettings);\n\n      expect(calculatedLockedFields).toEqual(expectedLockedFields);\n    });\n    test('flattens an empty object', () => {\n      const lockedSettings = { };\n      const expectedLockedFields = { };\n      const calculatedLockedFields = settingsImpl.determineLockedFields(lockedSettings);\n\n      expect(calculatedLockedFields).toEqual(expectedLockedFields);\n    });\n  });\n\n  describe('migrations', () => {\n    it(\"complains about empty settings because there's no version field\", () => {\n      const s: RecursivePartial<settings.Settings> = {};\n\n      expect(() => {\n        settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false);\n      }).toThrow('updating settings requires specifying an API version, but no version was specified');\n    });\n\n    it('complains about a non-numeric version field', () => {\n      const s: RecursivePartial<settings.Settings> = { version: 'no way' as unknown as typeof settings.CURRENT_SETTINGS_VERSION };\n\n      expect(() => {\n        settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false);\n      }).toThrow('updating settings requires specifying an API version, but \"no way\" is not a proper config version');\n    });\n\n    it('complains about a negative version field', () => {\n      const s: RecursivePartial<settings.Settings> = { version: -7 as unknown as typeof settings.CURRENT_SETTINGS_VERSION };\n\n      expect(() => {\n        settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false);\n      }).toThrow('updating settings requires specifying an API version, but \"-7\" is not a positive number');\n    });\n\n    it('correctly migrates version-9 no-proxy settings', () => {\n      const s: RecursivePartial<settings.Settings> = {\n        version:      9 as typeof settings.CURRENT_SETTINGS_VERSION,\n        experimental: {\n          virtualMachine: {\n            proxy: {\n              noproxy: [' ', '  1.2.3.4   ', '   ', '11.12.13.14  ', '    21.22.23.24'],\n            },\n          },\n        },\n      };\n      const expected: RecursivePartial<settings.Settings> = {\n        version:      settings.CURRENT_SETTINGS_VERSION,\n        experimental: {\n          virtualMachine: {\n            proxy: {\n              noproxy: ['1.2.3.4', '11.12.13.14', '21.22.23.24'],\n            },\n          },\n        },\n      };\n\n      expect(settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false)).toMatchObject(expected);\n    });\n\n    it('correctly migrates earlier no-proxy settings', () => {\n      /**\n       * This test verifies that we're no longer running into problems when\n       * the migrator tries to access the value of a nonexistent property.\n       *\n       * The bug, issue 5618, was that the migrator erroneously assumed\n       * that when users were migrating to version N, they were submitting a settings file\n       * that was based on the default settings of version N - 1.\n       */\n      const s: RecursivePartial<settings.Settings> = {\n        version:      1 as typeof settings.CURRENT_SETTINGS_VERSION,\n        experimental: {\n          virtualMachine: {\n            proxy: {\n              noproxy: [' ', '  1.2.3.4   ', '   ', '11.12.13.14  ', '    21.22.23.24'],\n            },\n          },\n        },\n      };\n      const expected: RecursivePartial<settings.Settings> = {\n        version:      settings.CURRENT_SETTINGS_VERSION,\n        experimental: {\n          virtualMachine: {\n            proxy: {\n              noproxy: ['1.2.3.4', '11.12.13.14', '21.22.23.24'],\n            },\n          },\n        },\n      };\n\n      expect(settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false)).toMatchObject(expected);\n    });\n\n    it('leaves unrecognized settings unchanged', () => {\n      const s: Record<string, any> = {\n        version:        1 as typeof settings.CURRENT_SETTINGS_VERSION,\n        registeredCows: '2021-05-17T08:57:17 +07:00',\n        fluentLatitude: -55.753309,\n        grouchyTags:    [\n          'moll',\n          'in',\n          'excitation',\n        ],\n        funnyFriends: [\n          {\n            id:   0,\n            name: 'Terry Serrano',\n          },\n          {\n            id:   1,\n            name: 'Reynolds Rogers',\n          },\n        ],\n        niceGreeting:  'Hello, Bates Middleton! You have 10 unread messages.',\n        favoriteFruit: 'banana',\n      };\n      const expected = _.merge({}, s, { version: settings.CURRENT_SETTINGS_VERSION });\n\n      expect(settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false)).toMatchObject(expected);\n    });\n\n    it('updates all old settings going back to version 1', () => {\n      const s: Record<string, any> = {\n        version:    1 as typeof settings.CURRENT_SETTINGS_VERSION,\n        kubernetes: {\n          rancherMode:     true,\n          suppressSudo:    true,\n          containerEngine: 'moby',\n          hostResolver:    true,\n          memoryInGB:      30,\n          numberCPUs:      200,\n          WSLIntegrations: {\n            Ubuntu:   true,\n            Debian:   false,\n            openSUSE: true,\n          },\n          experimental: {\n            socketVMNet: true,\n          },\n        },\n        debug:                  true,\n        pathManagementStrategy: PathManagementStrategy.Manual,\n        telemetry:              false,\n        updater:                true,\n      };\n      const expected: RecursivePartial<settings.Settings> = {\n        version:     settings.CURRENT_SETTINGS_VERSION,\n        application: {\n          adminAccess:            false,\n          debug:                  true,\n          pathManagementStrategy: PathManagementStrategy.Manual,\n          telemetry:              {\n            enabled: false,\n          },\n          updater: {\n            enabled: true,\n          },\n        },\n        containerEngine: {\n          mobyStorageDriver: 'auto',\n          name:              settings.ContainerEngine.MOBY,\n        },\n        experimental: {\n        },\n        kubernetes:     {},\n        virtualMachine: {\n          memoryInGB: 30,\n          numberCPUs: 200,\n        },\n        WSL: {\n          integrations: {\n            Ubuntu:   true,\n            Debian:   false,\n            openSUSE: true,\n          },\n        },\n      };\n\n      expect(settingsImpl.migrateSpecifiedSettingsToCurrentVersion(s, false)).toEqual(expected);\n    });\n\n    describe('migrates from step to step', () => {\n      const expectedMigrations: Record<number, [any, any]> = {\n        1: [\n          {\n            kubernetes: {\n              rancherMode: 'cattle',\n            },\n          },\n          {\n            kubernetes: {},\n          },\n        ],\n        2: [{ cows: 4 }, { cows: 4 }],\n        3: [{ fish: 5 }, { fish: 5 }],\n        4: [\n          {\n            kubernetes: {\n              suppressSudo: true,\n              memoryInGB:   300,\n              numberCPUs:   45,\n              experimental: {\n                socketVMNet: true,\n              },\n              WSLIntegrations: {\n                ubuntu: true,\n                debian: false,\n              },\n              containerEngine: settings.ContainerEngine.MOBY,\n            },\n            debug:                  true,\n            pathManagementStrategy: 'manual',\n            telemetry:              true,\n            updater:                true,\n          },\n          {\n            kubernetes:  {},\n            application: {\n              adminAccess:            false,\n              debug:                  true,\n              pathManagementStrategy: PathManagementStrategy.Manual,\n              telemetry:              { enabled: true },\n              updater:                { enabled: true },\n            },\n            virtualMachine: {\n              memoryInGB: 300,\n              numberCPUs: 45,\n            },\n            experimental: {\n              virtualMachine: {\n                socketVMNet: true,\n              },\n            },\n            WSL: {\n              integrations: {\n                ubuntu: true,\n                debian: false,\n              },\n            },\n            containerEngine: {\n              name: settings.ContainerEngine.MOBY,\n            },\n          },\n        ],\n        5: [\n          {\n            containerEngine: {\n              imageAllowList: {\n                enabled:  true,\n                patterns: ['wolves', 'lower'],\n              },\n            },\n            virtualMachine: {\n              experimental: {\n                socketVMNet: true,\n              },\n            },\n            autoStart:            true,\n            hideNotificationIcon: true,\n            window:               false,\n          },\n          {\n            containerEngine: {\n              allowedImages: {\n                enabled:  true,\n                patterns: ['wolves', 'lower'],\n              },\n            },\n            experimental: {\n              virtualMachine: { socketVMNet: true },\n            },\n            application: {\n              autoStart:            true,\n              hideNotificationIcon: true,\n              window:               false,\n            },\n            virtualMachine: {},\n          },\n        ],\n        6: [\n          {\n            extensions: {\n              'mice:oldest':   true,\n              'cats:youngest': false,\n            },\n          },\n          {\n            extensions: {\n              mice: 'oldest',\n            },\n          },\n        ],\n        7: [\n          { application: { pathManagementStrategy: 'notset' } },\n          {\n            application: {\n              pathManagementStrategy: process.platform === 'win32' ? PathManagementStrategy.Manual : PathManagementStrategy.RcFiles,\n            },\n          },\n        ],\n        8: [\n          {\n            extensions: { mice: 'oldest' },\n          },\n          {\n            application: {\n              extensions: {\n                installed: { mice: 'oldest' },\n              },\n            },\n          },\n        ],\n        9: [\n          {\n            experimental: {\n              virtualMachine: {\n                proxy: {\n                  noproxy: ['    ', '   mangoes', 'yucca   ', '   ', ' guava '],\n                },\n              },\n            },\n          },\n          {\n            experimental: {\n              virtualMachine: {\n                proxy: {\n                  noproxy: ['mangoes', 'yucca', 'guava'],\n                },\n              },\n            },\n          },\n        ],\n        11: [\n          {\n            experimental: {\n              virtualMachine: {\n                socketVMNet: true,\n              },\n            },\n          },\n          {\n            experimental: {},\n          },\n        ],\n      };\n\n      it.each(Object.entries(expectedMigrations))('migrate from %i', (version, beforeAndAfter) => {\n        const [fromSettings, toSettings] = beforeAndAfter;\n        const existingVersion = parseInt(version, 10);\n        const targetVersion = existingVersion + 1;\n\n        fromSettings.version = existingVersion;\n        toSettings.version = targetVersion;\n        expect(settingsImpl.migrateSpecifiedSettingsToCurrentVersion(fromSettings, false, targetVersion)).toEqual(toSettings);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/config/__tests__/settingsMigrations.spec.ts",
    "content": "import _ from 'lodash';\n\nimport { updateTable } from '../settingsImpl';\n\nimport { defaultSettings, MountType, VMType } from '@pkg/config/settings';\n\ndescribe('settings migrations', () => {\n  describe('step 9', () => {\n    const settings = _.cloneDeep(defaultSettings);\n\n    it('should ignore noproxy list if empty', () => {\n      const testSettings = _.cloneDeep(settings);\n\n      testSettings.experimental.virtualMachine.proxy.noproxy = [];\n      updateTable[9](testSettings, false);\n\n      expect(testSettings.experimental.virtualMachine.proxy.noproxy).toStrictEqual([]);\n    });\n\n    it('should remove unnecessary blanks', () => {\n      const testSettings = _.cloneDeep(settings);\n\n      testSettings.experimental.virtualMachine.proxy.noproxy = [\n        '0.0.0.0/8', ' 10.0.0.0/8', '127.0.0.0/8  ', '  169.254.0.0/16', '172.16.0.0/12',\n        '192.168.0.0/16 '];\n      updateTable[9](testSettings, false);\n\n      expect(testSettings.experimental.virtualMachine.proxy.noproxy).toStrictEqual([\n        '0.0.0.0/8', '10.0.0.0/8', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12',\n        '192.168.0.0/16']);\n    });\n\n    it('should remove tabs', () => {\n      const testSettings = _.cloneDeep(settings);\n\n      testSettings.experimental.virtualMachine.proxy.noproxy = [\n        '0.0.0.0/8\\t', '\\t10.0.0.0/8', '\\t 127.0.0.0/8', '169.254.0.0/16 \\t'];\n      updateTable[9](testSettings, false);\n\n      expect(testSettings.experimental.virtualMachine.proxy.noproxy).toStrictEqual([\n        '0.0.0.0/8', '10.0.0.0/8', '127.0.0.0/8', '169.254.0.0/16']);\n    });\n\n    it('should remove newlines', () => {\n      const testSettings = _.cloneDeep(settings);\n\n      testSettings.experimental.virtualMachine.proxy.noproxy = [\n        '0.0.0.0/8\\n', '\\n10.0.0.0/8', '\\n 127.0.0.0/8', '169.254.0.0/16 \\n'];\n      updateTable[9](testSettings, false);\n\n      expect(testSettings.experimental.virtualMachine.proxy.noproxy).toStrictEqual([\n        '0.0.0.0/8', '10.0.0.0/8', '127.0.0.0/8', '169.254.0.0/16']);\n    });\n\n    it('should remove empty entries', () => {\n      const testSettings = _.cloneDeep(settings);\n\n      testSettings.experimental.virtualMachine.proxy.noproxy = [\n        '0.0.0.0/8', '', '\\n', '10.0.0.0/8', ' ', '127.0.0.0/8', '    ', '\\t'];\n      updateTable[9](testSettings, false);\n\n      expect(testSettings.experimental.virtualMachine.proxy.noproxy).toStrictEqual([\n        '0.0.0.0/8', '10.0.0.0/8', '127.0.0.0/8']);\n    });\n  });\n\n  describe('step 10', () => {\n    it('should not disable wasm in normal settings', () => {\n      const testSettings = {};\n\n      updateTable[10](testSettings, false);\n      expect(testSettings).not.toHaveProperty('experimental.containerEngine.webAssembly.enabled', false);\n    });\n\n    it('should disable wasm in locked profiles', () => {\n      const testSettings = {};\n\n      updateTable[10](testSettings, true);\n      expect(testSettings).toHaveProperty('experimental.containerEngine.webAssembly.enabled', false);\n    });\n  });\n\n  describe('step 14', () => {\n    it('should migrate experimental.virtualMachine.type (and useRosetta) to virtualMachine.*', () => {\n      const testSettings = { experimental: { virtualMachine: { type: VMType.VZ, useRosetta: true } } };\n\n      updateTable[14](testSettings, false);\n      expect(testSettings).not.toHaveProperty('experimental.virtualMachine');\n      expect(testSettings).toHaveProperty('virtualMachine.type', VMType.VZ);\n      expect(testSettings).toHaveProperty('virtualMachine.useRosetta', true);\n    });\n  });\n\n  describe('step 15', () => {\n    it('should migrate experimental.virtualMachine.mount.type to virtualMachine.*', () => {\n      const testSettings = { experimental: { virtualMachine: { mount: { type: MountType.REVERSE_SSHFS } } } };\n\n      updateTable[15](testSettings, false);\n      expect(testSettings).not.toHaveProperty('experimental.virtualMachine.mount.type');\n      expect(testSettings).toHaveProperty('virtualMachine.mount.type', MountType.REVERSE_SSHFS);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/config/commandLineOptions.ts",
    "content": "import { join } from 'path';\n\nimport _ from 'lodash';\n\nimport { LockedSettingsType, Settings } from '@pkg/config/settings';\nimport { save, turnFirstRunOff } from '@pkg/config/settingsImpl';\nimport { TransientSettings } from '@pkg/config/transientSettings';\nimport SettingsValidator from '@pkg/main/commandServer/settingsValidator';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { RecursiveKeys, RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst console = Logging.settings;\n\nexport class LockedFieldError extends Error {}\n\nexport class FatalCommandLineOptionError extends Error {}\n\n/**\n * Takes an array of strings, presumably from a command-line used to launch the app.\n * Key operations:\n * * All options start with '--'.\n * * Ignore leading unrecognized options.\n * * Complain about any unrecognized options after a recognized option has been processed.\n * * This calls the same settings-validator as used by `rdctl set` and the API to catch\n *   any attempts to update a locked field.\n *\n *  * All errors are fatal as this function is like an API for launching the application.\n * @param cfg - current loaded settings - this is updated and also returned\n * @param lockedFields - current locked fields\n * @param commandLineArgs - new command-line args to be merged into `cfg` (error if the field is locked)\n * @return updated cfg\n */\nexport function updateFromCommandLine(cfg: Settings, lockedFields: LockedSettingsType, commandLineArgs: string[]): Settings {\n  const lim = commandLineArgs.length;\n\n  if (lim === 0) {\n    return cfg;\n  }\n  let processingExternalArguments = true;\n  let newSettings: RecursivePartial<Settings> = {};\n\n  // As long as processingExternalArguments is true, ignore anything we don't recognize.\n  // Once we see something that's \"ours\", set processingExternalArguments to false.\n  // Note that `i` is also incremented in the body of the loop to skip over parameter values.\n  for (let i = 0; i < lim; i++) {\n    const arg = commandLineArgs[i];\n\n    if (!arg.startsWith('--')) {\n      if (processingExternalArguments) {\n        continue;\n      }\n      throw new Error(`Unexpected argument '${ arg }' in command-line [${ commandLineArgs.join(' ') }]`);\n    }\n    const equalPosition = arg.indexOf('=');\n    const [fqFieldName, value] = equalPosition === -1 ? [arg.substring(2), ''] : [arg.substring(2, equalPosition), arg.substring(equalPosition + 1)];\n\n    if (fqFieldName === 'no-modal-dialogs') {\n      switch (value) {\n      case '':\n      case 'true':\n        TransientSettings.update({ noModalDialogs: true });\n        break;\n      case 'false':\n        TransientSettings.update({ noModalDialogs: false });\n        break;\n      default:\n        throw new Error(`Invalid associated value for ${ arg }: must be unspecified (set to true), true or false`);\n      }\n      processingExternalArguments = false;\n      continue;\n    }\n    const currentValue: boolean | string | number | Record<string, undefined> | undefined = _.get(cfg, fqFieldName);\n\n    if (currentValue === undefined) {\n      // Ignore unrecognized command-line options until we get to one we recognize\n      if (processingExternalArguments) {\n        console.warn(`Unrecognized command-line argument ${ arg }`);\n        continue;\n      }\n      throw new Error(`Can't evaluate command-line argument ${ arg } -- no such entry in current settings at ${ join(paths.config, 'settings.json') }`);\n    }\n\n    processingExternalArguments = false;\n    const currentValueType = typeof currentValue;\n    let finalValue: any = value;\n\n    // First ensure we aren't trying to overwrite a non-leaf, and then determine the value to assign.\n    switch (currentValueType) {\n    case 'object':\n      throw new Error(`Can't overwrite existing setting ${ arg } in current settings at ${ join(paths.config, 'settings.json') }`);\n    case 'boolean':\n      // --some-boolean-setting ==> --some-boolean-setting=true\n      if (equalPosition === -1) {\n        finalValue = 'true'; // JSON.parse to boolean `true` a few lines later.\n      }\n      break;\n    default:\n      if (equalPosition === -1) {\n        if (i === lim - 1) {\n          throw new Error(`No value provided for option ${ arg } in command-line [${ commandLineArgs.join(' ') }]`);\n        }\n        i += 1;\n        finalValue = commandLineArgs[i];\n      }\n    }\n    // Now verify we're not changing the type of the current value\n    if (['boolean', 'number'].includes(currentValueType)) {\n      try {\n        finalValue = JSON.parse(finalValue);\n      } catch (cause) {\n        throw new Error(`Can't evaluate --${ fqFieldName }=${ finalValue } as ${ currentValueType }: ${ cause }`, { cause });\n      }\n      // We know the current value's type is either boolean or number, so a constrained comparison is ok\n      // eslint-disable-next-line valid-typeof\n      if (typeof finalValue !== currentValueType) {\n        throw new TypeError(`Type of '${ finalValue }' is ${ typeof finalValue }, but current type of ${ fqFieldName } is ${ currentValueType } `);\n      }\n    }\n    newSettings = _.merge(newSettings, getObjectRepresentation(fqFieldName as RecursiveKeys<Settings>, finalValue));\n  }\n  const settingsValidator = new SettingsValidator();\n  const newKubernetesVersion = newSettings.kubernetes?.version;\n\n  if (newKubernetesVersion) {\n    // RD hasn't loaded the supported k8s versions yet, so fake the list.\n    // If the field is locked, we don't need to know what it's locked to,\n    // just that the proposed version is different from the current version.\n    // The current version doesn't have to be the locked version, but will be after processing ends.\n    const limitedK8sVersionList: string[] = [newKubernetesVersion];\n\n    if (cfg.kubernetes.version) {\n      limitedK8sVersionList.push(cfg.kubernetes.version);\n    }\n    settingsValidator.k8sVersions = limitedK8sVersionList;\n  }\n  const [needToUpdate, errors, isFatal] = settingsValidator.validateSettings(cfg, newSettings, lockedFields);\n\n  if (errors.length > 0) {\n    const errorString = `Error in command-line options:\\n${ errors.join('\\n') }`;\n\n    if (errors.some(error => /field \".+?\" is locked/.test(error))) {\n      throw new LockedFieldError(errorString);\n    }\n    if (isFatal) {\n      throw new FatalCommandLineOptionError(errorString);\n    }\n    throw new Error(errorString);\n  }\n  if (needToUpdate) {\n    cfg = _.merge(cfg, newSettings);\n    save(cfg);\n  } else {\n    console.debug(`No need to update preferences based on command-line options ${ commandLineArgs.join(', ') }`);\n  }\n  turnFirstRunOff();\n\n  return cfg;\n}\n\n// This is similar to `lodash.set({}, fqFieldAccessor, finalValue)\n// but it also does some error checking.\n// On the happy path, it's exactly like `lodash.set`\n// exported for unit tests only\nexport function getObjectRepresentation(fqFieldAccessor: RecursiveKeys<Settings>, finalValue: boolean | number | string): RecursivePartial<Settings> {\n  if (!fqFieldAccessor) {\n    throw new Error(\"Invalid command-line option: can't be the empty string.\");\n  }\n  const optionParts: string[] = fqFieldAccessor.split('.');\n\n  if (optionParts.length === 1) {\n    return { [fqFieldAccessor]: finalValue };\n  }\n  const lastField: string | undefined = optionParts.pop();\n\n  if (!lastField) {\n    throw new Error(\"Unrecognized command-line option ends with a dot ('.')\");\n  }\n\n  return _.set({}, fqFieldAccessor, finalValue) as RecursivePartial<Settings>;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/config/cookies.js",
    "content": "export const CSRF = 'CSRF';\nexport const USERNAME = 'R_USERNAME';\nexport const LOCALE = 'R_LOCALE';\n"
  },
  {
    "path": "pkg/rancher-desktop/config/emptyStubForJSLinter.js",
    "content": "// See https://stackoverflow.com/questions/39418555/syntaxerror-with-jest-and-react-and-importing-css-files\n// to continue to use jest-based unit-testing with CSS import statements.\n\n/** @type any */\nexport default {};\n"
  },
  {
    "path": "pkg/rancher-desktop/config/help.ts",
    "content": "import { shell } from 'electron';\n\nimport { TransientSettings } from '@pkg/config/transientSettings';\nimport { parseDocsVersion } from '@pkg/utils/version';\n\ntype Paths = Record<string, string>;\n\nclass Url {\n  private readonly baseUrl = 'https://docs.rancherdesktop.io';\n  private paths: Paths = {};\n\n  constructor(paths: Paths) {\n    this.paths = paths;\n  }\n\n  buildUrl(key: string | undefined, version: string): string {\n    if (key) {\n      const docsVersion = parseDocsVersion(version);\n\n      return `${ this.baseUrl }/${ docsVersion }/${ this.paths[key] }`;\n    }\n\n    return '';\n  }\n}\n\nclass PreferencesHelp {\n  private readonly url = new Url({\n    'Application-behavior':            'ui/preferences/application/behavior',\n    'Application-environment':         'ui/preferences/application/environment',\n    'Application-general':             'ui/preferences/application/general',\n    'Virtual Machine-hardware':        'ui/preferences/virtual-machine/hardware',\n    'Virtual Machine-volumes':         'ui/preferences/virtual-machine/volumes',\n    'Virtual Machine-network':         'ui/preferences/virtual-machine/network',\n    'Virtual Machine-emulation':       'ui/preferences/virtual-machine/emulation',\n    'Container Engine-general':        'ui/preferences/container-engine/general',\n    'Container Engine-allowed-images': 'ui/preferences/container-engine/allowed-images',\n    'WSL-integrations':                'ui/preferences/wsl/integrations',\n    'WSL-network':                     'ui/preferences/wsl/network',\n    'WSL-proxy':                       'ui/preferences/wsl/proxy',\n    Kubernetes:                        'ui/preferences/kubernetes',\n  });\n\n  openUrl(version: string): void {\n    const { current, currentTabs } = TransientSettings.value.preferences.navItem;\n    const tab = currentTabs[current] ? `-${ currentTabs[current] }` : '';\n\n    const url = this.url.buildUrl(`${ current }${ tab }`, version);\n\n    shell.openExternal(url);\n  }\n}\n\nexport const Help = { preferences: new PreferencesHelp() };\n"
  },
  {
    "path": "pkg/rancher-desktop/config/private-label.js",
    "content": "import { SETTING } from './settings';\n\nexport const ANY = 0;\nexport const STANDARD = 1;\nexport const CUSTOM = 2;\n\nconst STANDARD_VENDOR = 'Rancher';\nconst STANDARD_PRODUCT = 'Explorer';\n\nlet mode = STANDARD;\nlet vendor = STANDARD_VENDOR;\nlet product = STANDARD_PRODUCT;\n\nexport function setMode(m) {\n  mode = m;\n}\n\nexport function setVendor(v) {\n  vendor = v;\n}\n\nexport function setProduct(p) {\n  product = p;\n}\n\n// -------------------------------------\n\nexport function getMode() {\n  return mode;\n}\n\nexport function isStandard() {\n  return mode === STANDARD;\n}\n\nexport function matches(pl) {\n  if ( pl === ANY ) {\n    return true;\n  }\n\n  return pl === mode;\n}\n\nexport function getVendor() {\n  if ( vendor === SETTING.PL_RANCHER_VALUE ) {\n    return STANDARD_VENDOR;\n  }\n\n  return vendor;\n}\n\nexport function getProduct() {\n  return product;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/config/query-params.js",
    "content": "// Debugging\nexport const SPA = 'spa';\n\n// Login/Initial setup\nexport const LOCAL = 'local';\nexport const SETUP = 'setup';\nexport const STEP = 'step';\nexport const LOGGED_OUT = 'logged-out';\nexport const IS_SSO = 'is-sso';\nexport const IS_SLO = 'is-slo';\nexport const UPGRADED = 'upgraded';\nexport const TIMED_OUT = 'timed-out';\nexport const AUTH_TEST = 'test';\nexport const BACK_TO = 'back-to';\nexport const GITHUB_CODE = 'code';\nexport const GITHUB_NONCE = 'state';\nexport const GITHUB_SCOPE = 'scope';\nexport const GITHUB_REDIRECT = 'redirect_uri';\n\n// General\nexport const _FLAGGED = null; // The value for a key-only flag, like `?desc`\nexport const _UNFLAG = undefined; // The value to remove a query param\n\n// SortableTable\nexport const SEARCH_QUERY = 'q';\nexport const SORT_BY = 'sort';\nexport const DESCENDING = 'desc';\nexport const PAGE = 'page';\n\n// ResourceDetail/Yaml\nexport const MODE = 'mode';\nexport const _CREATE = 'create';\nexport const _VIEW = 'view';\nexport const _EDIT = 'edit';\nexport const _LIST = 'list';\nexport const _CLONE = 'clone';\nexport const _STAGE = 'stage';\nexport const _IMPORT = 'import';\n\nexport const AS = 'as';\nexport const _DETAIL = 'detail';\nexport const _CONFIG = 'config';\nexport const _YAML = 'yaml';\nexport const _GRAPH = 'graph';\nexport const FOCUS = 'focus';\n\nexport const PREVIEW = 'preview';\n\nexport const DIFF = 'diff';\nexport const _UNIFIED = 'unified';\nexport const _SPLIT = 'split';\n\n// CruResource\nexport const SUB_TYPE = 'type';\nexport const RKE_TYPE = 'rkeType';\n\n// App launch\nexport const REPO_TYPE = 'repo-type';\nexport const REPO = 'repo';\nexport const CHART = 'chart';\nexport const VERSION = 'version';\nexport const NAME = 'name';\nexport const NAMESPACE = 'namespace';\nexport const DESCRIPTION = 'description';\nexport const CATEGORY = 'category';\nexport const OPERATING_SYSTEM = 'os';\nexport const DEPRECATED = 'deprecated';\nexport const HIDDEN = 'hidden';\nexport const FROM_TOOLS = 'tools';\nexport const FROM_CLUSTER = 'cluster';\nexport const HIDE_SIDE_NAV = 'hide-side-nav';\n\n// Cluster provisioning\nexport const PROVIDER = 'provider';\nexport const CLOUD_CREDENTIAL = 'cloud';\n\n// NAMESPACE/PROJECT\nexport const PROJECT_ID = 'projectId';\nexport const FLAT_VIEW = 'flatView';\n"
  },
  {
    "path": "pkg/rancher-desktop/config/settings.ts",
    "content": "// This file contains exportable types and constants used for managing preferences\n// All the actual data and functions are in settingsImpl.ts\n\nimport os from 'os';\n\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nexport const CURRENT_SETTINGS_VERSION = 18 as const;\n\nexport enum VMType {\n  QEMU = 'qemu',\n  VZ = 'vz',\n}\nexport enum ContainerEngine {\n  NONE = '',\n  CONTAINERD = 'containerd',\n  MOBY = 'moby',\n}\n\nexport const ContainerEngineNames: Record<ContainerEngine, string> = {\n  [ContainerEngine.NONE]:       '',\n  [ContainerEngine.CONTAINERD]: 'containerd',\n  [ContainerEngine.MOBY]:       'dockerd',\n};\n\nexport enum MountType {\n  NINEP = '9p',\n  REVERSE_SSHFS = 'reverse-sshfs',\n  VIRTIOFS = 'virtiofs',\n}\n\nexport enum ProtocolVersion {\n  NINEP2000 = '9p2000',\n  NINEP2000_U = '9p2000.u',\n  NINEP2000_L = '9p2000.L',\n}\n\nexport enum SecurityModel {\n  PASSTHROUGH = 'passthrough',\n  MAPPED_XATTR = 'mapped-xattr',\n  MAPPED_FILE = 'mapped-file',\n  NONE = 'none',\n}\n\nexport enum CacheMode {\n  NONE = 'none',\n  LOOSE = 'loose',\n  FSCACHE = 'fscache',\n  MMAP = 'mmap',\n}\n\nexport enum Theme {\n  SYSTEM = 'system',\n  LIGHT = 'light',\n  DARK = 'dark',\n}\n\nexport class SettingsError extends Error {\n  toString() {\n    // This is needed on linux. Without it, we get a randomish replacement\n    // for 'SettingsError' (like 'ys Error')\n    return `SettingsError: ${ this.message }`;\n  }\n}\n\nexport const defaultSettings = {\n  version:     CURRENT_SETTINGS_VERSION,\n  application: {\n    adminAccess: false,\n    debug:       false,\n    extensions:  {\n      allowed: {\n        enabled: false,\n        list:    [] as string[],\n      },\n      /** Installed extensions, mapping to the installed version (tag). */\n      installed: { } as Record<string, string>,\n    },\n    pathManagementStrategy: process.platform === 'win32' ? PathManagementStrategy.Manual : PathManagementStrategy.RcFiles,\n    telemetry:              { enabled: true },\n    /** Whether we should check for updates and apply them. */\n    updater:                { enabled: true },\n    autoStart:              false,\n    startInBackground:      false,\n    hideNotificationIcon:   false,\n    window:                 { quitOnClose: false },\n    theme:                  Theme.SYSTEM,\n  },\n  containerEngine: {\n    allowedImages: {\n      enabled:  false,\n      patterns: [] as string[],\n    },\n    mobyStorageDriver: 'auto' as 'classic' | 'snapshotter' | 'auto',\n    name:              ContainerEngine.MOBY,\n  },\n  virtualMachine: {\n    memoryInGB: 2,\n    numberCPUs: 2,\n    /** can only be set to VMType.VZ on macOS Ventura and later */\n    type:       process.platform === 'darwin' && parseInt(os.release(), 10) >= 23 ? VMType.VZ : VMType.QEMU,\n    /** can only be used when type is VMType.VZ, and only on aarch64 */\n    useRosetta: false,\n    mount:      {\n      // Mount type defaults to virtiofs when using VZ.\n      type: process.platform === 'darwin' && parseInt(os.release(), 10) >= 23 ? MountType.VIRTIOFS : MountType.REVERSE_SSHFS,\n    },\n  },\n  WSL:        { integrations: {} as Record<string, boolean> },\n  kubernetes: {\n    /** The version of Kubernetes to launch, as a semver (without v prefix). */\n    version: '',\n    port:    6443,\n    enabled: true,\n    options: { traefik: true, flannel: true },\n    ingress: { localhostOnly: false },\n  },\n  portForwarding: { includeKubernetesServices: false },\n  images:         {\n    showAll:   true,\n    namespace: 'default',\n  },\n  containers: {\n    showAll:   true,\n    namespace: 'default',\n  },\n  diagnostics: {\n    showMuted:    false,\n    mutedChecks:  {} as Record<string, boolean>,\n    connectivity: {\n      interval: 5_000,\n      timeout:  5_000,\n    },\n  },\n  /**\n   * Experimental settings\n   */\n  experimental: {\n    containerEngine: { webAssembly: { enabled: false } },\n    /** can only be enabled if containerEngine.webAssembly.enabled is true */\n    kubernetes:      { options: { spinkube: false } },\n    virtualMachine:  {\n      diskSize: '100GiB',\n      mount:    {\n        '9p': {\n          securityModel:   SecurityModel.NONE,\n          protocolVersion: ProtocolVersion.NINEP2000_L,\n          msizeInKib:      128,\n          cacheMode:       CacheMode.MMAP,\n        },\n      },\n      proxy: {\n        enabled:  false,\n        address:  '',\n        password: '',\n        port:     3128,\n        username: '',\n        noproxy:  ['0.0.0.0/8', '10.0.0.0/8', '127.0.0.0/8', '169.254.0.0/16', '172.16.0.0/12', '192.168.0.0/16',\n          '224.0.0.0/4', '240.0.0.0/4'],\n      },\n      /** Lima only: use SSH port forwarding instead of gRPC. */\n      sshPortForwarder: true,\n    },\n  },\n};\n\nexport type Settings = typeof defaultSettings;\n\n// A settings-like type with a subset of all the fields of defaultSettings,\n// but all leaves are set to `true`.\nexport type LockedSettingsType = Record<string, any>;\n\nexport interface DeploymentProfileType {\n  defaults: RecursivePartial<Settings>;\n  locked:   RecursivePartial<Settings>;\n}\n\n// Imported from dashboard/config/settings.js\n// Setting IDs\nexport const SETTING = { PL_RANCHER_VALUE: 'rancher' };\n"
  },
  {
    "path": "pkg/rancher-desktop/config/settingsImpl.ts",
    "content": "// This file contains the code to work with the settings.json file along with\n// code docs on it.\n\nimport fs from 'fs';\nimport os from 'os';\nimport { dirname, join } from 'path';\n\nimport _ from 'lodash';\n\nimport {\n  CURRENT_SETTINGS_VERSION,\n  defaultSettings,\n  DeploymentProfileType,\n  LockedSettingsType,\n  Settings,\n  SettingsError,\n} from '@pkg/config/settings';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport clone from '@pkg/utils/clone';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';\nimport { getProductionVersion } from '@pkg/utils/version';\n\nconst console = Logging.settings;\n\n// A settings-like type with a subset of all the fields of defaultSettings,\n// but all leaves are set to `true`.\nlet lockedSettings: LockedSettingsType = {};\n\nlet _isFirstRun = false;\nlet settings: Settings | undefined;\n\n/**\n * Load the settings file from disk, doing any migrations as necessary.\n */\nfunction loadFromDisk(): Settings {\n  // Throw an ENOENT error if the file doesn't exist; the caller should know what to do.\n  const settingsPath = join(paths.config, 'settings.json');\n  const rawdata = fs.readFileSync(settingsPath);\n  let originalConfig: Record<string, any>;\n\n  try {\n    originalConfig = JSON.parse(rawdata.toString());\n  } catch (err: any) {\n    console.error(`Error JSON-parsing existing settings contents ${ rawdata }`, err);\n    console.error('The old settings file will be replaced with the default settings.');\n\n    return defaultSettings;\n  }\n\n  if (!('version' in originalConfig)) {\n    throw new SettingsError(`No version specified in ${ settingsPath }`);\n  }\n  const updatedConfig = migrateSettingsToCurrentVersion(originalConfig);\n\n  // If the existing settings file is partial, fill in the missing fields with defaults.\n  return _.defaultsDeep(updatedConfig, defaultSettings);\n}\n\nexport function save(cfg: Settings) {\n  try {\n    fs.mkdirSync(paths.config, { recursive: true });\n    const rawdata = JSON.stringify(cfg);\n\n    fs.writeFileSync(join(paths.config, 'settings.json'), rawdata);\n\n    // update the in-memory copy so subsequent calls to getSettings() will\n    // return an up to date settings object\n    settings = cfg;\n  } catch (err) {\n    if (err) {\n      const { dialog } = require('electron');\n\n      dialog.showErrorBox('Unable To Save Settings File', parseSaveError(err));\n    } else {\n      console.log('Settings file saved\\n');\n    }\n  }\n}\n\nexport function getSettings(): Settings {\n  return settings ?? defaultSettings;\n}\n\n/**\n * createSettings\n * - Called when either there's no settings file, or for testing purposes, where we want to use a particular deployment profile.\n * @param {DeploymentProfileType} deploymentProfiles\n * @returns default settings merged with any default profile\n */\nexport function createSettings(deploymentProfiles: DeploymentProfileType): Settings {\n  const cfg = clone(defaultSettings);\n\n  cfg.virtualMachine.memoryInGB = getDefaultMemory();\n  merge(cfg, deploymentProfiles.defaults);\n\n  // If there's no deployment profile, put up the first-run dialog box.\n  if (!Object.keys(deploymentProfiles.defaults).length && !Object.keys(deploymentProfiles.locked).length) {\n    _isFirstRun = true;\n  }\n\n  return finishConfiguringSettings(cfg, deploymentProfiles);\n}\n\n/**\n * Used for unit testing only.\n * Could be used in core code if we ever want to reload changed deployment profiles, but that isn't needed now.\n */\nexport function clearSettings() {\n  settings = undefined;\n}\n\n/**\n * Load the settings file or create it if not present.\n */\nexport function load(deploymentProfiles: DeploymentProfileType): Settings {\n  try {\n    return finishConfiguringSettings(loadFromDisk(), deploymentProfiles);\n  } catch (err: any) {\n    if (err.code === 'ENOENT') {\n      // See the migrateSettingsLocationOnWindows code to understand how it's impossible to\n      // end up in an infinite loop of recursive calls to `load()`\n      if (migrateSettingsLocationOnWindows()) {\n        return load(deploymentProfiles);\n      }\n\n      return createSettings(deploymentProfiles);\n    } else {\n      // JSON problems in the settings file will be caught, and we let any\n      // other errors (e.g. permission-related) bubble up to the surface\n      // and most likely result in a dialog box and the app shutting down.\n      throw err;\n    }\n  }\n}\n\nfunction finishConfiguringSettings(cfg: Settings, deploymentProfiles: DeploymentProfileType): Settings {\n  if (process.env.RD_FORCE_UPDATES_ENABLED) {\n    console.debug('updates enabled via RD_FORCE_UPDATES_ENABLED');\n    cfg.application.updater.enabled = true;\n  } else if (os.platform() === 'linux' && !process.env.APPIMAGE) {\n    cfg.application.updater.enabled = false;\n  } else {\n    const appVersion = getProductionVersion();\n\n    console.log(`appVersion is ${ appVersion }`);\n    // Auto-update doesn't work for CI or local builds, so don't enable it by default.\n    // CI builds use a version string like `git describe`, e.g. \"v1.1.0-4140-g717225dc\".\n    // Versions like \"1.9.0-tech-preview\" are pre-releases and not CI builds, so should not disable auto-update.\n    if ((/^v?\\d+\\.\\d+\\.\\d+-\\d+-g[0-9a-f]+$/.exec(appVersion)) || appVersion.includes('?')) {\n      cfg.application.updater.enabled = false;\n      console.log('updates disabled');\n    }\n  }\n  // Replace existing settings fields with whatever is set in the locked deployment-profile\n  merge(cfg, deploymentProfiles.locked);\n  save(cfg);\n  // Update the global settings variable for later retrieval\n  settings = cfg;\n\n  return cfg;\n}\n\nexport function getDefaultMemory() {\n  if (os.platform() === 'darwin' || os.platform() === 'linux') {\n    const totalMemoryInGB = os.totalmem() / 2 ** 30;\n\n    // 25% of available ram up to a maximum of 6gb\n    return Math.min(6, Math.round(totalMemoryInGB / 4.0));\n  } else {\n    return 2;\n  }\n}\n/**\n * Merge settings in-place with changes, returning the merged settings.\n * @param cfg Baseline settings.  This will be modified.\n * @param changes The set of changes to pull in.\n * @returns The merged settings (also modified in-place).\n */\nexport function merge<T = Settings>(cfg: T, changes: RecursivePartial<RecursiveReadonly<T>>): T {\n  const customizer = (objValue: any, srcValue: any) => {\n    if (Array.isArray(objValue)) {\n      // If the destination is an array of primitives, just return the source\n      // (i.e. completely overwrite).\n      if (objValue.every(i => typeof i !== 'object')) {\n        return srcValue;\n      }\n    }\n    if (typeof srcValue === 'object' && srcValue) {\n      // For objects, setting a value to `undefined` or `null` will remove it.\n      for (const [key, value] of Object.entries(srcValue)) {\n        if (typeof value === 'undefined' || value === null) {\n          delete srcValue[key];\n          if (typeof objValue === 'object' && objValue) {\n            delete objValue[key];\n          }\n        }\n      }\n      // Don't return anything, let _.mergeWith() do the actual merging.\n    }\n  };\n\n  return _.mergeWith(cfg, changes, customizer);\n}\n\nexport function getLockedSettings(): LockedSettingsType {\n  return lockedSettings;\n}\n\nexport function updateLockedFields(lockedDeploymentProfile: RecursivePartial<Settings>) {\n  lockedSettings = determineLockedFields(lockedDeploymentProfile);\n}\n\n/**\n * Returns an object that mirrors `lockedProfileSettings` but all leaves are `true`.\n * @param lockedProfileSettings\n */\nexport function determineLockedFields(lockedProfileSettings: LockedSettingsType): LockedSettingsType {\n  function isLockedSettingsType(input: any): input is LockedSettingsType {\n    return typeof input === 'object' && !Array.isArray(input) && input !== null;\n  }\n\n  return Object.fromEntries(Object.entries(lockedProfileSettings).map(([k, v]) => {\n    return [k, isLockedSettingsType(v) ? determineLockedFields(v) : true];\n  }));\n}\n\nexport function firstRunDialogNeeded() {\n  return _isFirstRun;\n}\n\nexport function turnFirstRunOff() {\n  _isFirstRun = false;\n}\n\nfunction safeFileTest(path: string, conditions: number) {\n  try {\n    fs.accessSync(path, conditions);\n\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\nexport function runInDebugMode(debug: boolean): boolean {\n  return debug || !!process.env.RD_DEBUG_ENABLED;\n}\n\nfunction fileExists(path: string) {\n  try {\n    fs.statSync(path);\n\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\nfunction fileIsWritable(path: string) {\n  try {\n    fs.accessSync(path, fs.constants.W_OK);\n\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\n/*\n * The purpose of this function is to let RD stop using AppData\\Roaming on Windows, and store almost everything\n * in AppData\\Local. The only file it needs to preserve is `AppData\\Roaming\\rancher-desktop\\settings.json`.\n * This is called by the loader when it doesn't find that file in `Local\\...`. So it looks to see if it's\n * in `Roaming\\...`, and if it is, will move it to `Local\\...` and then load it.\n */\nfunction migrateSettingsLocationOnWindows(): boolean {\n  if (process.platform !== 'win32') {\n    return false;\n  }\n  const appData = process.env['APPDATA'];\n  const rdAppHomeDir = paths.appHome;\n\n  if (!appData || !rdAppHomeDir) {\n    return false;\n  }\n  const oldConfigPath = join(appData, 'rancher-desktop', 'settings.json');\n  const newConfigPath = join(rdAppHomeDir, 'settings.json');\n\n  if (!fileExists(oldConfigPath) || fileExists(newConfigPath)) {\n    return false;\n  }\n  try {\n    fs.copyFileSync(oldConfigPath, newConfigPath);\n\n    // If the copy actually failed to create `newConfigPath`, return false, so the caller will create new settings.\n    return fileExists(newConfigPath);\n  } catch {\n    // Ignore any other problems, so create a new settings file.\n  }\n\n  return false;\n}\n\n/**\n * Simple function to wrap paths with spaces with double-quotes. Intended for human consumption.\n * Trying to avoid adding yet another external dependency.\n */\nfunction quoteIfNeeded(fullpath: string): string {\n  return /\\s/.test(fullpath) ? `\"${ fullpath }\"` : fullpath;\n}\n\nfunction parseSaveError(err: any) {\n  const msg = err.toString();\n\n  console.log(`settings save error: ${ msg }`);\n  const p = new RegExp(`^Error:\\\\s*${ err.code }:\\\\s*(.*?),\\\\s*${ err.syscall }\\\\s+'?${ err.path }`);\n  const m = p.exec(msg);\n  let friendlierMsg = `Error trying to ${ err.syscall } ${ err.path }`;\n\n  if (m) {\n    friendlierMsg += `: ${ m[1] }`;\n  }\n  const parentPath = dirname(err.path);\n\n  if (err.code === 'EACCES') {\n    if (!fileExists(err.path)) {\n      if (!fileExists(parentPath)) {\n        friendlierMsg += `\\n\\nCouldn't create preferences directory ${ parentPath }`;\n      } else if (!safeFileTest(parentPath, fs.constants.W_OK | fs.constants.X_OK)) {\n        friendlierMsg += `\\n\\nPossible fix: chmod +wx ${ quoteIfNeeded(parentPath) }`;\n      }\n    } else if (!fileIsWritable(err.path)) {\n      friendlierMsg += `\\n\\nPossible fix: chmod +w ${ quoteIfNeeded(err.path) }`;\n    }\n  }\n\n  return friendlierMsg;\n}\n\n/**\n * ReplacementDirective describes how a setting can be migrated.\n */\ninterface ReplacementDirective {\n  /**\n   * The path to the old value\n   */\n  oldPath: string;\n  /**\n   * The path to the new value\n   */\n  newPath: string;\n}\n\n/**\n * This function takes an array of `ReplacementDirectives`, and carries out each one which essentially\n * moves a value at an old location to a new one, and then deletes the old location.\n * @param settings - the settings object\n * @param replacements - a table used to update the settings object based on existing obsolete fields that need to be moved.\n */\nfunction processReplacements(settings: any, replacements: ReplacementDirective[]) {\n  for (const { oldPath, newPath } of replacements) {\n    if (_.hasIn(settings, oldPath)) {\n      // Transfer the current value for the old field to the new field\n      _.set(settings, newPath, _.get(settings, oldPath));\n      // Delete the old field\n      _.unset(settings, oldPath);\n    }\n  }\n}\n\n/**\n * Provide a mapping from settings version X to version X + 1\n *\n * Some migrations need to be done with bespoke code, but most of them\n * can be expressed in a descriptive table, and the operations are done\n * by `processReplacements`, which just moves old values to new locations,\n * and deletes the old location.\n *\n * The `settings` @param does not have to be a complete settings object.\n * And its type is `any` because it needs to work on older versions of the settings data.\n */\nexport const updateTable: Record<number, (settings: any, locked : boolean) => void> = {\n  1: (settings) => {\n    _.unset(settings, 'kubernetes.rancherMode');\n  },\n  2: (_) => {\n    // No need to still check for and delete archaic installations from version 0.3.0\n    // The updater still wants to see an entry here (for updating ancient systems),\n    // but will no longer delete obsolete files.\n  },\n  3: (_) => {\n    // With settings v5, all traces of the kim builder are gone now, so no need to update it.\n  },\n  4: (settings) => {\n    if (_.hasIn(settings, 'kubernetes.suppressSudo')) {\n      _.set(settings, 'application.adminAccess', !settings.kubernetes.suppressSudo);\n      delete settings.kubernetes.suppressSudo;\n    }\n    const replacements: ReplacementDirective[] = [\n      { oldPath: 'debug', newPath: 'application.debug' },\n      { oldPath: 'pathManagementStrategy', newPath: 'application.pathManagementStrategy' },\n      { oldPath: 'telemetry', newPath: 'application.telemetry.enabled' },\n      { oldPath: 'updater', newPath: 'application.updater.enabled' },\n      { oldPath: 'kubernetes.containerEngine', newPath: 'containerEngine.name' },\n      { oldPath: 'kubernetes.experimental.socketVMNet', newPath: 'experimental.virtualMachine.socketVMNet' },\n      { oldPath: 'kubernetes.hostResolver', newPath: 'virtualMachine.hostResolver' },\n      { oldPath: 'kubernetes.memoryInGB', newPath: 'virtualMachine.memoryInGB' },\n      { oldPath: 'kubernetes.numberCPUs', newPath: 'virtualMachine.numberCPUs' },\n      { oldPath: 'kubernetes.WSLIntegrations', newPath: 'WSL.integrations' },\n    ];\n\n    processReplacements(settings, replacements);\n    _.unset(settings, 'kubernetes.checkForExistingKimBuilder');\n    _.unset(settings, 'kubernetes.experimental');\n  },\n  5: (settings) => {\n    const replacements: ReplacementDirective[] = [\n      { oldPath: 'autoStart', newPath: 'application.autoStart' },\n      { oldPath: 'hideNotificationIcon', newPath: 'application.hideNotificationIcon' },\n      { oldPath: 'startInBackground', newPath: 'application.startInBackground' },\n      { oldPath: 'window', newPath: 'application.window' },\n      { oldPath: 'containerEngine.imageAllowList', newPath: 'containerEngine.allowedImages' },\n      { oldPath: 'virtualMachine.experimental.socketVMNet', newPath: 'experimental.virtualMachine.socketVMNet' },\n    ];\n\n    processReplacements(settings, replacements);\n    if (_.isEmpty(_.get(settings, 'virtualMachine.experimental'))) {\n      _.unset(settings, 'virtualMachine.experimental');\n    }\n  },\n  6: (settings) => {\n    // Rancher Desktop 1.9+\n    // extensions went from Record<string, boolean> to Record<string, string>\n    // The key used to be the extension image (including tag); it's now keyed\n    // by the image (without tag) with the value being the tag.\n    if (_.hasIn(settings, 'extensions')) {\n      const withTags = Object.entries(settings.extensions ?? {}).filter(([, v]) => v).map(([k]) => k);\n      const extensions = withTags.map((image) => {\n        return image.split(':', 2).concat('latest').slice(0, 2) as [string, string];\n      });\n\n      settings.extensions = Object.fromEntries(extensions);\n    }\n  },\n  7: (settings) => {\n    if (_.get(settings, 'application.pathManagementStrategy') === 'notset') {\n      if (process.platform === 'win32') {\n        settings.application.pathManagementStrategy = PathManagementStrategy.Manual;\n      } else {\n        settings.application.pathManagementStrategy = PathManagementStrategy.RcFiles;\n      }\n    }\n  },\n  8: (settings) => {\n    // Rancher Desktop 1.10: move .extensions to .application.extensions.installed\n    const replacements: ReplacementDirective[] = [\n      { oldPath: 'extensions', newPath: 'application.extensions.installed' },\n    ];\n\n    processReplacements(settings, replacements);\n  },\n  9: (settings) => {\n    // Rancher Desktop 1.11\n    // Use string-list component instead of textarea for noproxy field. Blanks that\n    // were accepted by the textarea need to be filtered out.\n    if (!_.isEmpty(_.get(settings, 'experimental.virtualMachine.proxy.noproxy'))) {\n      settings.experimental.virtualMachine.proxy.noproxy =\n        settings.experimental.virtualMachine.proxy.noproxy.map((entry: string) => {\n          return entry.trim();\n        }).filter((entry: string) => {\n          return entry.length > 0;\n        });\n    }\n  },\n  10: (settings, locked) => {\n    // Migrating from an older locked profile automatically locks newer features (wasm support).\n    if (locked && !_.has(settings, 'experimental.containerEngine.webAssembly.enabled')) {\n      _.set(settings, 'experimental.containerEngine.webAssembly.enabled', false);\n    }\n  },\n  11: (settings) => {\n    _.unset(settings, 'experimental.virtualMachine.socketVMNet');\n    if (_.isEmpty(_.get(settings, 'experimental.virtualMachine'))) {\n      _.unset(settings, 'experimental.virtualMachine');\n    }\n  },\n  12: (settings) => {\n    // This bump is only there to force networking tunnel.\n    _.set(settings, 'experimental.virtualMachine.networkingTunnel', true);\n  },\n  13: (settings) => {\n    _.unset(settings, 'virtualMachine.hostResolver');\n    _.unset(settings, 'experimental.virtualMachine.networkingTunnel');\n  },\n  14: (settings) => {\n    const replacements: ReplacementDirective[] = [\n      { oldPath: 'experimental.virtualMachine.type', newPath: 'virtualMachine.type' },\n      { oldPath: 'experimental.virtualMachine.useRosetta', newPath: 'virtualMachine.useRosetta' },\n    ];\n\n    processReplacements(settings, replacements);\n    if (_.isEmpty(_.get(settings, 'experimental.virtualMachine'))) {\n      _.unset(settings, 'experimental.virtualMachine');\n    }\n  },\n  15: (settings) => {\n    const replacements: ReplacementDirective[] = [\n      { oldPath: 'experimental.virtualMachine.mount.type', newPath: 'virtualMachine.mount.type' },\n    ];\n\n    processReplacements(settings, replacements);\n  },\n  16: (settings, locked) => {\n    if (!locked && !_.has(settings, 'containerEngine.mobyStorageDriver')) {\n      _.set(settings, 'containerEngine.mobyStorageDriver', 'auto');\n    }\n  },\n};\n\nfunction migrateSettingsToCurrentVersion(settings: Record<string, any>): Settings {\n  if (Object.keys(settings).length === 0) {\n    return defaultSettings;\n  }\n  const newSettings = migrateSpecifiedSettingsToCurrentVersion(settings, false);\n\n  return _.defaultsDeep(newSettings, defaultSettings);\n}\n\n/**\n * Used to migrate a settings payload from an earlier version to the current one.\n * Input payloads are expected to come from either the argument to `rdctl api settings -X PUT ...`\n * or a deployment profile.\n *\n * The contents of settings files go through the unexported function `migrateSettingsToCurrentVersion`\n * which assigns any missing defaults at the end. This function does not fill in missing values.\n * @param settings - a possibly partial settings object.\n * @param locked - perform special migrations for locked profiles.\n * @param targetVersion - used for unit testing, to run a specific step from version n to n + 1, and not the full migration\n */\nexport function migrateSpecifiedSettingsToCurrentVersion(settings: Record<string, any>, locked: boolean, targetVersion:number = CURRENT_SETTINGS_VERSION): RecursivePartial<Settings> {\n  const firstPart = 'updating settings requires specifying an API version';\n  let loadedVersion = settings.version;\n\n  if (!('version' in settings)) {\n    throw new TypeError(`${ firstPart }, but no version was specified`);\n  } else if ((typeof (loadedVersion) !== 'number') || isNaN(loadedVersion)) {\n    throw new TypeError(`${ firstPart }, but \"${ loadedVersion }\" is not a proper config version`);\n  } else if (loadedVersion <= 0) {\n    // Avoid someone specifying a number like -1000000000000 and burning CPU cycles in the loop below\n    throw new TypeError(`${ firstPart }, but \"${ loadedVersion }\" is not a positive number`);\n  } else if (loadedVersion >= targetVersion) {\n    // This will elicit an error message from the validator\n    return settings;\n  }\n  for (; loadedVersion < targetVersion; loadedVersion++) {\n    if (updateTable[loadedVersion]) {\n      updateTable[loadedVersion](settings, locked);\n    }\n  }\n  settings.version = targetVersion;\n\n  return settings;\n}\n\n// Imported from dashboard/config/settings.js\n// Setting IDs\nexport const SETTING = { PL_RANCHER_VALUE: 'rancher' };\n"
  },
  {
    "path": "pkg/rancher-desktop/config/transientSettings.ts",
    "content": "import _ from 'lodash';\n\nimport { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';\n\nexport const navItemNames = [\n  'Application',\n  'WSL',\n  'Virtual Machine',\n  'Container Engine',\n  'Kubernetes',\n] as const;\n\nexport type NavItemName = typeof navItemNames[number];\n\nexport const defaultTransientSettings = {\n  noModalDialogs: false,\n  preferences:    {\n    navItem: {\n      current:     'Application' as NavItemName,\n      currentTabs: {\n        Application:        'general',\n        'Virtual Machine':  'hardware',\n        'Container Engine': 'general',\n        ...(process.platform === 'win32' && { WSL: 'integration' }),\n      } as Record<NavItemName, string | undefined>,\n    },\n  },\n};\nexport type TransientSettings = typeof defaultTransientSettings;\n\nclass TransientSettingsImpl {\n  private _value = _.cloneDeep(defaultTransientSettings);\n\n  get value(): RecursiveReadonly<TransientSettings> {\n    return this._value;\n  }\n\n  update(transientSettings: RecursivePartial<TransientSettings>) {\n    _.merge(this._value, transientSettings);\n  }\n}\n\nexport const TransientSettings = new TransientSettingsImpl();\n"
  },
  {
    "path": "pkg/rancher-desktop/config/types.js",
    "content": "// --------------------------------------\n// 1. Provided by Steve and always potentially available\n// --------------------------------------\n\n// Standalone steve\n// Base: /v1\nexport const STEVE = {\n  PREFERENCE: 'userpreference',\n  CLUSTER:    'cluster',\n};\n\n// Auth (via Norman)\n// Base: /v3\nexport const NORMAN = {\n  AUTH_CONFIG:                   'authconfig',\n  ETCD_BACKUP:                   'etcdbackup',\n  CLUSTER_TOKEN:                 'clusterregistrationtoken',\n  CLUSTER_ROLE_TEMPLATE_BINDING: 'clusterRoleTemplateBinding',\n  GROUP:                         'group',\n  PRINCIPAL:                     'principal',\n  PROJECT:                       'project',\n  SPOOFED:                       { GROUP_PRINCIPAL: 'group.principal' },\n  TOKEN:                         'token',\n  USER:                          'user',\n};\n\n// Public (via Norman)\n// Base: /v3-public\nexport const PUBLIC = { AUTH_PROVIDER: 'authprovider' };\n\n// Common native k8s types (via Steve)\n// Base: /k8s/clusters/<id>/v1/\nexport const API_GROUP = 'apiGroups';\nexport const API_SERVICE = 'apiregistration.k8s.io.apiservice';\nexport const CONFIG_MAP = 'configmap';\nexport const COUNT = 'count';\nexport const EVENT = 'event';\nexport const ENDPOINTS = 'endpoints';\nexport const HPA = 'autoscaling.horizontalpodautoscaler';\nexport const INGRESS = 'networking.k8s.io.ingress';\nexport const NAMESPACE = 'namespace';\nexport const NODE = 'node';\nexport const NETWORK_POLICY = 'networking.k8s.io.networkpolicy';\nexport const POD = 'pod';\nexport const PV = 'persistentvolume';\nexport const PVC = 'persistentvolumeclaim';\nexport const RESOURCE_QUOTA = 'resourcequota';\nexport const SCHEMA = 'schema';\nexport const SERVICE = 'service';\nexport const SECRET = 'secret';\nexport const SERVICE_ACCOUNT = 'serviceaccount';\nexport const STORAGE_CLASS = 'storage.k8s.io.storageclass';\nexport const OBJECT_META = 'io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta';\n\nexport const RBAC = {\n  ROLE:                 'rbac.authorization.k8s.io.role',\n  CLUSTER_ROLE:         'rbac.authorization.k8s.io.clusterrole',\n  ROLE_BINDING:         'rbac.authorization.k8s.io.rolebinding',\n  CLUSTER_ROLE_BINDING: 'rbac.authorization.k8s.io.clusterrolebinding',\n};\n\nexport const WORKLOAD = 'workload';\n\n// The types that are aggregated into a \"workload\"\nexport const WORKLOAD_TYPES = {\n  DEPLOYMENT:             'apps.deployment',\n  CRON_JOB:               'batch.cronjob',\n  DAEMON_SET:             'apps.daemonset',\n  JOB:                    'batch.job',\n  STATEFUL_SET:           'apps.statefulset',\n  REPLICA_SET:            'apps.replicaset',\n  REPLICATION_CONTROLLER: 'replicationcontroller',\n};\n\nconst {\n  DAEMON_SET, CRON_JOB, JOB, ...scalableWorkloads\n} = WORKLOAD_TYPES;\n\nexport const SCALABLE_WORKLOAD_TYPES = scalableWorkloads;\n\nexport const METRIC = {\n  NODE: 'metrics.k8s.io.nodemetrics',\n  POD:  'metrics.k8s.io.podmetrics',\n};\n\nexport const CATALOG = {\n  CLUSTER_REPO: 'catalog.cattle.io.clusterrepo',\n  OPERATION:    'catalog.cattle.io.operation',\n  APP:          'catalog.cattle.io.app',\n  REPO:         'catalog.cattle.io.repo',\n};\n\nexport const MONITORING = {\n  ALERTMANAGER:   'monitoring.coreos.com.alertmanager',\n  PODMONITOR:     'monitoring.coreos.com.podmonitor',\n  PROMETHEUS:     'monitoring.coreos.com.prometheus',\n  PROMETHEUSRULE: 'monitoring.coreos.com.prometheusrule',\n  SERVICEMONITOR: 'monitoring.coreos.com.servicemonitor',\n  THANOSRULER:    'monitoring.coreos.com.thanosruler',\n  SPOOFED:        {\n    RECEIVER:             'monitoring.coreos.com.receiver',\n    RECEIVER_SPEC:        'monitoring.coreos.com.receiver.spec',\n    RECEIVER_EMAIL:       'monitoring.coreos.com.receiver.email',\n    RECEIVER_SLACK:       'monitoring.coreos.com.receiver.slack',\n    RECEIVER_WEBHOOK:     'monitoring.coreos.com.receiver.webhook',\n    RECEIVER_PAGERDUTY:   'monitoring.coreos.com.receiver.pagerduty',\n    RECEIVER_OPSGENIE:    'monitoring.coreos.com.receiver.opsgenie',\n    RECEIVER_HTTP_CONFIG: 'monitoring.coreos.com.receiver.httpconfig',\n    RESPONDER:            'monitoring.coreos.com.receiver.responder',\n    ROUTE:                'monitoring.coreos.com.route',\n    ROUTE_SPEC:           'monitoring.coreos.com.route.spec',\n  },\n};\n\nexport const LONGHORN = {\n  ENGINES:       'longhorn.io.engine',\n  ENGINE_IMAGES: 'longhorn.io.engineimage',\n  NODES:         'longhorn.io.node',\n  REPLICAS:      'longhorn.io.replica',\n  SETTINGS:      'longhorn.io.setting',\n  VOLUMES:       'longhorn.io.volume',\n};\n\n// --------------------------------------\n// 2. Only if Rancher is installed\n// --------------------------------------\n\n// Rancher Management API (via Steve)\n// Base: /v1\nexport const MANAGEMENT = {\n  AUTH_CONFIG:                   'management.cattle.io.authconfig',\n  CATALOG_TEMPLATE:              'management.cattle.io.catalogtemplate',\n  CATALOG:                       'management.cattle.io.catalog',\n  CLUSTER:                       'management.cattle.io.cluster',\n  CLUSTER_ROLE_TEMPLATE_BINDING: 'management.cattle.io.clusterroletemplatebinding',\n  FEATURE:                       'management.cattle.io.feature',\n  GROUP:                         'management.cattle.io.group',\n  KONTAINER_DRIVER:              'management.cattle.io.kontainerdriver',\n  NODE_DRIVER:                   'management.cattle.io.nodedriver',\n  NODE_POOL:                     'management.cattle.io.nodepool',\n  NODE_TEMPLATE:                 'management.cattle.io.nodetemplate',\n  PROJECT:                       'management.cattle.io.project',\n  PROJECT_ROLE_TEMPLATE_BINDING: 'management.cattle.io.projectroletemplatebinding',\n  ROLE_TEMPLATE:                 'management.cattle.io.roletemplate',\n  SETTING:                       'management.cattle.io.setting',\n  USER:                          'management.cattle.io.user',\n  TOKEN:                         'management.cattle.io.token',\n  GLOBAL_ROLE:                   'management.cattle.io.globalrole',\n  GLOBAL_ROLE_BINDING:           'management.cattle.io.globalrolebinding',\n  POD_SECURITY_POLICY_TEMPLATE:  'management.cattle.io.podsecuritypolicytemplate',\n};\n\nexport const CAPI = {\n  CAPI_CLUSTER:         'cluster.x-k8s.io.cluster',\n  MACHINE_DEPLOYMENT:   'cluster.x-k8s.io.machinedeployment',\n  MACHINE_SET:          'cluster.x-k8s.io.machineset',\n  MACHINE:              'cluster.x-k8s.io.machine',\n  RANCHER_CLUSTER:      'provisioning.cattle.io.cluster',\n  MACHINE_CONFIG_GROUP: 'rke-machine-config.cattle.io',\n};\n\n// --------------------------------------\n// 3. Optional add-on packages in a cluster\n// --------------------------------------\n// Base: /k8s/clusters/<id>/v1/\n\nexport const FLEET = {\n  BUNDLE:        'fleet.cattle.io.bundle',\n  CLUSTER:       'fleet.cattle.io.cluster',\n  CLUSTER_GROUP: 'fleet.cattle.io.clustergroup',\n  GIT_REPO:      'fleet.cattle.io.gitrepo',\n  WORKSPACE:     'management.cattle.io.fleetworkspace',\n  TOKEN:         'fleet.cattle.io.clusterregistrationtoken',\n};\n\nexport const GATEKEEPER = {\n  CONSTRAINT_TEMPLATE: 'templates.gatekeeper.sh.constrainttemplate',\n  SPOOFED:             { CONSTRAINT: 'constraints.gatekeeper.sh.constraint' },\n};\n\nexport const ISTIO = {\n  VIRTUAL_SERVICE:  'networking.istio.io.virtualservice',\n  DESTINATION_RULE: 'networking.istio.io.destinationrule',\n  GATEWAY:          'networking.istio.io.gateway',\n};\n\nexport const RIO = {\n  CLUSTER_DOMAIN: 'admin.rio.cattle.io.clusterdomain',\n  FEATURE:        'admin.rio.cattle.io.feature',\n  INFO:           'admin.rio.cattle.io.rioinfo',\n  PUBLIC_DOMAIN:  'admin.rio.cattle.io.publicdomain',\n\n  APP:              'rio.cattle.io.app',\n  EXTERNAL_SERVICE: 'rio.cattle.io.externalservice',\n  STACK:            'rio.cattle.io.stack',\n  ROUTER:           'rio.cattle.io.router',\n  SERVICE:          'rio.cattle.io.service',\n\n  SYSTEM_NAMESPACE: 'rio-system',\n};\n\nexport const LOGGING = {\n  // LOGGING:        'logging.banzaicloud.io.logging',\n  CLUSTER_FLOW:   'logging.banzaicloud.io.clusterflow',\n  CLUSTER_OUTPUT: 'logging.banzaicloud.io.clusteroutput',\n  FLOW:           'logging.banzaicloud.io.flow',\n  OUTPUT:         'logging.banzaicloud.io.output',\n  SPOOFED:        {\n    FILTERS:            'logging.banzaicloud.io.output.filters',\n    FILTER:             'logging.banzaicloud.io.output.filter',\n    CONCAT:             'logging.banzaicloud.io.output.filters.concat',\n    DEDOT:              'logging.banzaicloud.io.output.filters.dedot',\n    DETECTEXCEPTIONS:   'logging.banzaicloud.io.output.filters.detectExceptions',\n    GEOIP:              'logging.banzaicloud.io.output.filters.geoip',\n    GREP:               'logging.banzaicloud.io.output.filters.grep',\n    PARSER:             'logging.banzaicloud.io.output.filters.parser',\n    PROMETHEUS:         'logging.banzaicloud.io.output.filters.prometheus',\n    RECORD_MODIFIER:    'logging.banzaicloud.io.output.filters.record_modifier',\n    RECORD_TRANSFORMER: 'logging.banzaicloud.io.output.filters.record_transformer',\n    STDOUT:             'logging.banzaicloud.io.output.filters.stdout',\n    SUMOLOGIC:          'logging.banzaicloud.io.output.filters.sumologic',\n    TAG_NORMALISER:     'logging.banzaicloud.io.output.filters.tag_normaliser',\n    THROTTLE:           'logging.banzaicloud.io.output.filters.throttle',\n    RECORD:             'logging.banzaicloud.io.output.filters.record',\n    REGEXPSECTION:      'logging.banzaicloud.io.output.filters.regexpsection',\n    EXCLUDESECTION:     'logging.banzaicloud.io.output.filters.excludesection',\n    ORSECTION:          'logging.banzaicloud.io.output.filters.orsection',\n    ANDSECTION:         'logging.banzaicloud.io.output.filters.andsection',\n    PARSESECTION:       'logging.banzaicloud.io.output.filters.parsesection',\n    METRICSECTION:      'logging.banzaicloud.io.output.filters.metricsection',\n    REPLACE:            'logging.banzaicloud.io.output.filters.replace',\n    SINGLEPARSESECTION: 'logging.banzaicloud.io.output.filters.replace.singleparsesection',\n  },\n};\n\nexport const BACKUP_RESTORE = {\n  RESOURCE_SET: 'resources.cattle.io.resourceset',\n  BACKUP:       'resources.cattle.io.backup',\n  RESTORE:      'resources.cattle.io.restore',\n};\n\nexport const CIS = {\n  CLUSTER_SCAN:         'cis.cattle.io.clusterscan',\n  CLUSTER_SCAN_PROFILE: 'cis.cattle.io.clusterscanprofile',\n  BENCHMARK:            'cis.cattle.io.clusterscanbenchmark',\n  REPORT:               'cis.cattle.io.clusterscanreport',\n};\n\nexport const UI = { NAV_LINK: 'ui.cattle.io.navlink' };\n"
  },
  {
    "path": "pkg/rancher-desktop/entry/README.md",
    "content": "This directory contains the things used to hook up to Vue.\n"
  },
  {
    "path": "pkg/rancher-desktop/entry/index.ts",
    "content": "/**\n * This is the main entry point for Vue.\n */\n\nimport Cookies from 'cookie-universal';\nimport { createApp } from 'vue';\n\nimport usePlugins from './plugins';\nimport router from './router';\nimport store from './store';\n\n// This does just the Vuex part of cookie-universal-nuxt, which is all we need.\n(store as any).$cookies = Cookies();\n\n// Emulate Nuxt layouts by poking making the router match the main component we\n// will load, and then inspect it for the layout we set.\n// Because we're always using the hash mode for the router, get the correct\n// route based on the hash.\nconst matched = router.resolve(location.hash.substring(1)).matched.find(r => r);\nconst component = matched?.components?.default;\nconst layoutName: string = (component as any)?.layout ?? 'default';\nconst { default: layout } = await import(`../layouts/${ layoutName }.vue`);\n\nconst app = createApp(layout);\n\napp.use(store);\napp.use(router);\nawait usePlugins(app, store);\n\napp.mount('#app');\n"
  },
  {
    "path": "pkg/rancher-desktop/entry/plugins.ts",
    "content": "import { App } from 'vue';\nimport { Store } from 'vuex';\n\nimport cleanHTML from '../plugins/clean-html-directive';\nimport cleanTooltip from '../plugins/clean-tooltip-directive';\nimport directives from '../plugins/directives';\nimport i18n from '../plugins/i18n';\nimport shortKey from '../plugins/shortkey';\nimport tooltip from '../plugins/tooltip';\nimport trimWhitespace from '../plugins/trim-whitespace';\nimport vSelect from '../plugins/v-select';\n\nexport default async function usePlugins(app: App, store: Store<any>) {\n  await store.dispatch('i18n/init');\n\n  app.use(cleanHTML);\n  app.use(cleanTooltip);\n  app.use(directives);\n  app.use(i18n);\n  app.use(shortKey, {\n    prevent:          ['input', 'textarea', 'select'],\n    preventContainer: ['#modal-container-element'],\n  });\n  app.use(tooltip);\n  app.use(trimWhitespace);\n  app.use(vSelect);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/entry/router.ts",
    "content": "import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';\n\nimport Containers from '../pages/Containers.vue';\nimport DenyRoot from '../pages/DenyRoot.vue';\nimport Diagnostics from '../pages/Diagnostics.vue';\nimport Dialog from '../pages/Dialog.vue';\nimport Extensions from '../pages/Extensions.vue';\nimport FirstRun from '../pages/FirstRun.vue';\nimport General from '../pages/General.vue';\nimport Images from '../pages/Images.vue';\nimport KubernetesError from '../pages/KubernetesError.vue';\nimport PortForwarding from '../pages/PortForwarding.vue';\nimport Preferences from '../pages/Preferences.vue';\nimport Snapshots from '../pages/Snapshots.vue';\nimport SudoPrompt from '../pages/SudoPrompt.vue';\nimport Troubleshooting from '../pages/Troubleshooting.vue';\nimport UnmetPrerequisites from '../pages/UnmetPrerequisites.vue';\nimport Volumes from '../pages/Volumes.vue';\nimport ContainerInfo from '../pages/containers/ContainerInfo.vue';\nimport ExtensionsItem from '../pages/extensions/_root/_src/_id.vue';\nimport ImagesAdd from '../pages/images/add.vue';\nimport ImagesScan from '../pages/images/scans/_image-name.vue';\nimport SnapshotsCreate from '../pages/snapshots/create.vue';\nimport SnapshotsDialog from '../pages/snapshots/dialog.vue';\nimport VolumeFiles from '../pages/volumes/files/_name.vue';\n\nexport default createRouter({\n  history: createWebHashHistory(),\n  routes:  [\n    { path: '/', redirect: '/General' },\n    {\n      path: '/General', component: General, name: 'General',\n    },\n    {\n      path: '/Containers', component: Containers, name: 'Containers',\n    },\n    {\n      path: '/containers/info/:id', component: ContainerInfo, name: 'container-info',\n    },\n    {\n      path: '/Volumes', component: Volumes, name: 'Volumes',\n    },\n    {\n      path: '/volumes/files/:name', component: VolumeFiles, name: 'volumes-files-name',\n    },\n    {\n      path: '/PortForwarding', component: PortForwarding, name: 'Port Forwarding',\n    },\n    {\n      path:      '/Images',\n      component: Images,\n      name:      'Images',\n    },\n    {\n      path: '/images/add', component: ImagesAdd, name: 'images-add',\n    },\n    {\n      path: '/images/scans/:image-name?/:namespace?', component: ImagesScan, name: 'images-scans-image-name',\n    },\n    {\n      path: '/Snapshots', component: Snapshots, name: 'Snapshots',\n    },\n    {\n      path: '/snapshots/create', component: SnapshotsCreate, name: 'snapshots-create',\n    },\n    {\n      path: '/Troubleshooting', component: Troubleshooting, name: 'Troubleshooting',\n    },\n    {\n      path: '/Diagnostics', component: Diagnostics, name: 'Diagnostics',\n    },\n    {\n      path: '/Extensions', component: Extensions, name: 'Extensions',\n    },\n    {\n      path: '/extensions/:id/:root(.*)/:src', component: ExtensionsItem, name: 'rdx-root-src-id',\n    },\n    {\n      path: '/DenyRoot', component: DenyRoot, name: 'DenyRoot',\n    },\n    {\n      path: '/FirstRun', component: FirstRun, name: 'FirstRun',\n    },\n    {\n      path: '/KubernetesError', component: KubernetesError, name: 'KubernetesError',\n    },\n    {\n      path: '/preferences', component: Preferences, name: 'Preferences',\n    },\n    {\n      path: '/Dialog', component: Dialog, name: 'Dialog',\n    },\n    {\n      path: '/SudoPrompt', component: SudoPrompt, name: 'SudoPrompt',\n    },\n    {\n      path: '/UnmetPrerequisites', component: UnmetPrerequisites, name: 'UnmetPrerequisites',\n    },\n    {\n      path: '/SnapshotsDialog', component: SnapshotsDialog, name: 'SnapshotsDialog',\n    },\n  ],\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/entry/store.ts",
    "content": "import { createStore, mapActions, mapGetters, mapMutations, mapState, ModuleTree, Plugin } from 'vuex';\n\nimport * as ActionMenu from '../store/action-menu';\nimport * as ApplicationSettings from '../store/applicationSettings';\nimport * as ContainerEngine from '../store/container-engine';\nimport * as Credentials from '../store/credentials';\nimport * as Diagnostics from '../store/diagnostics';\nimport * as Extensions from '../store/extensions';\nimport * as I18n from '../store/i18n';\nimport * as ImageManager from '../store/imageManager';\nimport * as K8sManager from '../store/k8sManager';\nimport * as Page from '../store/page';\nimport * as Preferences from '../store/preferences';\nimport * as Prefs from '../store/prefs';\nimport * as ResourceFetch from '../store/resource-fetch';\nimport * as Snapshots from '../store/snapshots';\nimport * as TransientSettings from '../store/transientSettings';\n\nconst modules = {\n  'action-menu':       ActionMenu,\n  applicationSettings: ApplicationSettings,\n  'container-engine':  ContainerEngine,\n  credentials:         Credentials,\n  diagnostics:         Diagnostics,\n  extensions:          Extensions,\n  i18n:                I18n,\n  imageManager:        ImageManager,\n  k8sManager:          K8sManager,\n  page:                Page,\n  preferences:         Preferences,\n  prefs:               Prefs,\n  'resource-fetch':    ResourceFetch,\n  snapshots:           Snapshots,\n  transientSettings:   TransientSettings,\n};\n\nexport default createStore<any>({\n  modules: Object.fromEntries(Object.entries(modules).map(([k, v]) => [k, { namespaced: true, ...v }])),\n  plugins: Object.values(modules).flatMap(v => 'plugins' in v ? v.plugins : []),\n});\n\nexport type Modules = typeof modules;\n\n/**\n * mapTypedGetters is a wrapper around mapGetters that is aware of the types of\n * the Vuex stores we have availabile, and returns the correctly typed values.\n * @see https://vuex.vuejs.org/guide/getters.html#the-mapgetters-helper\n */\n// mapTypedGetters('namespace', ['getter', 'getter'])\nexport function mapTypedGetters\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { getters: any } ? Modules[N]['getters'] : never,\n  K extends keyof M,\n>(namespace: N, keys: K[]): { [key in K]: () => ReturnType<M[key]> };\n// mapTypedGetters('namespace', {name: newName, name: newName})\nexport function mapTypedGetters\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { getters: any } ? Modules[N]['getters'] : never,\n  K extends keyof M,\n  G extends Record<string, K>,\n>(namespace: N, mappings: G): { [key in keyof G]: () => ReturnType<M[G[key]]> };\n// Actual implementation defers to mapGetters.\nexport function mapTypedGetters(namespace: string, arg: any) {\n  return mapGetters(namespace, arg);\n}\n\n/**\n * mapTypedState is a wrapper around mapState that is aware of the types of the\n * Vuex stores we have available, and returns the correctly typed values.\n * @see https://vuex.vuejs.org/guide/state.html#the-mapstate-helper\n */\n// mapTypedState('namespace', ['state', 'state'])\nexport function mapTypedState\n<\n  N extends keyof Modules,\n  S extends ReturnType<Modules[N]['state']>,\n  K extends keyof S,\n>(namespace: N, keys: K[]): { [key in K]: () => S[key] };\n// mapTypedState('namespace', {key: 'name', key: (state) => (state.key)})\nexport function mapTypedState\n<\n  N extends keyof Modules,\n  S extends ReturnType<Modules[N]['state']>,\n  K extends keyof S,\n  G extends Record<string, K | ((state: S) => any)>,\n>(namespace: N, mappings: G): {\n  [key in keyof G]:\n  G[key] extends K ? () => S[G[key]] :\n    G[key] extends (state: S) => any ? () => ReturnType<G[key]> :\n      never;\n};\n// Actual implementation defers to mapState\nexport function mapTypedState(namespace: string, arg: any) {\n  return mapState(namespace, arg);\n}\n\n/**\n * mapTypedMutations is a wrapper around mapMutations that is aware of the types\n * of the Vuex stores we have available, and returns the correctly typed values.\n * @see https://vuex.vuejs.org/guide/mutations.html#committing-mutations-in-components\n */\n// mapTypedMutations('namespace', ['mutation', 'mutation'])\nexport function mapTypedMutations\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { mutations: any } ? Modules[N]['mutations'] : never,\n  K extends keyof M,\n>(namespace: N, keys: K[]): { [key in K]: (payload: Parameters<M[key]>[1]) => ReturnType<M[key]> };\n// mapTypedMutations('namespace', {key: 'name', key: (state) => (state.key)})\nexport function mapTypedMutations\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { mutations: any } ? Modules[N]['mutations'] : never,\n  K extends keyof M,\n  G extends Record<string, K>,\n>(namespace: N, mappings: G): {\n  [key in keyof G]: (payload: Parameters<M[G[key]]>[1]) => ReturnType<M[G[key]]>;\n};\n// Actual implementation defers to mapMutations\nexport function mapTypedMutations(namespace: string, arg: any) {\n  return mapMutations(namespace, arg);\n}\n\n/**\n * mapTypedActions is a wrapper around mapActions that is aware of the types\n * of the Vuex stores we have available, and returns the correctly typed values.\n * @see https://vuex.vuejs.org/guide/actions.html#dispatching-actions-in-components\n */\nexport function mapTypedActions\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { actions: any } ? Modules[N]['actions'] : never,\n  K extends keyof M,\n>(namespace: N, keys: K[]): {\n  [key in K]: Parameters<M[key]> extends [state: any]\n    ? () => Promise<Awaited<ReturnType<M[key]>>> // no arguments\n    : (payload: Parameters<M[key]>[1]) => Promise<Awaited<ReturnType<M[key]>>>\n};\nexport function mapTypedActions\n<\n  N extends keyof Modules,\n  M extends Modules[N] extends { actions: any } ? Modules[N]['actions'] : never,\n  K extends keyof M,\n  G extends Record<string, K>,\n>(namespace: N, mappings: G): {\n  [key in keyof G]: Parameters<M[G[key]]> extends [state: any]\n    ? () => Promise<Awaited<ReturnType<M[G[key]]>>> // no arguments\n    : (payload: Parameters<M[G[key]]>[1]) => Promise<Awaited<ReturnType<M[G[key]]>>>;\n};\nexport function mapTypedActions(namespace: string, arg: any) {\n  return mapActions(namespace, arg) as any;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/hocs/README.md",
    "content": "Higher-Order Components (HOCs) are functions that take in a component as an argument and return a new component with additional functionality. HOCs are a useful pattern for reusing component logic and implementing cross-cutting concerns, such as authentication and authorization, caching, logging, and error handling. They allow for extracting common functionality into a separate component and apply it to multiple components without duplicating code. \n"
  },
  {
    "path": "pkg/rancher-desktop/hocs/withCredentials.ts",
    "content": "import { ref, computed, onBeforeMount } from 'vue';\nimport { useStore } from 'vuex';\n\nimport type { Credentials } from '@pkg/store/credentials';\n\nexport default function useCredentials() {\n  const credentials = ref<Credentials>({\n    user:     '',\n    password: '',\n    port:     0,\n  });\n\n  const hasCredentials = computed(() => {\n    return !!credentials.value.user || !!credentials.value.password || !!credentials.value.port;\n  });\n\n  onBeforeMount(async() => {\n    credentials.value = await useStore().dispatch('credentials/fetchCredentials');\n  });\n\n  return { credentials, hasCredentials };\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/index.ts",
    "content": "/***\n * NOTE: @rancher/shell was removed from this project during the Nuxt removal.\n * I'm keeping this file around for its eventual reintroduction; it was easier\n * to limit the potential scope of issues associated with the Nuxt removal by\n * removing @rancher/shell\n */\n// import { importTypes } from '@rancher/auto-import';\n// import { IPlugin } from '@shell/core/types';\n\n// import routes from './router';\n\n// // Init the package\n// export default function(plugin: IPlugin) {\n//   // Auto-import model, detail, edit from the folders\n//   importTypes(plugin);\n\n//   // Provide plugin metadata from package.json\n//   plugin.metadata = require('./package.json');\n\n//   // Load a product\n//   plugin.addProduct(require('./product'));\n\n//   plugin.addRoutes(routes);\n// }\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/__tests__/manageLinesInFile.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { jest } from '@jest/globals';\n\nimport * as childProcess from '@pkg/utils/childProcess';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\nimport { withResource } from '@pkg/utils/testUtils/mockResources';\n\nconst modules = mockModules({\n  fs: {\n    ...fs,\n    promises: {\n      ...fs.promises,\n      rename:    jest.fn(fs.promises.rename),\n      writeFile: jest.fn(fs.promises.writeFile),\n    },\n  },\n});\n\nconst describeUnix = process.platform === 'win32' ? describe.skip : describe;\nconst testUnix = process.platform === 'win32' ? test.skip : test;\n\nconst FILE_NAME = 'fakercfile';\nconst TEST_LINE_1 = 'this is test line 1';\nconst TEST_LINE_2 = 'this is test line 2';\n\nconst { default: manageLinesInFile, START_LINE, END_LINE } = await import('@pkg/integrations/manageLinesInFile');\n\nlet testDir: string;\nlet rcFilePath: string;\nlet backupFilePath: string;\nlet tempFilePath: string;\nlet symlinkPath: string;\nlet SystemError: new (key: string, context: { code: string, syscall: string, message: string }) => NodeJS.ErrnoException;\n\nbeforeEach(async() => {\n  testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rdtest-'));\n  rcFilePath = path.join(testDir, FILE_NAME);\n  backupFilePath = `${ rcFilePath }.rd-backup~`;\n  tempFilePath = `${ rcFilePath }.rd-temp`;\n  symlinkPath = `${ rcFilePath }.real`;\n  SystemError = await (async() => {\n    try {\n      await fs.promises.readFile(rcFilePath);\n    } catch (ex) {\n      return Object.getPrototypeOf(ex).constructor;\n    }\n  })();\n});\n\nafterEach(async() => {\n  // It is best to be careful around rm's; we don't want to remove important things.\n  if (testDir) {\n    await fs.promises.rm(testDir, {\n      recursive: true, force: true, maxRetries: 5,\n    });\n  }\n});\n\ndescribe('manageLinesInFile', () => {\n  describe('Target does not exist', () => {\n    test('Create the file when desired', async() => {\n      const expectedContents = [START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n');\n\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], true);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).resolves.toEqual(expectedContents);\n      await expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n    });\n\n    test('Do nothing when not desired', async() => {\n      await expect(manageLinesInFile(rcFilePath, [TEST_LINE_1], false)).resolves.not.toThrow();\n      await expect(fs.promises.readFile(rcFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n    });\n  });\n\n  describe('Target exists as a plain file', () => {\n    testUnix('Preserves extended attributes', async() => {\n      const { listAttributes, getAttribute, setAttribute } = await import('@napi-rs/xattr');\n      const unmanagedContents = 'existing lines\\n';\n      const attributeKey = 'user.io.rancherdesktop.test';\n      const attributeValue = 'sample attribute contents';\n\n      await fs.promises.writeFile(rcFilePath, unmanagedContents);\n      await setAttribute(rcFilePath, attributeKey, attributeValue);\n      await expect(manageLinesInFile(rcFilePath, [TEST_LINE_1], true)).resolves.not.toThrow();\n\n      const allAttrs: string[] = await listAttributes(rcFilePath);\n      // filter out system-provided attributes (macOS com.apple.*, SELinux security.*)\n      const filteredAttrs = allAttrs.filter(item => !item.startsWith('com.apple.') && !item.startsWith('security.'));\n\n      expect(filteredAttrs).toEqual([attributeKey]);\n      await expect(getAttribute(rcFilePath, attributeKey)).resolves.toEqual(Buffer.from(attributeValue, 'utf-8'));\n    });\n\n    test('Delete file when false and it contains only the managed lines', async() => {\n      const data = [START_LINE, TEST_LINE_1, END_LINE].join('\\n');\n\n      await fs.promises.writeFile(rcFilePath, data, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n    });\n\n    test('Put lines in file that exists and has content', async() => {\n      const { listAttributes } = await import('@napi-rs/xattr');\n      const data = 'this is already present in the file\\n';\n      const expectedContents = [data, START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n');\n\n      await fs.promises.writeFile(rcFilePath, data, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], true);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).resolves.toEqual(expectedContents);\n      if (process.platform !== 'win32') {\n        const allAttrs: string[] = await listAttributes(rcFilePath);\n        // filter out system-provided attributes (macOS com.apple.*, SELinux security.*)\n        const filteredAttrs = allAttrs.filter(item => !item.startsWith('com.apple.') && !item.startsWith('security.'));\n\n        expect(filteredAttrs).toHaveLength(0);\n      }\n    });\n\n    test('Remove lines from file that exists and has content', async() => {\n      const unmanagedContents = 'this is already present in the file\\n';\n      const contents = [unmanagedContents, START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n');\n\n      expect(contents).toMatch(/(?<!\\n)\\n$/);\n      await fs.promises.writeFile(rcFilePath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).resolves.toEqual(unmanagedContents);\n    });\n\n    test('Update managed lines', async() => {\n      const topUnmanagedContents = 'this is at the top of the file\\n';\n      const bottomUnmanagedContents = 'this is at the bottom of the file\\n';\n      const contents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, END_LINE, bottomUnmanagedContents].join('\\n');\n      const expectedNewContents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, TEST_LINE_2, END_LINE,\n        bottomUnmanagedContents].join('\\n');\n\n      await fs.promises.writeFile(rcFilePath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1, TEST_LINE_2], true);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).resolves.toEqual(expectedNewContents);\n    });\n\n    test('Remove managed lines from between unmanaged lines', async() => {\n      const topUnmanagedContents = 'this is at the top of the file\\n';\n      const bottomUnmanagedContents = 'this is at the bottom of the file\\n';\n      const contents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, END_LINE, bottomUnmanagedContents].join('\\n');\n      const expectedNewContents = [topUnmanagedContents, bottomUnmanagedContents].join('\\n');\n\n      await fs.promises.writeFile(rcFilePath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(rcFilePath, 'utf8')).resolves.toEqual(expectedNewContents);\n    });\n\n    test('File mode should not be changed when updating a file', async() => {\n      const unmanagedContents = 'this is already present in the file\\n';\n      const contents = [unmanagedContents, START_LINE, TEST_LINE_1, END_LINE].join('\\n');\n\n      await fs.promises.writeFile(rcFilePath, contents, { mode: 0o623 });\n      const { mode: actualMode } = await fs.promises.stat(rcFilePath);\n\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.stat(rcFilePath)).resolves.toHaveProperty('mode', actualMode);\n    });\n\n    test('Should not write directly to target file', async() => {\n      const unmanagedContents = 'existing lines\\n';\n\n      await fs.promises.writeFile(rcFilePath, unmanagedContents, { mode: 0o600 });\n\n      using spyWriteFile = withResource(modules.fs.promises.writeFile);\n      using spyRename = withResource(modules.fs.promises.rename);\n\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], true);\n      expect(spyWriteFile).not.toHaveBeenCalledWith(rcFilePath, expect.anything());\n      expect(spyRename).toHaveBeenCalledWith(tempFilePath, rcFilePath);\n      expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      expect(fs.promises.readFile(rcFilePath, 'utf-8')).resolves\n        .toEqual([unmanagedContents, START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n'));\n    });\n\n    test('Handles errors writing to temporary file', async() => {\n      const unmanagedContents = 'existing lines\\n';\n\n      await fs.promises.writeFile(rcFilePath, unmanagedContents, { mode: 0o600 });\n      const originalWriteFile = fs.promises.writeFile;\n\n      using spyWriteFile = withResource(modules.fs.promises.writeFile)\n        .mockImplementation(async(file, data, options) => {\n          if (file.toString() === tempFilePath) {\n            throw new SystemError('EACCESS', {\n              code: 'EACCESS', syscall: 'write', message: '',\n            });\n          }\n          await originalWriteFile(file, data, options);\n        });\n\n      await expect(manageLinesInFile(rcFilePath, [TEST_LINE_1], true)).rejects.not.toBeUndefined();\n      expect(spyWriteFile).toHaveBeenCalledWith(tempFilePath, expect.anything(), expect.anything());\n      // The file should not have been modified\n      expect(fs.promises.readFile(rcFilePath, 'utf-8')).resolves.toEqual(unmanagedContents);\n      expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n    });\n  });\n\n  describeUnix('Target is a symlink', () => {\n    beforeEach(async() => {\n      await fs.promises.symlink(symlinkPath, rcFilePath, 'file');\n    });\n\n    test('Aborts if backup file already exists', async() => {\n      const backupContents = 'this is never read';\n      const unmanagedContents = 'existing lines\\n';\n\n      await fs.promises.writeFile(symlinkPath, unmanagedContents);\n      await fs.promises.writeFile(backupFilePath, backupContents);\n\n      await expect(manageLinesInFile(rcFilePath, ['hello'], true)).rejects.toThrow();\n      await expect(fs.promises.readFile(rcFilePath, 'utf-8')).resolves.toEqual(unmanagedContents);\n      await expect(fs.promises.readFile(backupFilePath, 'utf-8')).resolves.toEqual(backupContents);\n      await expect(fs.promises.readlink(rcFilePath)).resolves.toEqual(symlinkPath);\n    });\n\n    test('Leave the file empty if removing all content', async() => {\n      const data = [START_LINE, TEST_LINE_1, END_LINE].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, data, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(symlinkPath, 'utf8')).resolves.toEqual('');\n      await expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(backupFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readlink(rcFilePath)).resolves.toEqual(symlinkPath);\n    });\n\n    test('Put lines in file that exists and has content', async() => {\n      const data = 'this is already present in the file\\n';\n      const expectedContents = [data, START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, data, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], true);\n\n      await expect(fs.promises.readFile(symlinkPath, 'utf-8')).resolves.toEqual(expectedContents);\n      await expect(fs.promises.readlink(rcFilePath, 'utf-8')).resolves.toEqual(symlinkPath);\n    });\n\n    test('Remove lines from file that exists and has content', async() => {\n      const unmanagedContents = 'this is already present in the file\\n';\n      const contents = [unmanagedContents, START_LINE, TEST_LINE_1, END_LINE, ''].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(symlinkPath, 'utf-8')).resolves.toEqual(unmanagedContents);\n      await expect(fs.promises.readlink(rcFilePath, 'utf-8')).resolves.toEqual(symlinkPath);\n    });\n\n    test('Update managed lines', async() => {\n      const topUnmanagedContents = 'this is at the top of the file\\n';\n      const bottomUnmanagedContents = 'this is at the bottom of the file\\n';\n      const contents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, END_LINE, bottomUnmanagedContents].join('\\n');\n      const expectedNewContents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, TEST_LINE_2, END_LINE,\n        bottomUnmanagedContents].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1, TEST_LINE_2], true);\n\n      await expect(fs.promises.readFile(symlinkPath, 'utf8')).resolves.toEqual(expectedNewContents);\n      await expect(fs.promises.readlink(rcFilePath, 'utf-8')).resolves.toEqual(symlinkPath);\n    });\n\n    test('Remove managed lines from between unmanaged lines', async() => {\n      const topUnmanagedContents = 'this is at the top of the file\\n';\n      const bottomUnmanagedContents = 'this is at the bottom of the file\\n';\n      const contents = [\n        topUnmanagedContents, START_LINE, TEST_LINE_1, END_LINE, bottomUnmanagedContents].join('\\n');\n      const expectedNewContents = [topUnmanagedContents, bottomUnmanagedContents].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, contents, { mode: 0o644 });\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.readFile(symlinkPath, 'utf8')).resolves.toEqual(expectedNewContents);\n      await expect(fs.promises.readlink(rcFilePath, 'utf-8')).resolves.toEqual(symlinkPath);\n    });\n\n    test('File mode should not be changed when updating a file', async() => {\n      const unmanagedContents = 'this is already present in the file\\n';\n      const contents = [unmanagedContents, START_LINE, TEST_LINE_1, END_LINE].join('\\n');\n\n      await fs.promises.writeFile(symlinkPath, contents, { mode: 0o623 });\n      const { mode: actualMode } = await fs.promises.stat(symlinkPath);\n\n      await manageLinesInFile(rcFilePath, [TEST_LINE_1], false);\n\n      await expect(fs.promises.stat(symlinkPath)).resolves.toHaveProperty('mode', actualMode);\n      await expect(fs.promises.readlink(rcFilePath, 'utf-8')).resolves.toEqual(symlinkPath);\n    });\n\n    test('Write backup file during operation', async() => {\n      const unmanagedContents = 'existing lines\\n';\n\n      await fs.promises.writeFile(rcFilePath, unmanagedContents, { mode: 0o600 });\n      const originalWriteFile = fs.promises.writeFile;\n\n      using spyWriteFile = withResource(modules.fs.promises.writeFile)\n        .mockImplementation(async(file, data, options) => {\n          if (file !== rcFilePath) {\n            // Don't fail when writing to any other files.\n            await originalWriteFile(file, data, options);\n\n            return;\n          }\n          // When doing the actual write, the backup file should already have\n          // the old contents.\n          expect(await fs.promises.readFile(backupFilePath)).toEqual(unmanagedContents);\n          // We also haven't written to the target file yet.\n          expect(await fs.promises.readFile(symlinkPath)).toEqual(unmanagedContents);\n          // Throw an error and let it recover.\n          throw new SystemError('EIO', {\n            code: 'EIO', syscall: 'write', message: 'Fake error',\n          });\n        });\n\n      await expect(manageLinesInFile(rcFilePath, [TEST_LINE_1], true)).rejects.toThrow();\n      expect(spyWriteFile).toHaveBeenCalledWith(rcFilePath, expect.anything(), expect.anything());\n      await expect(fs.promises.readFile(tempFilePath)).rejects.toHaveProperty('code', 'ENOENT');\n      await expect(fs.promises.readFile(backupFilePath, 'utf-8')).resolves.toEqual(unmanagedContents);\n    });\n  });\n\n  describeUnix('Target is neither normal file nor symlink', () => {\n    // An incorrect implementation would write into the pipe and block, so\n    // set a timeout to ensure we bail in that case.\n    test('Abort if target is not a file', async() => {\n      await childProcess.spawnFile('mknod', [rcFilePath, 'p']);\n      await expect(manageLinesInFile(rcFilePath, [], true)).rejects.toThrow();\n      await expect(childProcess.spawnFile('test', ['-p', rcFilePath])).resolves.not.toThrow();\n    }, 1_000);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/__tests__/pathManager.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { jest } from '@jest/globals';\n\nimport { START_LINE, END_LINE } from '@pkg/integrations/manageLinesInFile';\njest.unstable_mockModule('@pkg/main/mainEvents', () => ({\n  __esModule: true,\n  default:    {\n    emit:   jest.fn(),\n    invoke: jest.fn(),\n  },\n}));\n\nconst describeUnix = os.platform() === 'win32' ? describe.skip : describe;\nlet testDir = '';\nconst savedEnv = process.env;\n\n// Recursively gets paths of all files in a specific directory and\n// its children. Files are returned as a flat array of absolute paths.\nfunction readdirRecursive(dirPath: string): string[] {\n  const dirents = fs.readdirSync(dirPath, { withFileTypes: true });\n  const files = dirents.map((dirent) => {\n    const absolutePath = path.resolve(dirPath, dirent.name);\n\n    return dirent.isDirectory() ? readdirRecursive(absolutePath) : absolutePath;\n  });\n\n  return files.flat();\n}\n\ndescribeUnix('RcFilePathManager', () => {\n  let pathManager: import('@pkg/integrations/pathManagerImpl').RcFilePathManager;\n\n  beforeEach(async() => {\n    const { RcFilePathManager } = await import('@pkg/integrations/pathManagerImpl');\n\n    pathManager = new RcFilePathManager();\n    testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rdtest-'));\n    const spy = jest.spyOn(os, 'homedir');\n\n    spy.mockReturnValue(testDir);\n    process.env = { ...process.env, XDG_CONFIG_HOME: path.join(testDir, '.config') };\n  });\n\n  afterEach(async() => {\n    process.env = savedEnv;\n    const spy = jest.spyOn(os, 'homedir');\n\n    spy.mockRestore();\n    await fs.promises.rm(testDir, { recursive: true, force: true });\n  });\n\n  describe('enforce', () => {\n    let bashProfilePath: string;\n    let bashLoginPath: string;\n    let profilePath: string;\n\n    beforeEach(() => {\n      bashProfilePath = path.join(testDir, '.bash_profile');\n      bashLoginPath = path.join(testDir, '.bash_login');\n      profilePath = path.join(testDir, '.profile');\n    });\n\n    it('should create rc files if they do not exist', async() => {\n      const rcNames = ['bashrc', 'zshrc', 'cshrc', 'tcshrc', 'config.fish'];\n\n      await pathManager.enforce();\n      let fileBlob = readdirRecursive(testDir).join(os.EOL);\n\n      rcNames.forEach((rcName) => {\n        expect(fileBlob).toMatch(rcName);\n      });\n      await pathManager.remove();\n      fileBlob = readdirRecursive(testDir).join(os.EOL);\n      rcNames.forEach((rcName) => {\n        expect(fileBlob).not.toMatch(rcName);\n      });\n    });\n\n    it('should create .bash_profile if it, .profile or .bash_login does not exist', async() => {\n      await pathManager.enforce();\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).resolves.toMatch('.rd/bin');\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).rejects.toThrow(/ENOENT/);\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).rejects.toThrow(/ENOENT/);\n    });\n\n    it('should modify .bash_profile if it, .bash_login and .profile exist', async() => {\n      await fs.promises.writeFile(bashProfilePath, '');\n      await fs.promises.writeFile(bashLoginPath, '');\n      await fs.promises.writeFile(profilePath, '');\n\n      await pathManager.enforce();\n\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).resolves.toMatch('.rd/bin');\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).resolves.not.toMatch('.rd/bin');\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).resolves.not.toMatch('.rd/bin');\n    });\n\n    it('should modify .bash_login if only it and/or .profile (and not .bash_profile) exist', async() => {\n      await fs.promises.writeFile(bashLoginPath, '');\n      await fs.promises.writeFile(profilePath, '');\n\n      await pathManager.enforce();\n\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).rejects.toThrow(/ENOENT/);\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).resolves.toMatch('.rd/bin');\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).resolves.not.toMatch('.rd/bin');\n    });\n\n    it('should modify .profile if only it (and not .bash_profile or .bash_login) exists', async() => {\n      await fs.promises.writeFile(profilePath, '');\n\n      await pathManager.enforce();\n\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).rejects.toThrow(/ENOENT/);\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).rejects.toThrow(/ENOENT/);\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).resolves.toMatch('.rd/bin');\n    });\n\n    it('should remove lines from bash login shell files if they exist', async() => {\n      const managedContent = 'managed content';\n      const unmanagedContent = 'should not be touched';\n      const content = [\n        unmanagedContent,\n        START_LINE,\n        managedContent,\n        END_LINE,\n      ].join(os.EOL);\n\n      await fs.promises.writeFile(bashProfilePath, content);\n      await fs.promises.writeFile(bashLoginPath, content);\n      await fs.promises.writeFile(profilePath, content);\n\n      await pathManager.remove();\n\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).resolves.not.toMatch(managedContent);\n      await expect(fs.promises.readFile(bashProfilePath, { encoding: 'utf-8' })).resolves.toMatch(unmanagedContent);\n\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).resolves.not.toMatch(managedContent);\n      await expect(fs.promises.readFile(bashLoginPath, { encoding: 'utf-8' })).resolves.toMatch(unmanagedContent);\n\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).resolves.not.toMatch(managedContent);\n      await expect(fs.promises.readFile(profilePath, { encoding: 'utf-8' })).resolves.toMatch(unmanagedContent);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/__tests__/unixIntegrationManager.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport UnixIntegrationManager, { ensureSymlink } from '@pkg/integrations/unixIntegrationManager';\n\nconst INTEGRATION_DIR_NAME = 'integrationDir';\nconst TMPDIR_PREFIX = 'rdtest-';\n\nconst describeUnix = os.platform() === 'win32' ? describe.skip : describe;\nconst binDir = path.join('resources', os.platform(), 'bin');\nconst dockerCLIPluginSource = path.join('resources', os.platform(), 'docker-cli-plugins');\nlet testDir: string;\n\n// Creates integration directory and docker CLI plugin directory with\n// relevant symlinks in them. Useful for testing removal parts\n// of UnixIntegrationManager.\nasync function createTestSymlinks(integrationDirectory: string, dockerCLIPluginDest: string): Promise<void> {\n  await fs.promises.mkdir(integrationDirectory, { recursive: true, mode: 0o755 });\n  await fs.promises.mkdir(dockerCLIPluginDest, { recursive: true, mode: 0o755 });\n\n  const kubectlSrcPath = path.join(binDir, 'kubectl');\n  const kubectlDstPath = path.join(integrationDirectory, 'kubectl');\n\n  await fs.promises.symlink(kubectlSrcPath, kubectlDstPath);\n\n  const composeSrcPath = path.join(dockerCLIPluginSource, 'docker-compose');\n  const composeDstPath = path.join(dockerCLIPluginDest, 'docker-compose');\n\n  await fs.promises.symlink(composeSrcPath, composeDstPath);\n}\n\nbeforeEach(async() => {\n  testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), TMPDIR_PREFIX));\n});\n\nafterEach(async() => {\n  if (testDir.includes(TMPDIR_PREFIX)) {\n    await fs.promises.rm(testDir, { recursive: true, force: true });\n  }\n});\n\ndescribeUnix('UnixIntegrationManager', () => {\n  let integrationDir: string;\n  let dockerCLIPluginDest: string;\n  let integrationManager: UnixIntegrationManager;\n\n  beforeEach(() => {\n    integrationDir = path.join(testDir, INTEGRATION_DIR_NAME);\n    dockerCLIPluginDest = path.join(testDir, 'dockerCliPluginDir');\n    integrationManager = new UnixIntegrationManager({\n      binDir, integrationDir, dockerCLIPluginSource, dockerCLIPluginDest,\n    });\n  });\n\n  describe('enforce', () => {\n    test('should create dirs and symlinks properly', async() => {\n      await integrationManager.enforce();\n      for (const name of await fs.promises.readdir(binDir)) {\n        const integrationPath = path.join(integrationDir, name);\n        const expectedValue = path.join(binDir, name);\n\n        await expect(fs.promises.readlink(integrationPath, 'utf8')).resolves.toEqual(expectedValue);\n      }\n      for (const name of await fs.promises.readdir(dockerCLIPluginSource)) {\n        const binPath = path.join(integrationDir, name);\n        const pluginPath = path.join(dockerCLIPluginDest, name);\n        const expectedValue = path.join(dockerCLIPluginSource, name);\n\n        await expect(fs.promises.readlink(pluginPath, 'utf8')).resolves.toEqual(binPath);\n        await expect(fs.promises.readlink(binPath, 'utf8')).resolves.toEqual(expectedValue);\n      }\n    });\n\n    test('should not overwrite an existing docker CLI plugin that is a regular file', async() => {\n      // create existing plugin\n      const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const existingPluginContents = 'meaningless contents';\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.writeFile(existingPluginPath, existingPluginContents);\n\n      await integrationManager.enforce();\n\n      const newContents = await fs.promises.readFile(existingPluginPath, 'utf8');\n\n      expect(newContents).toEqual(existingPluginContents);\n    });\n\n    test('should update an existing docker CLI plugin that is a dangling symlink', async() => {\n      const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const nonExistentPath = '/somepaththatshouldnevereverexist';\n      const expectedTarget = path.join(integrationDir, 'docker-compose');\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.symlink(nonExistentPath, existingPluginPath);\n\n      await integrationManager.enforce();\n\n      const newTarget = await fs.promises.readlink(existingPluginPath);\n\n      expect(newTarget).toEqual(expectedTarget);\n    });\n\n    test('should update an existing docker CLI plugin whose target is resources directory', async() => {\n      const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const sourceDir = path.join(dockerCLIPluginSource, 'docker-compose');\n      const expectedTarget = path.join(integrationDir, 'docker-compose');\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.symlink(sourceDir, existingPluginPath);\n\n      await integrationManager.enforce();\n\n      const newTarget = await fs.promises.readlink(existingPluginPath);\n\n      expect(newTarget).toEqual(expectedTarget);\n    });\n\n    test('should be idempotent', async() => {\n      await integrationManager.enforce();\n      const intDirAfterFirstCall = await fs.promises.readdir(integrationDir);\n      const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCLIPluginDest);\n\n      await integrationManager.enforce();\n      const intDirAfterSecondCall = await fs.promises.readdir(integrationDir);\n      const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCLIPluginDest);\n\n      expect(intDirAfterFirstCall).toEqual(intDirAfterSecondCall);\n      expect(dockerCliDirAfterFirstCall).toEqual(dockerCliDirAfterSecondCall);\n    });\n\n    test('should convert a regular file in integration directory to correct symlink', async() => {\n      const integrationPath = path.join(integrationDir, 'kubectl');\n      const expectedTarget = path.join(binDir, 'kubectl');\n\n      await fs.promises.mkdir(integrationDir);\n      await fs.promises.writeFile(integrationPath, 'contents', 'utf-8');\n      await integrationManager.enforce();\n      await expect(fs.promises.readlink(integrationPath)).resolves.toEqual(expectedTarget);\n    });\n\n    test('should fix an incorrect symlink in integration directory', async() => {\n      const integrationPath = path.join(integrationDir, 'kubectl');\n      const originalTargetPath = path.join(testDir, 'kubectl');\n      const expectedTarget = path.join(binDir, 'kubectl');\n\n      await fs.promises.mkdir(integrationDir);\n      await fs.promises.writeFile(originalTargetPath, 'contents', 'utf-8');\n      await fs.promises.symlink(originalTargetPath, integrationPath);\n      await integrationManager.enforce();\n      await expect(fs.promises.readlink(integrationPath)).resolves.toEqual(expectedTarget);\n    });\n\n    test('should fix a dangling symlink in integration directory', async() => {\n      const integrationPath = path.join(integrationDir, 'kubectl');\n      const originalTargetPath = path.join(testDir, 'kubectl');\n      const expectedTarget = path.join(binDir, 'kubectl');\n\n      await fs.promises.mkdir(integrationDir);\n      await fs.promises.symlink(originalTargetPath, integrationPath);\n      await integrationManager.enforce();\n      await expect(fs.promises.readlink(integrationPath)).resolves.toEqual(expectedTarget);\n    });\n\n    test('should remove a file that does not have a counterpart in resources directory', async() => {\n      const integrationPath = path.join(integrationDir, 'nameThatShouldNeverBeInResourcesDir');\n\n      await fs.promises.mkdir(integrationDir);\n      await fs.promises.writeFile(integrationPath, 'content', 'utf-8');\n      await integrationManager.enforce();\n      await expect(fs.promises.readFile(integrationPath, 'utf-8')).rejects.toThrow('ENOENT');\n    });\n\n    test('should not modify a docker plugin that does not have a counterpart in resources directory', async() => {\n      const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'nameThatShouldNeverBeInResourcesDir');\n      const content = 'content';\n\n      await fs.promises.mkdir(dockerCLIPluginDest);\n      await fs.promises.writeFile(dockerCliPluginPath, content, 'utf-8');\n      await integrationManager.enforce();\n      await expect(fs.promises.readFile(dockerCliPluginPath, 'utf-8')).resolves.toEqual(content);\n    });\n  });\n\n  describe('remove', () => {\n    test('should remove symlinks and dirs properly', async() => {\n      await createTestSymlinks(integrationDir, dockerCLIPluginDest);\n\n      await integrationManager.remove();\n      await expect(fs.promises.readdir(integrationDir)).rejects.toThrow();\n      await expect(fs.promises.readdir(dockerCLIPluginDest)).resolves.toEqual([]);\n    });\n\n    test('should not remove an existing docker CLI plugin that is a regular file', async() => {\n      // create existing plugin\n      const existingPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const existingPluginContents = 'meaningless contents';\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.writeFile(existingPluginPath, existingPluginContents);\n\n      await integrationManager.remove();\n\n      const newContents = await fs.promises.readFile(existingPluginPath, 'utf8');\n\n      expect(newContents).toEqual(existingPluginContents);\n    });\n\n    test('should not remove an existing docker CLI plugin that is not an expected symlink', async() => {\n      const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const existingTarget = path.join(testDir, 'docker-compose');\n      const existingPluginContents = 'meaningless contents';\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.writeFile(existingTarget, existingPluginContents);\n      await fs.promises.symlink(existingTarget, dockerCliPluginPath);\n\n      await integrationManager.remove();\n\n      await expect(fs.promises.readlink(dockerCliPluginPath, 'utf8')).resolves.toEqual(existingTarget);\n    });\n\n    test('should remove an existing docker CLI plugin that is a dangling symlink', async() => {\n      const dockerCliPluginPath = path.join(dockerCLIPluginDest, 'docker-compose');\n      const existingTarget = path.join(testDir, 'docker-compose');\n\n      await fs.promises.mkdir(dockerCLIPluginDest, { mode: 0o755 });\n      await fs.promises.symlink(existingTarget, dockerCliPluginPath);\n\n      await integrationManager.remove();\n\n      await expect(fs.promises.readlink(dockerCliPluginPath, 'utf8')).rejects.toThrow('ENOENT');\n    });\n\n    test('should be idempotent', async() => {\n      await integrationManager.remove();\n      const testDirAfterFirstCall = await fs.promises.readdir(testDir);\n\n      expect(testDirAfterFirstCall).not.toContain(INTEGRATION_DIR_NAME);\n      const dockerCliDirAfterFirstCall = await fs.promises.readdir(dockerCLIPluginDest);\n\n      expect(dockerCliDirAfterFirstCall).toEqual([]);\n\n      await integrationManager.remove();\n      const testDirAfterSecondCall = await fs.promises.readdir(testDir);\n\n      expect(testDirAfterSecondCall).not.toContain(INTEGRATION_DIR_NAME);\n      const dockerCliDirAfterSecondCall = await fs.promises.readdir(dockerCLIPluginDest);\n\n      expect(dockerCliDirAfterFirstCall).toEqual(dockerCliDirAfterSecondCall);\n    });\n  });\n\n  describe('removeSymlinksOnly', () => {\n    test('should remove symlinks but not integration directory', async() => {\n      await createTestSymlinks(integrationDir, dockerCLIPluginDest);\n\n      await integrationManager.removeSymlinksOnly();\n      await expect(fs.promises.readdir(integrationDir)).resolves.toEqual([]);\n      await expect(fs.promises.readdir(dockerCLIPluginDest)).resolves.toEqual([]);\n    });\n  });\n\n  describe('weOwnDockerCliFile', () => {\n    let dstPath: string;\n    const credHelper = 'docker-credential-pass';\n\n    beforeEach(async() => {\n      await fs.promises.mkdir(dockerCLIPluginDest, { recursive: true, mode: 0o755 });\n      dstPath = path.join(dockerCLIPluginDest, credHelper);\n    });\n\n    test(\"should return true when the symlink's target matches the integration directory\", async() => {\n      const resourcesPath = path.join(dockerCLIPluginSource, credHelper);\n      const srcPath = path.join(integrationDir, credHelper);\n\n      // create symlink in integration dir; otherwise, it is dangling\n      await fs.promises.mkdir(integrationDir);\n      await fs.promises.symlink(resourcesPath, srcPath);\n\n      await fs.promises.symlink(srcPath, dstPath);\n      expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(true);\n    });\n\n    test(\"should return true when the symlink's target matches the resources directory\", async() => {\n      const srcPath = path.join(dockerCLIPluginSource, credHelper);\n\n      await fs.promises.symlink(srcPath, dstPath);\n      expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(true);\n    });\n\n    test('should return true when the file is a dangling symlink', async() => {\n      const srcPath = path.join(testDir, 'testfilethatdoesntexist');\n\n      await fs.promises.symlink(srcPath, dstPath);\n      expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(true);\n    });\n\n    test(\"should return false when the symlink's target doesn't match the integration or resources directory\", async() => {\n      const srcPath = path.join(testDir, 'someothername');\n\n      await fs.promises.writeFile(srcPath, 'some content', 'utf-8');\n      await fs.promises.symlink(srcPath, dstPath);\n      expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(false);\n    });\n\n    test('should return false when the file is not a symlink', async() => {\n      const contents = 'this is a regular file for testing';\n\n      await fs.promises.writeFile(dstPath, contents, 'utf-8');\n      expect(integrationManager['weOwnDockerCliFile'](dstPath)).resolves.toEqual(false);\n    });\n  });\n});\n\ndescribeUnix('ensureSymlink', () => {\n  const srcPath = path.join(dockerCLIPluginSource, 'kubectl');\n  let dstPath: string;\n\n  beforeEach(() => {\n    dstPath = path.join(testDir, 'kubectl');\n  });\n\n  test(\"should create the symlink if it doesn't exist\", async() => {\n    const dirContentsBefore = await fs.promises.readdir(testDir);\n\n    expect(dirContentsBefore).toEqual([]);\n\n    await ensureSymlink(srcPath, dstPath);\n\n    return fs.promises.readlink(dstPath);\n  });\n\n  test('should do nothing if file is correct symlink', async() => {\n    await fs.promises.symlink(srcPath, dstPath);\n    await ensureSymlink(srcPath, dstPath);\n\n    const newTarget = await fs.promises.readlink(dstPath);\n\n    expect(newTarget).toEqual(srcPath);\n  });\n\n  test('should correct a symlink with an incorrect target', async() => {\n    // create a file to target in the bad symlink\n    const badSrcDir = path.join(testDir, 'resources', os.platform(), 'bin');\n    const badSrcPath = path.join(badSrcDir, 'fakeKubectl');\n\n    await fs.promises.mkdir(badSrcDir, { recursive: true, mode: 0o755 });\n    await fs.promises.writeFile(badSrcPath, 'contents');\n    await fs.promises.symlink(badSrcPath, dstPath);\n    await ensureSymlink(srcPath, dstPath);\n\n    const newTarget = await fs.promises.readlink(dstPath);\n\n    expect(newTarget).toEqual(srcPath);\n  });\n\n  test('should replace a regular file with a symlink', async() => {\n    // create the non-symlink dst file\n    const contents = 'these contents should be replaced';\n\n    await fs.promises.writeFile(dstPath, contents);\n    await ensureSymlink(srcPath, dstPath);\n\n    const newTarget = await fs.promises.readlink(dstPath);\n\n    expect(newTarget).toEqual(srcPath);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/__tests__/windowsIntegrationManager.spec.ts",
    "content": "/** @jest-environment node */\n\nimport { jest } from '@jest/globals';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nmockModules({ electron: undefined });\n\nconst { default: WindowsIntegrationManager, WSLDistro } = await import('@pkg/integrations/windowsIntegrationManager');\n\ndescribe('WindowsIntegrationManager', () => {\n  let integrationManager: InstanceType<typeof WindowsIntegrationManager>;\n  let captureCommandMock: jest.Spied<InstanceType<typeof WindowsIntegrationManager>['captureCommand']>;\n  const wslOutput = `  NAME                    STATE           VERSION\n* Ubuntu                  Stopped         2\n  OtherDistro             Running         1\n  rancher-desktop-data    Stopped         2\n  rancher-desktop         Stopped         2`;\n\n  beforeEach(() => {\n    integrationManager = new WindowsIntegrationManager();\n    captureCommandMock = jest.spyOn(integrationManager as any, 'captureCommand')\n      .mockResolvedValue(wslOutput);\n  });\n\n  afterEach(() => {\n    captureCommandMock.mockReset();\n  });\n\n  describe('nonBlacklistedDistros', () => {\n    it('should parse output of wsl.exe --list --verbose correctly', async() => {\n      const distros = await integrationManager['nonBlacklistedDistros'];\n\n      distros.sort((a, b) => a.name.localeCompare(b.name, 'en'));\n      expect(distros).toMatchObject([\n        { name: 'OtherDistro', version: 1 },\n        { name: 'Ubuntu', version: 2 },\n      ]);\n    });\n\n    it('should not output blacklisted distros', async() => {\n      const distros = await integrationManager['nonBlacklistedDistros'];\n\n      expect(distros).toHaveLength(2);\n      for (const distro of distros) {\n        expect(['rancher-desktop-data', 'rancher-desktop']).not.toContain(distro.name);\n      }\n    });\n  });\n\n  describe('supportedDistros', () => {\n    it('should only output v2 distros', async() => {\n      const distros = await integrationManager['supportedDistros'];\n\n      expect(distros).toHaveLength(1);\n      expect(distros).not.toEqual(expect.arrayContaining([expect.objectContaining({ version: 1 })]));\n    });\n  });\n\n  describe('getStateForIntegration', () => {\n    it('should return a string explaining that only v2 distros are supported', async() => {\n      const distro = new WSLDistro('Ubuntu', 1);\n      const state = await integrationManager['getStateForIntegration'](distro);\n\n      expect(state).toEqual(\n        expect.stringMatching(`Rancher Desktop can only integrate with v2 WSL distributions.*`),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/integrationManager.ts",
    "content": "import os from 'os';\nimport path from 'path';\n\nimport UnixIntegrationManager from '@pkg/integrations/unixIntegrationManager';\nimport WindowsIntegrationManager from '@pkg/integrations/windowsIntegrationManager';\nimport paths from '@pkg/utils/paths';\n\n/**\n * An IntegrationManager is a class that manages integrations for a particular\n * platform. An \"integration\" is a tool that is used with Rancher Desktop, such\n * as kubectl, nerdctl, docker CLI plugins and so on. These tools are included\n * in the Rancher Desktop installation, but extra steps are usually needed to\n * make them available to the user. Carrying out these steps, as well as reversing\n * them when desired, is what an IntegrationManager is for.\n */\nexport interface IntegrationManager {\n  /** Idempotent. Realize any changes to the system. */\n  enforce(): Promise<void>\n  /**\n   * Idempotent. Remove any changes from the system that the IntegrationManager\n   * may have made.\n   */\n  remove(): Promise<void>\n  /**\n   * A leaky part of this abstraction. Was introduced for the case where RD is\n   * running as an AppImage on Linux. In this case, we need to remove and remake\n   * integration symlinks on every quit-start cycle since they are mounted at a\n   * different location every run. Idempotent.\n   */\n  removeSymlinksOnly(): Promise<void>\n\n  /**\n   * On Windows only, list the integrations available; returns a mapping of WSL\n   * distribution to:\n   * - true: integration is enabled\n   * - false: integration is disabled\n   * - (string): error with given details\n   * On non-Windows platforms, returns null.\n   */\n  listIntegrations(): Promise<Record<string, boolean | string> | null>;\n}\n\nexport function getIntegrationManager(): IntegrationManager {\n  const platform = os.platform();\n  const binDir = path.join(paths.resources, platform, 'bin');\n  const dockerCLIPluginSource = path.join(paths.resources, platform, 'docker-cli-plugins');\n  const dockerCLIPluginDest = path.join(os.homedir(), '.docker', 'cli-plugins');\n\n  switch (platform) {\n  case 'linux':\n  case 'darwin':\n    return new UnixIntegrationManager({\n      binDir, integrationDir: paths.integration, dockerCLIPluginSource, dockerCLIPluginDest,\n    });\n  case 'win32':\n    return WindowsIntegrationManager.getInstance();\n  default:\n    throw new Error(`OS ${ platform } is not supported`);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/manageLinesInFile.ts",
    "content": "import fs from 'fs';\n\nimport isEqual from 'lodash/isEqual.js';\n\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging['path-management'];\n\nexport const START_LINE = '### MANAGED BY RANCHER DESKTOP START (DO NOT EDIT)';\nexport const END_LINE = '### MANAGED BY RANCHER DESKTOP END (DO NOT EDIT)';\nconst DEFAULT_FILE_MODE = 0o644;\n\n/**\n * `newErrorWithPath` returns a dynamically constructed subclass of `Error` that\n * has a constructor that constructs a message using the `messageTemplate`\n * function, and also sets any inputs to the function as properties on the\n * resulting object.\n * @param messageTemplate A function used to generate an error message, based on\n *                        any arguments passed in as properties of an object.\n * @returns A subclass of Error.\n */\nfunction newErrorWithPath<T extends Record<string, any>>(messageTemplate: (input: T) => string) {\n  const result = class extends Error {\n    constructor(input: T, options?: ErrorOptions) {\n      super(messageTemplate(input), options);\n      Object.assign(this, input);\n    }\n  };\n\n  return result as unknown as new(...args: ConstructorParameters<typeof result>) => (InstanceType<typeof result> & T);\n}\n\n/**\n * `ErrorDeterminingExtendedAttributes` signifies that we failed to determine if\n * the given path contains extended attributes; to be safe, we are not managing\n * this file.\n */\nexport const ErrorDeterminingExtendedAttributes =\n  newErrorWithPath(({ path }: { path: string }) => `Failed to determine if \\`${ path }\\` contains extended attributes`);\n/**\n * `ErrorCopyingExtendedAttributes occurs if we failed to copy extended\n * attributes while managing a file.\n */\nexport const ErrorCopyingExtendedAttributes =\n  newErrorWithPath(({ path }: { path: string }) => `Failed to copy extended attributes while managing \\`${ path }\\``);\n/**\n * `ErrorNotRegularFile` signifies that we were unable to process a file because\n * it is not a regular file (e.g. a named pipe or a device).\n */\nexport const ErrorNotRegularFile =\n  newErrorWithPath(({ path }: { path: string }) => `Refusing to manage \\`${ path }\\` which is neither a regular file nor a symbolic link`);\n/**\n * `ErrorWritingFile` signifies that we attempted to process a file but writing\n * to it resulted in unexpected contents.\n */\nexport const ErrorWritingFile =\n  newErrorWithPath(({ path, backupPath }: { path: string, backupPath: string }) => `Error writing to \\`${ path }\\`: written contents are unexpected; see backup in \\`${ backupPath }\\``);\n\n/**\n * Inserts/removes fenced lines into/from a file. Idempotent.\n * @param path The path to the file to work on.\n * @param desiredManagedLines The lines to insert into the file.\n * @param desiredPresent Whether the lines should be present.\n * @throws If the file could not be managed; for example, if it has extended\n *         attributes, is not a regular file, or a backup exists.\n */\nexport default async function manageLinesInFile(path: string, desiredManagedLines: string[], desiredPresent: boolean): Promise<void> {\n  const desired = getDesiredLines(desiredManagedLines, desiredPresent);\n  let fileStats: fs.Stats;\n\n  try {\n    fileStats = await fs.promises.lstat(path);\n  } catch (ex: any) {\n    if (ex && 'code' in ex && ex.code === 'ENOENT') {\n      // File does not exist.\n      const content = computeTargetContents('', desired);\n\n      if (content) {\n        await fs.promises.writeFile(path, content, { mode: DEFAULT_FILE_MODE });\n      }\n\n      return;\n    } else {\n      throw ex;\n    }\n  }\n\n  if (fileStats.isFile()) {\n    const tempName = `${ path }.rd-temp`;\n\n    await fs.promises.copyFile(path, tempName, fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);\n\n    try {\n      const currentContents = await fs.promises.readFile(tempName, 'utf-8');\n      const targetContents = computeTargetContents(currentContents, desired);\n\n      if (targetContents === undefined) {\n        // No changes are needed\n        return;\n      }\n\n      if (targetContents === '') {\n        // The resulting file is empty; unlink it.\n        await fs.promises.unlink(path);\n\n        return;\n      }\n\n      await copyFileExtendedAttributes(path, tempName);\n      await fs.promises.writeFile(tempName, targetContents, 'utf-8');\n      await fs.promises.rename(tempName, path);\n    } finally {\n      try {\n        await fs.promises.unlink(tempName);\n      } catch {\n        // Ignore errors unlinking the temporary file; if everything went well,\n        // it no longer exists anyway.\n      }\n    }\n  } else if (fileStats.isSymbolicLink()) {\n    const backupPath = `${ path }.rd-backup~`;\n\n    await fs.promises.copyFile(path, backupPath, fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);\n\n    const currentContents = await fs.promises.readFile(backupPath, 'utf-8');\n    const targetContents = computeTargetContents(currentContents, desired);\n\n    if (targetContents === undefined) {\n      // No changes are needed; just remove the backup file again.\n      await fs.promises.unlink(backupPath);\n\n      return;\n    }\n    // Always write the file, even if the result will be empty.\n    await fs.promises.writeFile(path, targetContents, 'utf-8');\n\n    const actualContents = await fs.promises.readFile(path, 'utf-8');\n\n    if (!isEqual(targetContents, actualContents)) {\n      throw new ErrorWritingFile({ path, backupPath });\n    }\n    await fs.promises.unlink(backupPath);\n  } else {\n    // Target exists, and is neither a normal file nor a symbolic link.\n    // Return with an error.\n    throw new ErrorNotRegularFile({ path });\n  }\n}\n\n/**\n * Copies extended attributes from an existing file to a different file.  Both\n * files must already exist.\n */\nasync function copyFileExtendedAttributes(fromPath: string, toPath: string): Promise<void> {\n  const { listAttributes, getAttribute, removeAttribute, setAttribute } = await import('@napi-rs/xattr');\n  try {\n    for (const attr of await listAttributes(fromPath)) {\n      const value = await getAttribute(fromPath, attr);\n\n      if (value === null) {\n        await removeAttribute(toPath, attr);\n      } else {\n        await setAttribute(toPath, attr, value);\n      }\n    }\n  } catch (cause) {\n    if (process.env.NODE_ENV === 'test' && !(process.env.RD_TEST ?? '').includes('e2e')) {\n      // When running unit tests, assume they do not have extended attributes.\n      return;\n    }\n\n    if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'MODULE_NOT_FOUND') {\n      console.error(`Failed to import fs-xattr, cannot copy extended attributes from ${ fromPath }:`, cause);\n\n      throw new ErrorDeterminingExtendedAttributes({ path: fromPath }, { cause });\n    }\n    throw new ErrorCopyingExtendedAttributes({ path: fromPath }, { cause });\n  }\n}\n\n/**\n * Splits a file into three arrays containing the lines before the managed portion,\n * the lines in the managed portion and the lines after the managed portion.\n * @param lines An array where each element represents a line in a file.\n */\nfunction splitLinesByDelimiters(lines: string[]): [string[], string[], string[]] {\n  const startIndex = lines.indexOf(START_LINE);\n  const endIndex = lines.indexOf(END_LINE);\n\n  if (startIndex < 0 && endIndex < 0) {\n    return [lines, [], []];\n  } else if (startIndex < 0 || endIndex < 0) {\n    throw new Error('exactly one of the delimiter lines is not present');\n  } else if (startIndex >= endIndex) {\n    throw new Error('the delimiter lines are in the wrong order');\n  }\n\n  const before = lines.slice(0, startIndex);\n  const currentManagedLines = lines.slice(startIndex + 1, endIndex);\n  const after = lines.slice(endIndex + 1);\n\n  return [before, currentManagedLines, after];\n}\n\n/**\n * Calculate the desired content of the managed lines.\n * @param desiredManagedLines The lines to insert into the file.\n * @param desiredPresent Whether the lines should be present.\n * @returns The lines that should end up in the managed section of the final file.\n */\nfunction getDesiredLines(desiredManagedLines: string[], desiredPresent: boolean): string[] {\n  const desired = desiredPresent && desiredManagedLines.length > 0;\n\n  return desired ? [START_LINE, ...desiredManagedLines, END_LINE] : [];\n}\n\n/**\n * Given the current contents of the file, determine what the final file\n * contents should be.\n * @param currentContents The current contents of the file.\n * @param desired The desired content of the managed lines.\n * @returns The final content; if no changes are needed, `undefined` is returned.\n *          There will never be any leading empty lines,\n *          and there will always be exactly one trailing empty line.\n */\nfunction computeTargetContents(currentContents: string, desired: string[]): string | undefined {\n  const [before, current, after] = splitLinesByDelimiters(currentContents.split('\\n'));\n\n  if (isEqual(current, desired)) {\n    // No changes are needed\n    return undefined;\n  }\n\n  const lines = [...before, ...desired, ...after];\n\n  // Remove all leading empty lines.\n  while (lines.length > 0 && lines[0] === '') {\n    lines.shift();\n  }\n  // Remove all trailing empty lines.\n  while (lines.length > 0 && lines[lines.length - 1] === '') {\n    lines.pop();\n  }\n  // Add one trailing empty line to the end.\n  lines.push('');\n\n  return lines.join('\\n');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/pathManager.ts",
    "content": "// Definitions only\n\n/**\n * PathManager is the interface that anything that manages the\n *  PATH variable must implement.\n */\nexport interface PathManager {\n  /** The PathManagementStrategy that corresponds to the implementation. */\n  readonly strategy: PathManagementStrategy\n  /**\n   * Applies changes to the system. Should be idempotent, and should not throw\n   * any exceptions.\n   */\n  enforce(): Promise<void>\n  /**\n   * Removes any changes that the PathManager may have made. Should be\n   * idempotent, and should not throw any exceptions.\n   */\n  remove(): Promise<void>\n}\n\n/**\n * ManualPathManager is for when the user has chosen to manage\n * their PATH themselves. It does nothing.\n */\nexport class ManualPathManager implements PathManager {\n  readonly strategy = PathManagementStrategy.Manual;\n  async enforce(): Promise<void> {}\n  async remove(): Promise<void> {}\n}\n\nexport enum PathManagementStrategy {\n  Manual = 'manual',\n  RcFiles = 'rcfiles',\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/pathManagerImpl.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { Mutex } from 'async-mutex';\n\nimport manageLinesInFile from '@pkg/integrations/manageLinesInFile';\nimport { ManualPathManager, PathManagementStrategy, PathManager } from '@pkg/integrations/pathManager';\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst console = Logging['path-management'];\n\n/**\n * RcFilePathManager is for when the user wants Rancher Desktop to\n * make changes to their PATH by putting lines that change it in their\n * shell .rc files.\n */\nexport class RcFilePathManager implements PathManager {\n  readonly strategy = PathManagementStrategy.RcFiles;\n  private readonly posixMutex :Mutex;\n  private readonly cshMutex :  Mutex;\n  private readonly fishMutex : Mutex;\n\n  constructor() {\n    const platform = os.platform();\n\n    if (platform !== 'linux' && platform !== 'darwin') {\n      throw new Error(`Platform \"${ platform }\" is not supported by RcFilePathManager`);\n    }\n    this.posixMutex = new Mutex();\n    this.cshMutex = new Mutex();\n    this.fishMutex = new Mutex();\n  }\n\n  async enforce(): Promise<void> {\n    try {\n      await this.managePosix(true);\n      await this.manageCsh(true);\n      await this.manageFish(true);\n    } catch (error) {\n      console.error(error);\n    }\n  }\n\n  async remove(): Promise<void> {\n    try {\n      await this.managePosix(false);\n      await this.manageCsh(false);\n      await this.manageFish(false);\n    } catch (error) {\n      console.error(error);\n    }\n  }\n\n  /**\n   * Call manageFilesInLine, wrapped in calls to trigger diagnostics updates.\n   */\n  protected async manageLinesInFile(fileName: string, filePath: string, lines: string[], desiredPresent: boolean) {\n    try {\n      await manageLinesInFile(filePath, lines, desiredPresent);\n      mainEvents.emit('diagnostics-event', {\n        id: 'path-management', fileName, error: undefined,\n      });\n    } catch (error: any) {\n      mainEvents.emit('diagnostics-event', {\n        id: 'path-management', fileName, error,\n      });\n      throw error;\n    }\n  }\n\n  /**\n   * bash requires some special handling. This is because the files it reads\n   * on startup differ depending on whether it is a login shell or a\n   * non-login shell. We must cover both cases.\n   */\n  protected async managePosix(desiredPresent: boolean): Promise<void> {\n    await this.posixMutex.runExclusive(async() => {\n      const pathLine = `export PATH=\"${ paths.integration }:$PATH\"`;\n      // Note: order is important here.  Only the first one has the PATH added;\n      // all others have it removed.\n      const bashLoginShellFiles = [\n        '.bash_profile',\n        '.bash_login',\n        '.profile',\n      ];\n\n      // Handle files that pertain to bash login shells\n      if (desiredPresent) {\n        let linesAdded = false;\n\n        // Write the first file that exists, if any\n        for (const fileName of bashLoginShellFiles) {\n          const filePath = path.join(os.homedir(), fileName);\n\n          try {\n            await fs.promises.stat(filePath);\n          } catch (error: any) {\n            if (error.code === 'ENOENT') {\n              // If the file does not exist, it is not an error.\n              mainEvents.emit('diagnostics-event', {\n                id: 'path-management', fileName, error: undefined,\n              });\n              continue;\n            }\n            mainEvents.emit('diagnostics-event', {\n              id: 'path-management', fileName, error,\n            });\n            throw error;\n          }\n          await this.manageLinesInFile(fileName, filePath, [pathLine], !linesAdded);\n          linesAdded = true;\n        }\n\n        // If none of the files exist, write .bash_profile\n        if (!linesAdded) {\n          const fileName = bashLoginShellFiles[0];\n          const filePath = path.join(os.homedir(), fileName);\n\n          await this.manageLinesInFile(fileName, filePath, [pathLine], true);\n        }\n      } else {\n        // Ensure lines are not present in any of the files\n        await Promise.all(bashLoginShellFiles.map(async(fileName) => {\n          const filePath = path.join(os.homedir(), fileName);\n\n          await this.manageLinesInFile(fileName, filePath, [], false);\n        }));\n      }\n\n      // Handle other shells' rc files and .bashrc\n      await Promise.all(['.bashrc', '.zshrc'].map((fileName) => {\n        const rcPath = path.join(os.homedir(), fileName);\n\n        return this.manageLinesInFile(fileName, rcPath, [pathLine], desiredPresent);\n      }));\n\n      mainEvents.invoke('diagnostics-trigger', 'RD_BIN_IN_BASH_PATH');\n      mainEvents.invoke('diagnostics-trigger', 'RD_BIN_IN_ZSH_PATH');\n    });\n  }\n\n  protected async manageCsh(desiredPresent: boolean): Promise<void> {\n    await this.cshMutex.runExclusive(async() => {\n      const pathLine = `setenv PATH \"${ paths.integration }\"\\\\:\"$PATH\"`;\n\n      await Promise.all(['.cshrc', '.tcshrc'].map((fileName) => {\n        const rcPath = path.join(os.homedir(), fileName);\n\n        return this.manageLinesInFile(fileName, rcPath, [pathLine], desiredPresent);\n      }));\n    });\n  }\n\n  protected async manageFish(desiredPresent: boolean): Promise<void> {\n    await this.fishMutex.runExclusive(async() => {\n      const pathLine = `set --export --prepend PATH \"${ paths.integration }\"`;\n      const configHome = process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config');\n      const fileName = 'config.fish';\n      const fishConfigDir = path.join(configHome, 'fish');\n      const fishConfigPath = path.join(fishConfigDir, fileName);\n\n      await fs.promises.mkdir(fishConfigDir, { recursive: true, mode: 0o700 });\n      await this.manageLinesInFile(fileName, fishConfigPath, [pathLine], desiredPresent);\n    });\n  }\n}\n\n/**\n * Changes the path manager to match a PathManagementStrategy and realizes the\n * changes that the new path manager represents.\n */\nexport function getPathManagerFor(strategy: PathManagementStrategy): PathManager {\n  switch (strategy) {\n  case PathManagementStrategy.Manual:\n    return new ManualPathManager();\n  case PathManagementStrategy.RcFiles:\n    return new RcFilePathManager();\n  default:\n    throw new Error(`Invalid strategy \"${ strategy }\"`);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/unixIntegrationManager.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { IntegrationManager } from '@pkg/integrations/integrationManager';\nimport Logging from '@pkg/utils/logging';\n\ninterface UnixIntegrationManagerOptions {\n  /** Directory containing tools shipped with Rancher Desktop. */\n  binDir:                string;\n  /** Directory to place tools the user can use. */\n  integrationDir:        string;\n  /** Directory containing docker CLI plugins shipped with Rancher Desktop. */\n  dockerCLIPluginSource: string;\n  /** Directory to place docker CLI plugins for with the docker CLI. */\n  dockerCLIPluginDest:   string;\n}\n\nconst console = Logging.integrations;\n\n/**\n * Manages integrations for Unix-like operating systems. Integrations take\n * the form of symlinks from the Rancher Desktop installation to two separate\n * directories: the \"integrations directory\", which should be in the user's path\n * somehow, and the \"docker CLI plugins directory\", which is the directory that\n * docker looks in for CLI plugins.\n */\nexport default class UnixIntegrationManager implements IntegrationManager {\n  protected binDir:                string;\n  protected integrationDir:        string;\n  protected dockerCLIPluginSource: string;\n  protected dockerCLIPluginDest:   string;\n\n  constructor(options: UnixIntegrationManagerOptions) {\n    this.binDir = options.binDir;\n    this.integrationDir = options.integrationDir;\n    this.dockerCLIPluginSource = options.dockerCLIPluginSource;\n    this.dockerCLIPluginDest = options.dockerCLIPluginDest;\n  }\n\n  // Idempotently installs directories and symlinks onto the system.\n  async enforce(): Promise<void> {\n    await this.ensureIntegrationDir(true);\n    await this.ensureIntegrationSymlinks(true);\n    await this.ensureDockerCliSymlinks(true);\n  }\n\n  // Idempotently removes any trace of managed directories and symlinks from\n  // the system.\n  async remove(): Promise<void> {\n    await this.ensureDockerCliSymlinks(false);\n    await this.ensureIntegrationSymlinks(false);\n    await this.ensureIntegrationDir(false);\n  }\n\n  // Idempotently removes any symlinks from the system. Does not remove\n  // directories. Intended for AppImages, where any symlinks to the installation\n  // are invalidated each time the application exits (the application directory\n  // is a filesystem image that is mounted in /tmp for each run).\n  async removeSymlinksOnly(): Promise<void> {\n    await this.ensureDockerCliSymlinks(false);\n    await this.ensureIntegrationSymlinks(false);\n  }\n\n  protected async ensureIntegrationDir(desiredPresent: boolean): Promise<void> {\n    if (desiredPresent) {\n      await fs.promises.mkdir(this.integrationDir, { recursive: true, mode: 0o755 });\n    } else {\n      await fs.promises.rm(this.integrationDir, { force: true, recursive: true });\n    }\n  }\n\n  /**\n   * Set up the symbolic links in the integration directory.  This will include\n   * both files from `binDir` as well as `dockerCLIPluginSource`; this is needed\n   * in case users try to run `docker-compose` instead of `docker compose`.\n   */\n  protected async ensureIntegrationSymlinks(desiredPresent: boolean): Promise<void> {\n    const RDIntegration = 'rancher-desktop';\n    const sourceDirs = [this.binDir, this.dockerCLIPluginSource];\n    const validIntegrations = Object.fromEntries((await Promise.all(sourceDirs.map(async(d) => {\n      return (await fs.promises.readdir(d)).map(f => [f, d] as const);\n    }))).flat(1));\n    let currentIntegrationNames: string[] = [];\n\n    // integration directory may or may not be present; handle error if not\n    try {\n      currentIntegrationNames = await fs.promises.readdir(this.integrationDir);\n      currentIntegrationNames = currentIntegrationNames.filter(v => v !== RDIntegration);\n    } catch (error: any) {\n      if (error.code !== 'ENOENT') {\n        throw error;\n      }\n    }\n\n    // remove current integrations that are not valid\n    await Promise.all(currentIntegrationNames.map(async(name) => {\n      if (!(name in validIntegrations)) {\n        await fs.promises.rm(path.join(this.integrationDir, name), { force: true });\n      }\n    }));\n\n    // create or remove the integrations\n    for (const [name, dir] of Object.entries(validIntegrations)) {\n      const resourcesPath = path.join(dir, name);\n      const integrationPath = path.join(this.integrationDir, name);\n\n      if (desiredPresent) {\n        await ensureSymlink(resourcesPath, integrationPath);\n      } else {\n        await fs.promises.rm(integrationPath, { force: true });\n      }\n    }\n\n    // manage the special rancher-desktop integration; this symlink\n    // exists so that rdctl can find the path to the AppImage\n    // that Rancher Desktop is running from\n    const rancherDesktopPath = path.join(this.integrationDir, RDIntegration);\n    const appImagePath = process.env['APPIMAGE'];\n\n    if (desiredPresent && appImagePath) {\n      await ensureSymlink(appImagePath, rancherDesktopPath);\n    } else {\n      await fs.promises.rm(rancherDesktopPath, { force: true });\n    }\n  }\n\n  protected async ensureDockerCliSymlinks(desiredPresent: boolean): Promise<void> {\n    // ensure the docker plugin path exists\n    await fs.promises.mkdir(this.dockerCLIPluginDest, { recursive: true, mode: 0o755 });\n\n    // get a list of docker plugins\n    const pluginNames = await fs.promises.readdir(this.dockerCLIPluginSource);\n\n    // create or remove the plugin links\n    for (const name of pluginNames) {\n      // We create symlinks to the integration directory instead of the file\n      // directly, to avoid factory reset having to deal with it.\n      const sourcePath = path.join(this.integrationDir, name);\n      const destPath = path.join(this.dockerCLIPluginDest, name);\n\n      if (!await this.weOwnDockerCliFile(destPath)) {\n        console.debug(`Skipping ${ destPath } - we don't own it`);\n        continue;\n      }\n\n      console.debug(`Will update ${ destPath }`);\n\n      if (desiredPresent) {\n        await ensureSymlink(sourcePath, destPath);\n      } else {\n        await fs.promises.rm(destPath, { force: true });\n      }\n    }\n  }\n\n  listIntegrations(): Promise<Record<string, boolean | string> | null> {\n    return Promise.resolve(null);\n  }\n\n  // Tells the caller whether Rancher Desktop is allowed to modify/remove\n  // a file in the docker CLI plugins directory.\n  protected async weOwnDockerCliFile(filePath: string): Promise<boolean> {\n    let linkedTo: string;\n\n    try {\n      linkedTo = await fs.promises.readlink(filePath);\n    } catch (error: any) {\n      if (error.code === 'ENOENT') {\n        // symlink doesn't exist, so create it\n        console.debug(`Symlink ${ filePath } does not exist, will create.`);\n\n        return true;\n      } else if (error.code === 'EINVAL') {\n        // not a symlink\n        console.debug(`${ filePath } is not a symlink, will ignore.`);\n\n        return false;\n      }\n      throw error;\n    }\n\n    try {\n      await fs.promises.stat(linkedTo);\n    } catch (error: any) {\n      if (error.code === 'ENOENT') {\n        // symlink is dangling\n        console.debug(`Symlink ${ filePath } links to dangling ${ linkedTo }, will replace.`);\n\n        return true;\n      }\n    }\n\n    if (path.dirname(linkedTo).endsWith(this.integrationDir)) {\n      console.debug(`Symlink ${ filePath } links to ${ linkedTo } which is in ${ this.integrationDir }, will replace`);\n\n      return true;\n    }\n\n    if (path.dirname(linkedTo).endsWith(path.join('resources', os.platform(), 'docker-cli-plugins'))) {\n      console.debug(`Symlink ${ filePath } links to ${ linkedTo }, will replace`);\n\n      return true;\n    }\n\n    console.debug(`Symlink ${ filePath } links to unknown path ${ linkedTo }, will ignore.`);\n\n    return false;\n  }\n}\n\n// Ensures that the file/symlink at dstPath\n// a) is a symlink\n// b) has a target path of srcPath\nexport async function ensureSymlink(srcPath: string, dstPath: string): Promise<void> {\n  let linkedTo: string;\n\n  try {\n    linkedTo = await fs.promises.readlink(dstPath);\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      // symlink doesn't exist, so create it\n      await fs.promises.symlink(srcPath, dstPath);\n\n      return;\n    } else if (error.code === 'EINVAL') {\n      // not a symlink; remove and replace with symlink\n      await fs.promises.rm(dstPath, { force: true });\n      await fs.promises.symlink(srcPath, dstPath);\n\n      return;\n    }\n    throw error;\n  }\n\n  if (linkedTo !== srcPath) {\n    console.debug(`Replacing symlinks at ${ dstPath } from ${ linkedTo } to ${ srcPath }`);\n    await fs.promises.unlink(dstPath);\n    await fs.promises.symlink(srcPath, dstPath);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/integrations/windowsIntegrationManager.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport semver from 'semver';\n\nimport DEPENDENCY_VERSIONS from '@pkg/assets/dependencies.yaml';\nimport K3sHelper from '@pkg/backend/k3sHelper';\nimport { State } from '@pkg/backend/k8s';\nimport { Settings, ContainerEngine } from '@pkg/config/settings';\nimport { runInDebugMode } from '@pkg/config/settingsImpl';\nimport type { IntegrationManager } from '@pkg/integrations/integrationManager';\nimport mainEvents from '@pkg/main/mainEvents';\nimport BackgroundProcess from '@pkg/utils/backgroundProcess';\nimport { spawn, spawnFile } from '@pkg/utils/childProcess';\nimport clone from '@pkg/utils/clone';\nimport Latch from '@pkg/utils/latch';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport { defined, RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst console = Logging.integrations;\n\n/**\n * A list of distributions in which we should never attempt to integrate with.\n */\nconst DISTRO_BLACKLIST = [\n  'rancher-desktop', // That's ourselves\n  'rancher-desktop-data', // Another internal distro\n  'docker-desktop', // Not meant for interactive use\n  'docker-desktop-data', // Not meant for interactive use\n  'wsl-vpnkit', // Our executable does not run\n];\n\n/**\n * Represents a WSL distro, as output by `wsl.exe --list --verbose`.\n */\nexport class WSLDistro {\n  name:    string;\n  version: number;\n\n  constructor(name: string, version: number) {\n    this.name = name;\n    if (![1, 2].includes(version)) {\n      throw new Error(`version \"${ version }\" is not recognized by Rancher Desktop`);\n    }\n    this.version = version;\n  }\n}\n\nenum SyncStateKey {\n  /** No sync is ongoing. */\n  IDLE,\n  /** A sync is running, but there is no queued sync. */\n  ACTIVE,\n  /** A sync is running, there is also a queued sync that will happen after. */\n  QUEUED,\n}\n\ntype SyncState =\n  { state: SyncStateKey.IDLE } |\n  /** The `active` promise will be resolved once the current sync is complete. */\n  { state: SyncStateKey.ACTIVE, active: ReturnType<typeof Latch> } |\n  /** The `queued` promise will be resolved after the current sync +1 is complete. */\n  { state: SyncStateKey.QUEUED, active: ReturnType<typeof Latch>, queued: ReturnType<typeof Latch> };\n\n/**\n * DiagnosticKey limits the `key` argument of the diagnostic events.\n */\ntype DiagnosticKey =\n  'docker-plugins' |\n  'docker-socket' |\n  'kubeconfig' |\n  'spin-cli' |\n  never;\n\n/**\n * WindowsIntegrationManager manages various integrations on Windows, for both\n * the Win32 host, as well as for each (foreign) WSL distribution.\n * This includes:\n * - Docker socket forwarding.\n * - Kubeconfig.\n * - docker CLI plugin executables (WSL distributions only).\n */\nexport default class WindowsIntegrationManager implements IntegrationManager {\n  /** A snapshot of the application-wide settings. */\n  protected settings: RecursivePartial<Settings> = {};\n\n  /** Background processes for docker socket forwarding, per WSL distribution. */\n  protected distroSocketProxyProcesses: Record<string, BackgroundProcess> = {};\n\n  /** Background process for docker socket forwarding to the Windows host. */\n  protected windowsSocketProxyProcess: BackgroundProcess;\n\n  /** Whether integrations as a whole are enabled. */\n  protected enforcing = false;\n\n  protected syncState: SyncState = { state: SyncStateKey.IDLE };\n\n  /** Whether the backend is in a state where the processes should run. */\n  protected backendReady = false;\n\n  /** Set when we're about to quit. */\n  protected quitting = false;\n\n  /** Extra debugging arguments for wsl-helper. */\n  protected wslHelperDebugArgs: string[] = [];\n\n  /** Singleton instance. */\n  private static instance: WindowsIntegrationManager;\n\n  constructor() {\n    mainEvents.on('settings-update', (settings) => {\n      this.wslHelperDebugArgs = runInDebugMode(settings.application.debug) ? ['--verbose'] : [];\n      this.settings = clone(settings);\n      this.sync();\n    });\n    mainEvents.on('k8s-check-state', (mgr) => {\n      this.backendReady = [State.STARTED, State.STARTING, State.DISABLED].includes(mgr.state);\n      this.sync();\n    });\n    mainEvents.handle('shutdown-integrations', async() => {\n      this.quitting = true;\n      await Promise.all(Object.values(this.distroSocketProxyProcesses).map(p => p.stop()));\n    });\n    this.windowsSocketProxyProcess = new BackgroundProcess(\n      'Win32 socket proxy',\n      {\n        spawn: async() => {\n          const stream = await Logging['wsl-helper'].fdStream;\n\n          console.debug('Spawning Windows docker proxy');\n\n          return spawn(\n            executable('wsl-helper'),\n            ['docker-proxy', 'serve', ...this.wslHelperDebugArgs], {\n              stdio:       ['ignore', stream, stream],\n              windowsHide: true,\n            });\n        },\n      });\n\n    // Trigger a settings-update.\n    mainEvents.emit('settings-write', {});\n  }\n\n  /** Static method to access the singleton instance. */\n  public static getInstance(): WindowsIntegrationManager {\n    WindowsIntegrationManager.instance ||= new WindowsIntegrationManager();\n\n    return WindowsIntegrationManager.instance;\n  }\n\n  async enforce(): Promise<void> {\n    this.enforcing = true;\n    await this.sync();\n  }\n\n  async remove(): Promise<void> {\n    this.enforcing = false;\n    await this.sync();\n  }\n\n  async sync(): Promise<void> {\n    const latch = Latch();\n\n    switch (this.syncState.state) {\n    case SyncStateKey.IDLE:\n      this.syncState = { state: SyncStateKey.ACTIVE, active: latch };\n      break;\n    case SyncStateKey.ACTIVE: {\n      // There is a sync already active; wait for it, then do the re-sync.\n      const { active } = this.syncState;\n\n      this.syncState = {\n        state: SyncStateKey.QUEUED, active, queued: latch,\n      };\n      console.debug('Waiting for previous sync to finish before starting new sync.');\n      await active;\n      // Continue with the rest of the function, in ACTIVE mode.\n      break;\n    }\n    case SyncStateKey.QUEUED:\n      // We already have a queued sync; just wait for that to complete.\n      console.debug('Merging duplicate sync with previous pending sync.');\n\n      return this.syncState.queued;\n    }\n    try {\n      let kubeconfigPath: string | undefined;\n\n      try {\n        kubeconfigPath = await K3sHelper.findKubeConfigToUpdate('rancher-desktop');\n        this.diagnostic({ key: 'kubeconfig' });\n      } catch (error) {\n        console.error(`Could not determine kubeconfig: ${ error } - Kubernetes configuration will not be updated.`);\n        this.diagnostic({ key: 'kubeconfig', error });\n        kubeconfigPath = undefined;\n      }\n\n      await Promise.all([\n        this.syncHostSocketProxy(),\n        this.syncHostDockerPluginConfig(),\n        ...(await this.supportedDistros).map(distro => this.syncDistro(distro.name, kubeconfigPath)),\n      ]);\n    } catch (ex) {\n      console.error(`Integration sync: Error: ${ ex }`);\n    } finally {\n      mainEvents.emit('integration-update', await this.listIntegrations());\n      // TypeScript is being too smart and thinking we can only be ACTIVE here;\n      // but that may be set from concurrent calls to sync().\n      const currentState: SyncState = this.syncState as any;\n\n      switch (currentState.state) {\n      case SyncStateKey.IDLE:\n        // This should never be reached\n        break;\n      case SyncStateKey.ACTIVE:\n        this.syncState = { state: SyncStateKey.IDLE };\n        break;\n      case SyncStateKey.QUEUED:\n        this.syncState = { state: SyncStateKey.ACTIVE, active: currentState.queued };\n        // The sync() that set the state to QUEUED will continue, and eventually\n        // set the state back to IDLE.\n      }\n      latch.resolve();\n    }\n  }\n\n  async syncDistro(distro: string, kubeconfigPath?: string): Promise<void> {\n    let state = this.settings.WSL?.integrations?.[distro] === true;\n\n    console.debug(`Integration sync: ${ distro } -> ${ state }`);\n    try {\n      await Promise.all([\n        this.syncDistroSocketProxy(distro, state),\n        this.syncDistroDockerPlugins(distro, state),\n        this.syncDistroKubeconfig(distro, kubeconfigPath, state),\n        this.syncDistroSpinCLI(distro, state),\n      ]);\n    } catch (ex) {\n      console.error(`Failed to sync integration for ${ distro }: ${ ex }`);\n      mainEvents.emit('settings-write', { WSL: { integrations: { [distro]: false } } });\n      state = false;\n    } finally {\n      await this.markIntegration(distro, state);\n    }\n  }\n\n  /**\n   * Helper function to trigger a diagnostic report.  If a diagnostic should be\n   * cleared, call this with the error unset.\n   */\n  protected diagnostic(input: { key: DiagnosticKey, distro?: string, error?: unknown }) {\n    const error = input.error instanceof Error ? input.error : input.error ? new Error(`${ input.error }`) : undefined;\n\n    mainEvents.emit('diagnostics-event', {\n      id:     'integrations-windows',\n      key:    input.key,\n      distro: input.distro,\n      error,\n    });\n  }\n\n  #wslExe = '';\n  /**\n   * The path to the wsl.exe executable.\n   *\n   * @note This is memoized.\n   */\n  protected get wslExe(): Promise<string> {\n    if (this.#wslExe) {\n      return Promise.resolve(this.#wslExe);\n    }\n\n    if (process.env.RD_TEST_WSL_EXE) {\n      // Running under test; use the alternate executable.\n      return Promise.resolve(process.env.RD_TEST_WSL_EXE);\n    }\n\n    const wslExe = path.join(process.env.SystemRoot ?? '', 'system32', 'wsl.exe');\n\n    return new Promise((resolve, reject) => {\n      fs.promises.access(wslExe, fs.constants.X_OK).then(() => {\n        this.#wslExe = wslExe;\n        resolve(wslExe);\n      }).catch(reject);\n    });\n  }\n\n  /**\n   * Execute the given command line in the given WSL distribution.\n   * Output is logged to the log file.\n   */\n  protected async execCommand(opts: { distro?: string, encoding?: BufferEncoding, root?: boolean, env?: Record<string, string> }, ...command: string[]):Promise<void> {\n    const logStream = opts.distro ? Logging[`wsl-helper.${ opts.distro }`] : console;\n    const args = [];\n\n    if (opts.distro) {\n      args.push('--distribution', opts.distro);\n      if (opts.root) {\n        args.push('--user', 'root');\n      }\n      args.push('--exec');\n    }\n    args.push(...command);\n    console.debug(`Running ${ await this.wslExe } ${ args.join(' ') }`);\n\n    await spawnFile(\n      await this.wslExe,\n      args,\n      {\n        env:         opts.env,\n        encoding:    opts.encoding ?? 'utf-8',\n        stdio:       ['ignore', logStream, logStream],\n        windowsHide: true,\n      },\n    );\n  }\n\n  /**\n   * Runs the `wsl.exe` command, either on the host or in a specified\n   * WSL distro. Returns whatever it prints to stdout, and logs whatever\n   * it prints to stderr.\n   */\n  protected async captureCommand(opts: { distro?: string, encoding?: BufferEncoding, env?: Record<string, string> }, ...command: string[]):Promise<string> {\n    const logStream = opts.distro ? Logging[`wsl-helper.${ opts.distro }`] : console;\n    const args = [];\n\n    if (opts.distro) {\n      args.push('--distribution', opts.distro, '--exec');\n    }\n    args.push(...command);\n    console.debug(`Running ${ await this.wslExe } ${ args.join(' ') }`);\n\n    const { stdout } = await spawnFile(\n      await this.wslExe,\n      args,\n      {\n        env:         opts.env,\n        encoding:    opts.encoding ?? 'utf-8',\n        stdio:       ['ignore', 'pipe', logStream],\n        windowsHide: true,\n      },\n    );\n\n    return stdout;\n  }\n\n  /**\n   * Return the Linux path to the WSL helper executable.\n   */\n  protected async getLinuxToolPath(distro: string, tool: string): Promise<string> {\n    // We need to get the Linux path to our helper executable; it is easier to\n    // just get WSL to do the transformation for us.\n    return (await this.captureCommand( { distro }, '/bin/wslpath', '-a', '-u', tool)).trim();\n  }\n\n  protected async syncHostSocketProxy(): Promise<void> {\n    const reason = this.dockerSocketProxyReason;\n\n    console.debug(`Syncing Win32 socket proxy: ${ reason ? `should not run (${ reason })` : 'should run' }`);\n    try {\n      if (!reason) {\n        this.windowsSocketProxyProcess.start();\n      } else {\n        await this.windowsSocketProxyProcess.stop();\n      }\n      this.diagnostic({ key: 'docker-socket' });\n    } catch (error) {\n      this.diagnostic({ key: 'docker-socket', error });\n    }\n  }\n\n  /**\n   * Get the reason that the docker socket should not run; if it _should_ run,\n   * returns undefined.\n   */\n  get dockerSocketProxyReason(): string | undefined {\n    if (this.quitting) {\n      return 'quitting Rancher Desktop';\n    } else if (!this.enforcing) {\n      return 'not enforcing';\n    } else if (!this.backendReady) {\n      return 'backend not ready';\n    } else if (this.settings.containerEngine?.name !== ContainerEngine.MOBY) {\n      return `unsupported container engine ${ this.settings.containerEngine?.name }`;\n    }\n  }\n\n  /**\n   * syncDistroSocketProxy ensures that the background process for the given\n   * distribution is started or stopped, as desired.\n   * @param distro The distribution to manage.\n   * @param state Whether integration is enabled for the given distro.\n   */\n  protected async syncDistroSocketProxy(distro: string, state: boolean) {\n    try {\n      const shouldRun = state && !this.dockerSocketProxyReason;\n\n      console.debug(`Syncing ${ distro } socket proxy: ${ shouldRun ? 'should' : 'should not' } run.`);\n      if (shouldRun) {\n        const linuxExecutable = await this.getLinuxToolPath(distro, executable('wsl-helper-linux'));\n        const logStream = Logging[`wsl-helper.${ distro }`];\n\n        this.distroSocketProxyProcesses[distro] ??= new BackgroundProcess(\n          `${ distro } socket proxy`,\n          {\n            spawn: async() => {\n              return spawn(await this.wslExe,\n                ['--distribution', distro, '--user', 'root', '--exec', linuxExecutable,\n                  'docker-proxy', 'serve', ...this.wslHelperDebugArgs],\n                {\n                  stdio:       ['ignore', await logStream.fdStream, await logStream.fdStream],\n                  windowsHide: true,\n                },\n              );\n            },\n            destroy: async(child) => {\n              child?.kill('SIGTERM');\n              // Ensure we kill the WSL-side process; sometimes things can get out\n              // of sync.\n              await this.execCommand({ distro, root: true },\n                linuxExecutable, 'docker-proxy', 'kill', ...this.wslHelperDebugArgs);\n            },\n          });\n        this.distroSocketProxyProcesses[distro].start();\n      } else {\n        await this.distroSocketProxyProcesses[distro]?.stop();\n        if (!(distro in (this.settings.WSL?.integrations ?? {}))) {\n          delete this.distroSocketProxyProcesses[distro];\n        }\n      }\n      this.diagnostic({ key: 'docker-socket', distro });\n    } catch (error) {\n      console.error(`Error syncing ${ distro } distro socket proxy: ${ error }`);\n      this.diagnostic({\n        key: 'docker-socket', distro, error,\n      });\n    }\n  }\n\n  protected async syncHostDockerPluginConfig() {\n    try {\n      const configPath = path.join(os.homedir(), '.docker', 'config.json');\n      let config: { cliPluginsExtraDirs?: string[] } = {};\n\n      try {\n        config = JSON.parse(await fs.promises.readFile(configPath, 'utf-8'));\n      } catch (error) {\n        if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {\n          // If the file does not exist, create it.\n        } else {\n          console.error(`Could not set up docker plugins:`, error);\n          this.diagnostic({ key: 'docker-plugins', error });\n\n          return;\n        }\n      }\n\n      // All of the docker plugins are in the `docker-cli-plugins` directory.\n      const binDir = path.join(paths.resources, process.platform, 'docker-cli-plugins');\n\n      if (config.cliPluginsExtraDirs?.includes(binDir)) {\n        // If it's already configured, no need to do so again.\n        return;\n      }\n\n      config.cliPluginsExtraDirs ??= [];\n      config.cliPluginsExtraDirs.push(binDir);\n\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify(config), 'utf-8');\n      this.diagnostic({ key: 'docker-plugins' });\n    } catch (error) {\n      this.diagnostic({ key: 'docker-plugins', error });\n    }\n  }\n\n  /**\n   * syncDistroDockerPlugins sets up docker CLI configuration in WSL distros to\n   * use the plugins shipped with Rancher Desktop.\n   * @param distro The distribution to update.\n   * @param state Whether the plugins should be enabled.\n   */\n  protected async syncDistroDockerPlugins(distro: string, state: boolean): Promise<void> {\n    try {\n      const binDir = await this.getLinuxToolPath(distro,\n        path.join(paths.resources, 'linux', 'bin'));\n      const srcPath = await this.getLinuxToolPath(distro,\n        path.join(paths.resources, 'linux', 'docker-cli-plugins'));\n      const wslHelper = await this.getLinuxToolPath(distro, executable('wsl-helper-linux'));\n      const args = ['wsl', 'integration', 'docker',\n        `--plugin-dir=${ srcPath }`, `--bin-dir=${ binDir }`, `--state=${ state }`];\n\n      if (this.settings.application?.debug) {\n        args.push('--verbose');\n      }\n\n      await this.execCommand({ distro }, wslHelper, ...args);\n      this.diagnostic({ key: 'docker-plugins', distro });\n    } catch (error) {\n      console.error(`Failed to set up ${ distro } docker plugins: ${ error }`.trim());\n      this.diagnostic({\n        key: 'docker-plugins', distro, error,\n      });\n    }\n  }\n\n  /**\n   * verifyAllDistrosKubeConfig loops through all the available distros\n   * and checks if the kubeconfig can be managed; if any distro fails\n   * the check, an exception is thrown.\n   */\n  async verifyAllDistrosKubeConfig() {\n    const distros = await this.supportedDistros;\n\n    await Promise.all(distros.map(async(distro) => {\n      await this.verifyDistroKubeConfig(distro.name);\n    }));\n  }\n\n  /**\n   * verifyDistroKubeConfig calls the wsl-helper kubeconfig --verify per distro.\n   * It determines the condition of the kubeConfig from the returned error code.\n   */\n  protected async verifyDistroKubeConfig(distro: string) {\n    try {\n      const wslHelper = await this.getLinuxToolPath(distro, executable('wsl-helper-linux'));\n\n      await this.execCommand({ distro }, wslHelper, 'kubeconfig', '--verify');\n    } catch (cause: any) {\n      // Only throw for a specific error code 1, since we control that from the\n      // kubeconfig --verify command. The logic here is to bubble up this error\n      // so that the diagnostic is very specific to this issue. Any other errors\n      // are captured as log messages.\n      if (cause && 'code' in cause && cause.code === 1) {\n        throw new Error(`The kubeConfig contains non-Rancher Desktop configuration in distro ${ distro }`, { cause });\n      } else {\n        console.error(`Verifying kubeconfig in distro ${ distro } failed: ${ cause }`);\n      }\n    }\n    console.debug(`Verified kubeconfig in the following distro: ${ distro }`);\n  }\n\n  protected async syncDistroKubeconfig(distro: string, kubeconfigPath: string | undefined, state: boolean) {\n    if (!kubeconfigPath) {\n      console.debug(`Skipping syncing ${ distro } kubeconfig: no kubeconfig found`);\n      this.diagnostic({ key: 'kubeconfig', distro });\n\n      return 'Error setting up integration';\n    }\n    try {\n      console.debug(`Syncing ${ distro } kubeconfig`);\n      await this.execCommand(\n        {\n          distro,\n          env: {\n            ...process.env,\n            KUBECONFIG: kubeconfigPath,\n            WSLENV:     `${ process.env.WSLENV }:KUBECONFIG/up`,\n          },\n        },\n        await this.getLinuxToolPath(distro, executable('wsl-helper-linux')),\n        'kubeconfig',\n        `--enable=${ state && this.settings.kubernetes?.enabled }`,\n      );\n      this.diagnostic({ key: 'kubeconfig', distro });\n    } catch (error: any) {\n      if (typeof error?.stdout === 'string') {\n        error.stdout = error.stdout.replace(/\\0/g, '');\n      }\n      if (typeof error?.stderr === 'string') {\n        error.stderr = error.stderr.replace(/\\0/g, '');\n      }\n      console.error(`Could not set up kubeconfig integration for ${ distro }:`, error);\n      this.diagnostic({\n        key: 'kubeconfig', distro, error,\n      });\n\n      return `Error setting up integration`;\n    }\n    console.log(`kubeconfig integration for ${ distro } set to ${ state }`);\n  }\n\n  protected async syncDistroSpinCLI(distro: string, state: boolean) {\n    try {\n      if (state && this.settings.experimental?.containerEngine?.webAssembly) {\n        const version = semver.parse(DEPENDENCY_VERSIONS.spinCLI);\n        const env = {\n          KUBE_PLUGIN_VERSION: DEPENDENCY_VERSIONS.spinKubePlugin,\n          SPIN_TEMPLATES_TAG:  (version ? `spin/templates/v${ version.major }.${ version.minor }` : 'unknown'),\n        };\n        const wslenv = Object.keys(env).join(':');\n\n        // wsl-exec is needed to correctly resolve DNS names\n        await this.execCommand({\n          distro,\n          env: {\n            ...process.env, ...env, WSLENV: wslenv,\n          },\n        }, await this.getLinuxToolPath(distro, executable('setup-spin')));\n      }\n      this.diagnostic({ key: 'spin-cli', distro });\n    } catch (error) {\n      this.diagnostic({\n        key: 'spin-cli', distro, error,\n      });\n    }\n  }\n\n  protected get nonBlacklistedDistros(): Promise<WSLDistro[]> {\n    return (async() => {\n      let wslOutput: string;\n\n      try {\n        wslOutput = await this.captureCommand({ encoding: 'utf16le' }, '--list', '--verbose');\n      } catch (error: any) {\n        console.error(`Error listing distros: ${ error }`);\n\n        return Promise.resolve([]);\n      }\n      // As wsl.exe may be localized, don't check state here.\n      const parser = /^[\\s*]+(?<name>.*?)\\s+\\w+\\s+(?<version>\\d+)\\s*$/;\n\n      return wslOutput.trim()\n        .split(/[\\r\\n]+/)\n        .slice(1) // drop the title row\n        .map(line => (parser.exec(line))?.groups)\n        .filter(defined)\n        .map(group => new WSLDistro(group.name, parseInt(group.version)))\n        .filter((distro: WSLDistro) => !DISTRO_BLACKLIST.includes(distro.name));\n    })();\n  }\n\n  /**\n   * Returns a list of WSL distros that RD can integrate with.\n   */\n  protected get supportedDistros(): Promise<WSLDistro[]> {\n    return (async() => {\n      return (await this.nonBlacklistedDistros).filter(distro => distro.version === 2);\n    })();\n  }\n\n  protected async markIntegration(distro: string, state: boolean): Promise<void> {\n    try {\n      const exe = await this.getLinuxToolPath(distro, executable('wsl-helper-linux'));\n      const mode = state ? 'set' : 'delete';\n\n      await this.execCommand({ distro, root: true }, exe, 'wsl', 'integration', 'state', `--mode=${ mode }`);\n    } catch (ex) {\n      console.error(`Failed to mark integration for ${ distro }:`, ex);\n    }\n  }\n\n  async listIntegrations(): Promise<Record<string, boolean | string>> {\n    // Get the results in parallel\n    const distros = await this.nonBlacklistedDistros;\n    const states = distros.map(d => (async() => [d.name, await this.getStateForIntegration(d)] as const)());\n\n    return Object.fromEntries(await Promise.all(states));\n  }\n\n  /**\n   * Tells the caller what the state of a distro is. For more information see\n   * the comment on `IntegrationManager.listIntegrations`.\n   */\n  protected async getStateForIntegration(distro: WSLDistro): Promise<boolean | string> {\n    if (distro.version !== 2) {\n      console.log(`WSL distro \"${ distro.name }\": is version ${ distro.version }`);\n\n      return `Rancher Desktop can only integrate with v2 WSL distributions (this is v${ distro.version }).`;\n    }\n    try {\n      const exe = await this.getLinuxToolPath(distro.name, executable('wsl-helper-linux'));\n      const stdout = await this.captureCommand(\n        { distro: distro.name },\n        exe, 'wsl', 'integration', 'state', '--mode=show');\n\n      console.debug(`WSL distro \"${ distro.name }\": wsl-helper output: \"${ stdout.trim() }\"`);\n      if (['true', 'false'].includes(stdout.trim())) {\n        return stdout.trim() === 'true';\n      } else {\n        return `Error: ${ stdout.trim() }`;\n      }\n    } catch (error) {\n      console.log(`WSL distro \"${ distro.name }\" ${ error }`);\n      if ((typeof error === 'object' && error) || typeof error === 'string') {\n        return `${ error }`;\n      } else {\n        return `Error: unexpected error getting state of distro`;\n      }\n    }\n  }\n\n  async removeSymlinksOnly(): Promise<void> {}\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/layouts/default.vue",
    "content": "<template>\n  <div\n    class=\"wrapper\"\n    :class=\"{\n      blur,\n    }\"\n  >\n    <rd-nav\n      class=\"nav\"\n      :items=\"routes\"\n      :extensions=\"installedExtensions\"\n      @open-dashboard=\"openDashboard\"\n      @open-preferences=\"openPreferences\"\n    />\n    <the-title ref=\"title\" />\n    <main\n      ref=\"body\"\n      class=\"body\"\n    >\n      <RouterView />\n    </main>\n    <!-- The extension area is used for sizing the extension view. -->\n    <div\n      id=\"extension-spacer\"\n      class=\"extension\"\n    />\n    <status-bar class=\"status-bar\" />\n    <!-- The ActionMenu is used by SortableTable for per-row actions. -->\n    <ActionMenu data-testid=\"actionmenu\" />\n  </div>\n</template>\n\n<script>\n\nimport { mapGetters, mapState } from 'vuex';\n\nimport ActionMenu from '@pkg/components/ActionMenu.vue';\nimport Nav from '@pkg/components/Nav.vue';\nimport StatusBar from '@pkg/components/StatusBar.vue';\nimport TheTitle from '@pkg/components/TheTitle.vue';\nimport { mapTypedState } from '@pkg/entry/store';\nimport initExtensions from '@pkg/preload/extensions';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { mainRoutes } from '@pkg/window/constants';\n\nexport default {\n  name:       'App',\n  components: {\n    StatusBar,\n    ActionMenu,\n    rdNav: Nav,\n    TheTitle,\n  },\n\n  data() {\n    return { blur: false };\n  },\n\n  computed: {\n    routes() {\n      const badges = {\n        '/Diagnostics': this.diagnosticsCount,\n        '/Extensions':  this.extensionUpgradeCount,\n      };\n\n      return mainRoutes.map((route) => {\n        if (route.route in badges) {\n          return { ...route, error: badges[route.route] };\n        }\n\n        return route;\n      });\n    },\n    paths() {\n      return mainRoutes.map(r => r.route);\n    },\n    /** @returns {number} The number of diagnostics errors. */\n    diagnosticsCount() {\n      return this.diagnostics.filter(diagnostic => !diagnostic.mute).length;\n    },\n    /** @returns {number} The number of extensions with upgrade available. */\n    extensionUpgradeCount() {\n      return this.installedExtensions.filter(ext => ext.canUpgrade).length;\n    },\n    ...mapState('credentials', ['credentials']),\n    ...mapTypedState('diagnostics', ['diagnostics']),\n    ...mapGetters('extensions', ['installedExtensions']),\n  },\n\n  beforeMount() {\n    // The window title isn't set correctly in E2E; as a workaround, force set\n    // it here again.\n    document.title ||= 'Rancher Desktop';\n\n    this.fetch().catch(ex => console.error(ex));\n\n    initExtensions();\n    ipcRenderer.on('window/blur', (event, blur) => {\n      this.blur = blur;\n    });\n    ipcRenderer.on('backend-locked', (_event, action) => {\n      ipcRenderer.send('preferences-close');\n      this.showCreatingSnapshotDialog(action);\n    });\n    ipcRenderer.on('backend-unlocked', () => {\n      ipcRenderer.send('dialog/close', { dialog: 'SnapshotsDialog', snapshotEventType: 'backend-lock' });\n    });\n\n    ipcRenderer.send('backend-state-check');\n\n    ipcRenderer.on('k8s-check-state', (event, state) => {\n      this.$store.dispatch('k8sManager/setK8sState', state);\n    });\n    ipcRenderer.on('route', (event, args) => {\n      this.goToRoute(args);\n    });\n    ipcRenderer.on('extensions/changed', () => {\n      this.$store.dispatch('extensions/fetch');\n    });\n    this.$store.dispatch('extensions/fetch');\n\n    ipcRenderer.on('preferences/changed', () => {\n      this.$store.dispatch('preferences/fetchPreferences');\n    });\n\n    ipcRenderer.on('extensions/getContentArea', () => {\n      /** @type {DOMRect} */\n      const titleRect = this.$refs.title.$el.getBoundingClientRect();\n      /** @type {DOMRect} */\n      const bodyRect = this.$refs.body.getBoundingClientRect();\n      const payload = {\n        top:    titleRect.top,\n        right:  titleRect.right,\n        bottom: bodyRect.bottom,\n        left:   titleRect.left,\n      };\n\n      ipcRenderer.send('ok:extensions/getContentArea', payload);\n    });\n  },\n\n  mounted() {\n    this.$store.dispatch('credentials/fetchCredentials').catch(console.error);\n    this.$store.dispatch('i18n/init').catch(ex => console.error(ex));\n  },\n\n  beforeUnmount() {\n    ipcRenderer.off('k8s-check-state');\n    ipcRenderer.off('extensions/getContentArea');\n    ipcRenderer.removeAllListeners('backend-locked');\n    ipcRenderer.removeAllListeners('backend-unlocked');\n    ipcRenderer.removeAllListeners('window/blur');\n  },\n\n  methods: {\n    async fetch() {\n      await this.$store.dispatch('credentials/fetchCredentials');\n      if (!this.credentials.port || !this.credentials.user || !this.credentials.password) {\n        console.log(`Credentials aren't ready for getting diagnostics -- will try later`);\n\n        return;\n      }\n      await this.$store.dispatch('preferences/fetchPreferences');\n      await this.$store.dispatch('diagnostics/fetchDiagnostics');\n    },\n\n    openDashboard() {\n      ipcRenderer.send('dashboard-open');\n    },\n    openPreferences() {\n      ipcRenderer.send('preferences-open');\n    },\n    goToRoute(args) {\n      const { path, direction } = args;\n\n      if (path) {\n        this.$router.push({ path });\n\n        return;\n      }\n\n      if (direction) {\n        const dir = (direction === 'forward' ? 1 : -1);\n        const idx = (this.paths.length + this.paths.indexOf(this.$router.currentRoute.path) + dir) % this.paths.length;\n\n        this.$router.push({ path: this.paths[idx] });\n      }\n    },\n    showCreatingSnapshotDialog(action) {\n      ipcRenderer.invoke(\n        'show-snapshots-blocking-dialog',\n        {\n          window: {\n            buttons:  [],\n            cancelId: 1,\n          },\n          format: {\n            header:            action || this.t('snapshots.dialog.generic.header', {}, true),\n            /** TODO: put here operation type information from 'state' */\n            message:           this.t('snapshots.dialog.generic.message', {}, true),\n            showProgressBar:   true,\n            snapshotEventType: 'backend-lock',\n          },\n        },\n      );\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" src=\"@pkg/assets/styles/app.scss\"></style>\n<style lang=\"scss\" scoped>\n.wrapper {\n  display: grid;\n  grid-template:\n    \"nav        title\"\n    \"nav        body\"    1fr\n    \"status-bar status-bar\"\n    / var(--nav-width) 1fr;\n  background-color: var(--body-bg);\n  width: 100vw;\n  height: 100vh;\n\n  &.blur {\n   opacity: 0.2;\n  }\n\n  .header {\n    grid-area: header;\n    border-bottom: var(--header-border-size) solid var(--header-border);\n  }\n\n  .nav {\n    grid-area: nav;\n    border-right: var(--nav-border-size) solid var(--nav-border);\n  }\n\n  .title {\n    grid-area: title;\n  }\n\n  .body {\n    grid-area: body;\n    display: flex;\n    flex-direction: column;\n    padding: 0 20px 20px 20px;\n    overflow: auto;\n  }\n\n  .extension {\n    grid-area: title / title / body / body;\n    z-index: -1000;\n  }\n\n  .status-bar {\n    grid-area: status-bar;\n    border-top: var(--nav-border-size) solid var(--nav-border);\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/layouts/dialog.vue",
    "content": "<!--\n  - This layout is used by dialog boxes that do not want navigation.\n  - With the help of .../pkg/rancher-desktop/window/index.ts, this also handles automatic sizing\n  - of the dialog boxes, using the given events:\n  - emit    'dialog/load':     the dialog has been mounted.\n  - receive 'dialog/populate': any additional data for the dialog.\n  - emit    'dialog/ready':    the dialog is ready to be displayed.\n  - The page component may set \"data-flex\" on the root element to a whitespace-\n  - separated list of \"width\" or \"height\" to help implement flexbox behaviour;\n  - however, this interacts badly with automatic resizing, and should only be\n  - used for larger dialogs.\n  -->\n\n<template>\n  <div\n    ref=\"wrapper\"\n    class=\"wrapper\"\n    open\n  >\n    <RouterView class=\"body\" />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name: 'dialog-layout',\n  mounted() {\n    this.$store.dispatch('i18n/init').catch(ex => console.error(ex));\n    // The page component is mounted before the layout (because the layout\n    // contains the page component); so we can safely send `dialog/load` here\n    // and assume the page has already been mounted.\n    ipcRenderer.on('dialog/populate', async() => {\n      await this.$nextTick();\n      (this.$refs.wrapper as Element)?.setAttribute('data-loaded', '');\n      ipcRenderer.send('dialog/ready');\n    });\n    ipcRenderer.send('dialog/load');\n  },\n});\n</script>\n\n<style lang=\"scss\">\n  html {\n    height: initial;\n  }\n  body {\n    overflow: hidden;\n  }\n</style>\n\n<style lang=\"scss\" src=\"@pkg/assets/styles/app.scss\"></style>\n<style lang=\"scss\" scoped>\n.wrapper {\n  background-color: var(--body-bg);\n  border: none;\n  color: var(--body-text);\n  min-width: 24rem;\n  padding: 1.25rem;\n  margin: 0 auto;\n}\n\n.body {\n  display: flex;\n  flex-flow: column;\n  flex-grow: 1;\n  gap: 1rem;\n}\n\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/layouts/preferences.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name: 'preferences-layout',\n  mounted() {\n    this.$store.dispatch('i18n/init').catch(ex => console.error(ex));\n    ipcRenderer.send('preferences/load');\n  },\n});\n</script>\n\n<template>\n  <div class=\"wrapper\">\n    <RouterView />\n  </div>\n</template>\n\n<style lang=\"scss\" src=\"@pkg/assets/styles/app.scss\"></style>\n<style lang=\"scss\" scoped>\n  .wrapper {\n    background-color: var(--body-bg);\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/main/__tests__/containerExec.spec.ts",
    "content": "/** @jest-environment node */\n\nimport { EventEmitter } from 'events';\n\nimport { jest } from '@jest/globals';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\n// Fake IPC proxy backed by a plain EventEmitter.\n// ContainerExecHandler registers handlers via ipcMainProxy.on(); tests trigger\n// them by emitting on fakeProxy directly.\nconst fakeProxy = new EventEmitter();\n\nmockModules({\n  electron:             undefined,\n  '@pkg/utils/logging': undefined,\n  '@pkg/main/ipcMain':  { getIpcMainProxy: jest.fn(() => fakeProxy) },\n});\n\nlet ContainerExecHandler: Awaited<typeof import('@pkg/main/containerExec')>['ContainerExecHandler'];\n\nbeforeAll(async() => {\n  ({ ContainerExecHandler } = await import('@pkg/main/containerExec'));\n});\n\n// ── test helpers ───────────────────────────────────────────────────────────────\n\n/** Fake WritableReadableProcess with a spied stdin.write and kill. */\nfunction makeProcess() {\n  const proc = new EventEmitter() as any;\n\n  proc.stdout = new EventEmitter();\n  proc.stderr = new EventEmitter();\n  proc.stdin = { write: jest.fn() };\n  proc.kill = jest.fn();\n\n  return proc;\n}\n\n/**\n * Fake return value for the pre-check runClient call.\n * Resolves on exit code 0, rejects with the exit code otherwise.\n */\nfunction makeCheckProcess(exitCode = 0) {\n  if (exitCode === 0) {\n    return Promise.resolve({});\n  }\n  const err: any = new Error(`Exited with exit code ${ exitCode }`);\n\n  err.code = exitCode;\n\n  return Promise.reject(err);\n}\n\n/** Fake Electron WebFrameMain. */\nfunction makeFrame() {\n  return { send: jest.fn() } as any;\n}\n\n/** Fake IPC event (the first arg passed to ipcMain.on handlers). */\nfunction makeEvent(frame = makeFrame()) {\n  return { sender: frame } as any;\n}\n\n/**\n * Start a session where the `script` pre-check succeeds.\n * Returns the session proc and frame for further setup.\n */\nasync function startSession(handler: any, containerId: string) {\n  const checkProc = makeCheckProcess(0);\n  const shellProc = makeProcess();\n  const frame = makeFrame();\n\n  // runClient is called twice: first for the pre-check (exits 0), then for the real shell session.\n  handler._mockClient.runClient\n    .mockReturnValueOnce(checkProc)\n    .mockReturnValueOnce(shellProc);\n\n  fakeProxy.emit('container-exec/start', makeEvent(frame), containerId, undefined);\n\n  // Let the async handler (pre-check await) run.\n  await new Promise(setImmediate);\n\n  const session = handler.sessions.get(containerId)!;\n\n  return {\n    shellProc, frame, session, containerId,\n  };\n}\n\n// ── tests ──────────────────────────────────────────────────────────────────────\n\ndescribe('ContainerExecHandler', () => {\n  let mockClient: { runClient: jest.Mock };\n  let handler: any; // access protected fields via `any`\n\n  beforeEach(() => {\n    fakeProxy.removeAllListeners();\n    mockClient = { runClient: jest.fn() };\n    handler = new ContainerExecHandler(mockClient as any);\n    handler._mockClient = mockClient; // stored for startSession helper\n  });\n\n  // ── new session ─────────────────────────────────────────────────────────────\n\n  describe('container-exec/start — new session', () => {\n    it('runs a pre-check for script availability before starting the session', async() => {\n      const checkProc = makeCheckProcess(0);\n      const shellProc = makeProcess();\n\n      mockClient.runClient\n        .mockReturnValueOnce(checkProc)\n        .mockReturnValueOnce(shellProc);\n\n      fakeProxy.emit('container-exec/start', makeEvent(), 'ctr1', undefined);\n      await new Promise(setImmediate);\n\n      expect(mockClient.runClient).toHaveBeenCalledTimes(2);\n      // First call: pre-check (uses 'ignore' so all stdio goes to /dev/null)\n      expect(mockClient.runClient).toHaveBeenNthCalledWith(\n        1,\n        expect.arrayContaining(['exec', 'ctr1', 'sh', '-c', 'command -v script']),\n        'ignore',\n        expect.any(Object),\n      );\n      // Second call: real session with `script`\n      expect(mockClient.runClient).toHaveBeenNthCalledWith(\n        2,\n        expect.arrayContaining(['exec', '-i', 'ctr1', 'script']),\n        'interactive',\n        expect.any(Object),\n      );\n    });\n\n    it('sends container-exec/unsupported when script is not available', async() => {\n      const checkProc = makeCheckProcess(127);\n      const frame = makeFrame();\n\n      mockClient.runClient.mockReturnValueOnce(checkProc);\n\n      fakeProxy.emit('container-exec/start', makeEvent(frame), 'ctr1', undefined);\n      await new Promise(setImmediate);\n\n      expect(frame.send).toHaveBeenCalledWith('container-exec/unsupported');\n      expect(mockClient.runClient).toHaveBeenCalledTimes(1);\n      expect(handler.sessions.size).toBe(0);\n    });\n\n    it('passes namespace through to runClient', async() => {\n      const checkProc = makeCheckProcess(0);\n      const shellProc = makeProcess();\n\n      mockClient.runClient\n        .mockReturnValueOnce(checkProc)\n        .mockReturnValueOnce(shellProc);\n\n      fakeProxy.emit('container-exec/start', makeEvent(), 'ctr1', 'my-ns');\n      await new Promise(setImmediate);\n\n      expect(mockClient.runClient).toHaveBeenCalledWith(\n        expect.any(Array),\n        'interactive',\n        expect.objectContaining({ namespace: 'my-ns' }),\n      );\n    });\n\n    it('sends container-exec/ready immediately after the session is spawned', async() => {\n      const { frame, containerId } = await startSession(handler, 'ctr1');\n\n      expect(frame.send).toHaveBeenCalledWith('container-exec/ready', containerId, '');\n    });\n\n    it('forwards stdout chunks to renderer as container-exec/output', async() => {\n      const { shellProc, frame, containerId } = await startSession(handler, 'ctr1');\n\n      shellProc.stdout.emit('data', Buffer.from('hello\\n'));\n\n      expect(frame.send).toHaveBeenCalledWith('container-exec/output', containerId, 'hello\\n');\n    });\n\n    it('sends container-exec/exit with the process exit code', async() => {\n      const { shellProc, frame, containerId } = await startSession(handler, 'ctr1');\n\n      shellProc.emit('exit', 42);\n\n      expect(frame.send).toHaveBeenCalledWith('container-exec/exit', containerId, 42);\n    });\n\n    it('cleans up both session maps on process exit', async() => {\n      const { shellProc } = await startSession(handler, 'ctr1');\n\n      shellProc.emit('exit', 0);\n\n      expect(handler.sessions.size).toBe(0);\n    });\n  });\n\n  // ── output ring buffer ───────────────────────────────────────────────────────\n\n  describe('output ring buffer', () => {\n    it('accumulates stdout in outputBuf', async() => {\n      const { shellProc, session } = await startSession(handler, 'ctr1');\n\n      shellProc.stdout.emit('data', Buffer.from('line1\\n'));\n      shellProc.stdout.emit('data', Buffer.from('line2\\n'));\n\n      expect(session.outputBuf).toContain('line1\\n');\n      expect(session.outputBuf).toContain('line2\\n');\n    });\n\n    it('caps outputBuf at 50 KB', async() => {\n      const { shellProc, session } = await startSession(handler, 'ctr1');\n\n      const MAX = 50 * 1024;\n      const big = Buffer.alloc(MAX + 4096, 'x');\n\n      shellProc.stdout.emit('data', big);\n\n      expect(session.outputBuf.length).toBeLessThanOrEqual(MAX);\n    });\n  });\n\n  // ── input ────────────────────────────────────────────────────────────────────\n\n  describe('container-exec/input', () => {\n    it('writes data to stdin', async() => {\n      const { shellProc, containerId } = await startSession(handler, 'ctr1');\n\n      fakeProxy.emit('container-exec/input', {}, containerId, 'ls\\n');\n\n      expect(shellProc.stdin.write).toHaveBeenCalledWith('ls\\n');\n    });\n  });\n\n  // ── detach ────────────────────────────────────────────────────────────────────\n\n  describe('container-exec/detach', () => {\n    it('nulls the frame and marks the session as detached', async() => {\n      const { session, containerId } = await startSession(handler, 'ctr1');\n\n      fakeProxy.emit('container-exec/detach', {}, containerId);\n\n      expect(session.sender).toBeNull();\n      expect(session.detached).toBe(true);\n    });\n\n    it('keeps the process alive after detach', async() => {\n      const { shellProc, containerId } = await startSession(handler, 'ctr1');\n\n      fakeProxy.emit('container-exec/detach', {}, containerId);\n\n      expect(shellProc.kill).not.toHaveBeenCalled();\n      expect(handler.sessions.size).toBe(1);\n    });\n  });\n\n  // ── reconnect ─────────────────────────────────────────────────────────────────\n\n  describe('container-exec/start — reconnect', () => {\n    it('reattaches the frame and replays buffered history without spawning a new process', async() => {\n      const { shellProc, containerId } = await startSession(handler, 'ctr1');\n\n      shellProc.stdout.emit('data', Buffer.from('hello\\n'));\n      fakeProxy.emit('container-exec/detach', {}, containerId);\n\n      const frame2 = makeFrame();\n\n      fakeProxy.emit('container-exec/start', makeEvent(frame2), 'ctr1', undefined);\n      await new Promise(setImmediate);\n\n      // runClient was called twice for the initial session (check + shell);\n      // no additional calls for the reconnect.\n      expect(mockClient.runClient).toHaveBeenCalledTimes(2);\n      expect(frame2.send).toHaveBeenCalledWith(\n        'container-exec/ready',\n        containerId,\n        expect.stringContaining('hello'),\n      );\n    });\n\n    it('spawns a fresh process when the previous session was killed', async() => {\n      const { containerId } = await startSession(handler, 'ctr1');\n\n      fakeProxy.emit('container-exec/kill', {}, containerId);\n\n      // Two more runClient calls for the new session (check + shell).\n      const checkProc2 = makeCheckProcess(0);\n      const shellProc2 = makeProcess();\n\n      mockClient.runClient\n        .mockReturnValueOnce(checkProc2)\n        .mockReturnValueOnce(shellProc2);\n\n      fakeProxy.emit('container-exec/start', makeEvent(), 'ctr1', undefined);\n      await new Promise(setImmediate);\n\n      expect(mockClient.runClient).toHaveBeenCalledTimes(4); // 2 initial + 2 new\n    });\n  });\n\n  // ── kill ──────────────────────────────────────────────────────────────────────\n\n  describe('container-exec/kill', () => {\n    it('terminates the process and removes both session entries', async() => {\n      const { shellProc, containerId } = await startSession(handler, 'ctr1');\n\n      fakeProxy.emit('container-exec/kill', {}, containerId);\n\n      expect(shellProc.kill).toHaveBeenCalledWith('SIGTERM');\n      expect(handler.sessions.size).toBe(0);\n    });\n  });\n\n  // ── killAll ───────────────────────────────────────────────────────────────────\n\n  describe('killAll', () => {\n    it('kills all sessions and clears both maps', async() => {\n      const { shellProc: proc1 } = await startSession(handler, 'ctr1');\n      const { shellProc: proc2 } = await startSession(handler, 'ctr2');\n\n      handler.killAll();\n\n      expect(proc1.kill).toHaveBeenCalledWith('SIGTERM');\n      expect(proc2.kill).toHaveBeenCalledWith('SIGTERM');\n      expect(handler.sessions.size).toBe(0);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/__tests__/deploymentProfiles.spec.ts",
    "content": "/* eslint object-curly-newline: [\"error\", {\"consistent\": true}] */\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport * as settings from '@pkg/config/settings';\nimport { readDeploymentProfiles, validateDeploymentProfile } from '@pkg/main/deploymentProfiles';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst [describeWindows, describeNotWindows] = process.platform === 'win32' ? [describe, describe.skip] : [describe.skip, describe];\n\ndescribe('deployment profiles', () => {\n  describeWindows('windows deployment profiles', () => {\n    let testDir = '';\n    let regFilePath = '';\n\n    // Note that we can't modify the HKLM hive without admin privileges,\n    // so this whole test will just work with the user's HKCU hive.\n    const REG_PATH_START = ['SOFTWARE', 'Rancher Desktop'];\n    const FULL_REG_PATH_START = ['HKEY_CURRENT_USER'].concat(REG_PATH_START);\n    const REGISTRY_PROFILE_PATHS = [REG_PATH_START.concat('TestProfile')];\n\n    const NON_PROFILE_PATH = FULL_REG_PATH_START.join('\\\\');\n    const FULL_PROFILE_PATH = FULL_REG_PATH_START.concat('TestProfile').join('\\\\');\n    const FULL_DEFAULTS_PATH = `${ FULL_PROFILE_PATH }\\\\Defaults`;\n    const FULL_DEFAULTS_PATH_IN_MESSAGE = `HKCU\\\\${ REG_PATH_START.join('\\\\') }\\\\TestProfile\\\\Defaults`;\n\n    // We *could* write a routine that converts json to reg files, but that's not the point of this test.\n    // Better to just hard-wire a few regfiles here.\n\n    const versionHex = `00${ settings.CURRENT_SETTINGS_VERSION.toString(16) }`.slice(-2);\n    const defaultsUserRegFile = `Windows Registry Editor Version 5.00\n\n[${ NON_PROFILE_PATH }]\n\n[${ FULL_PROFILE_PATH }]\n\n[${ FULL_DEFAULTS_PATH }]\n\"version\"=dword:${ versionHex }\n\n[${ FULL_DEFAULTS_PATH }\\\\application]\n\n[${ FULL_DEFAULTS_PATH }\\\\application]\n\"Debug\"=dword:1\n\"adminAccess\"=dword:0\n\n[${ FULL_DEFAULTS_PATH }\\\\application\\\\Telemetry]\n\"ENABLED\"=dword:1\n\n[${ FULL_DEFAULTS_PATH }\\\\application\\\\extensions\\\\installed]\n\"bellingham\"=\"WA\"\n\"portland\"=\"OR\"\n\"shasta\"=\"CA\"\n\"elko\"=\"NV\"\n\n[${ FULL_DEFAULTS_PATH }\\\\CONTAINERENGINE]\n\"name\"=\"moby\"\n\n[${ FULL_DEFAULTS_PATH }\\\\containerEngine\\\\allowedImages]\n\"patterns\"=hex(7):${ stringToMultiStringHexBytes(['edmonton', 'calgary', 'red deer', 'bassano']) }\n\"enabled\"=dword:00000000\n\n[${ FULL_DEFAULTS_PATH }\\\\wsl]\n\n[${ FULL_DEFAULTS_PATH }\\\\wsl\\\\integrations]\n\"kingston\"=dword:0\n\"napanee\"=dword:0\n\"yarker\"=dword:1\n\"weed\"=dword:1\n\n[${ FULL_DEFAULTS_PATH }\\\\kubernetes]\n\"version\"=\"867-5309\"\n\n[${ FULL_DEFAULTS_PATH }\\\\diagnostics]\n\"showmuted\"=dword:1\n\n[${ FULL_DEFAULTS_PATH }\\\\diagnostics\\\\mutedChecks]\n\"montreal\"=dword:1\n\"riviere du loup\"=dword:0\n\"magog\"=dword:0\n`;\n\n    const lockedUserRegFile = `Windows Registry Editor Version 5.00\n\n[${ NON_PROFILE_PATH }]\n\n[${ FULL_PROFILE_PATH }]\n\n[${ FULL_PROFILE_PATH }\\\\Locked]\n\"version\"=dword:${ versionHex }\n\n[${ FULL_PROFILE_PATH }\\\\Locked\\\\containerEngine]\n\n[${ FULL_PROFILE_PATH }\\\\Locked\\\\containerEngine\\\\allowedImages]\n\"enabled\"=dword:00000000\n\"patterns\"=hex(7):${ stringToMultiStringHexBytes(['busybox', 'nginx']) }\n`;\n\n    // Deliberate errors in defaults:\n    // * Specifying application/updater=1 instead of application/updater/enabled=1\n    // * Specifying application/adminAccess/debug=string when it should be 0 or 1\n    // * application/debug should be a number, not a string\n    // * containerEngine/name should be a string, not a number\n    // * containerEngine/allowedImages/patterns should be a list of strings, not a number\n    // * containerEngine/allowedImages/enabled should be a boolean, not a string\n    // * images/namespace should be a single string, not a multi-SZ string value\n    // * wsl/integrations should be a special-purpose object\n    // * diagnostics/mutedChecks should be a special-purpose object\n    // * kubernetes/version should be a string, not an object\n\n    const incorrectDefaultsUserRegFile = `Windows Registry Editor Version 5.00\n\n[${ NON_PROFILE_PATH }]\n\n[${ FULL_PROFILE_PATH }]\n\n[${ FULL_DEFAULTS_PATH }]\n\"version\"=dword:${ versionHex }\n\n[${ FULL_DEFAULTS_PATH }\\\\application]\n\n[${ FULL_DEFAULTS_PATH }\\\\application]\n\"Debug\"=\"should be a number\"\n\"Updater\"=dword:0\n\n[${ FULL_DEFAULTS_PATH }\\\\application\\\\adminAccess]\n\"sudo\"=dword:1\n\n[${ FULL_DEFAULTS_PATH }\\\\application\\\\Telemetry]\n\"ENABLED\"=dword:1\n\n[${ FULL_DEFAULTS_PATH }\\\\CONTAINERENGINE]\n\"name\"=dword:5\n\n[${ FULL_DEFAULTS_PATH }\\\\containerEngine\\\\allowedImages]\n\"patterns\"=dword:19\n\"enabled\"=\"should be a boolean\"\n\n[${ FULL_DEFAULTS_PATH }\\\\images]\n\"namespace\"=hex(7):${ stringToMultiStringHexBytes(['busybox', 'nginx']) }\n\n[${ FULL_DEFAULTS_PATH }\\\\wsl]\n\"integrations\"=\"should be a sub-object\"\n\n[${ FULL_DEFAULTS_PATH }\\\\kubernetes]\n\n[${ FULL_DEFAULTS_PATH }\\\\kubernetes\\\\version]\n\n[${ FULL_DEFAULTS_PATH }\\\\diagnostics]\n\"showmuted\"=dword:1\n\"mutedChecks\"=dword:42\n`;\n\n    const arrayFromSingleStringDefaultsUserRegFile = `Windows Registry Editor Version 5.00\n\n[${ NON_PROFILE_PATH }]\n\n[${ FULL_PROFILE_PATH }]\n\n[${ FULL_DEFAULTS_PATH }]\n\"version\"=dword:${ versionHex }\n\n[${ FULL_DEFAULTS_PATH }\\\\CONTAINERENGINE]\n\"name\"=\"moby\"\n\n[${ FULL_DEFAULTS_PATH }\\\\containerEngine\\\\allowedImages]\n\"patterns\"=\"hokey smoke!\"\n`;\n\n    async function clearRegistry() {\n      try {\n        await spawnFile('reg', ['DELETE', `HKCU\\\\${ REGISTRY_PROFILE_PATHS[0].join('\\\\') }`, '/f']);\n      } catch {\n        // Ignore any errors\n      }\n    }\n\n    async function installInRegistry(regFileContents: string) {\n      await fs.promises.writeFile(regFilePath, regFileContents, { encoding: 'ascii' });\n      try {\n        await spawnFile('reg', ['IMPORT', regFilePath]);\n      } catch (ex: any) {\n        // Use expect to display the error message\n        expect(ex).toBeNull();\n      }\n    }\n\n    // Registry multi-stringSZ settings in a reg file are hard to read, so expand them here.\n    // e.g.=> [\"abc\", \"def\"] would be ucs-2-encoded as '61,00,62,00,63,00,00,00,64,00,65,00,66,00,00,00,00,00'\n    // where a null 16-bit word (so two 00 bytes) separate each pair of words and\n    // two null 16-bit words (\"00 00 00 00\") indicate the end of the list\n    function stringToMultiStringHexBytes(s: string[]): string {\n      const hexBytes = Buffer.from(s.join('\\x00'), 'ucs2')\n        .toString('hex')\n        .split(/(..)/)\n        .filter(x => x)\n        .join(',');\n\n      return `${ hexBytes },00,00,00,00`;\n    }\n\n    beforeEach(async() => {\n      const nativeReg = await import('native-reg');\n      nativeReg.deleteTree(nativeReg.HKCU, path.join(...(REGISTRY_PROFILE_PATHS[0])));\n      testDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'regtest-'));\n      regFilePath = path.join(testDir, 'import.reg');\n    });\n    afterEach(async() => {\n      await fs.promises.rm(testDir, { force: true, recursive: true });\n    });\n    // TODO:  Add an `afterAll(clearRegistry)` when we're finished developing.\n\n    describe('profile', () => {\n      describe('defaults', () => {\n        describe('happy paths', () => {\n          const defaultUserProfile: RecursivePartial<settings.Settings> = {\n            version:     settings.CURRENT_SETTINGS_VERSION,\n            application: {\n              debug:       true,\n              adminAccess: false,\n              telemetry:   { enabled: true },\n              extensions:  {\n                installed: {\n                  bellingham: 'WA',\n                  portland:   'OR',\n                  shasta:     'CA',\n                  elko:       'NV',\n                },\n              },\n            },\n            containerEngine: {\n              allowedImages: {\n                enabled:  false,\n                patterns: ['edmonton', 'calgary', 'red deer', 'bassano'],\n              },\n              name: settings.ContainerEngine.MOBY,\n            },\n            WSL: {\n              integrations: {\n                kingston: false,\n                napanee:  false,\n                yarker:   true,\n                weed:     true,\n              },\n            },\n            kubernetes: {\n              version: '867-5309',\n            },\n            diagnostics: {\n              showMuted:   true,\n              mutedChecks: {\n                montreal:          true,\n                'riviere du loup': false,\n                magog:             false,\n              },\n            },\n          };\n          const lockedUserProfile: RecursivePartial<settings.Settings> = {\n            version:         settings.CURRENT_SETTINGS_VERSION,\n            containerEngine: {\n              allowedImages: {\n                enabled:  false,\n                patterns: ['busybox', 'nginx'],\n              },\n            },\n          };\n\n          describe('no system profiles, no user profiles', () => {\n            it('loads nothing', async() => {\n              const profile = await readDeploymentProfiles(REGISTRY_PROFILE_PATHS);\n\n              expect(profile.defaults).toEqual({});\n              expect(profile.locked).toEqual({});\n            });\n          });\n\n          describe('no system profiles, both user profiles', () => {\n            it('loads both profiles', async() => {\n              await clearRegistry();\n              await installInRegistry(defaultsUserRegFile);\n              await installInRegistry(lockedUserRegFile);\n              const profile = await readDeploymentProfiles(REGISTRY_PROFILE_PATHS);\n\n              expect(profile.defaults).toEqual(defaultUserProfile);\n              expect(profile.locked).toEqual(lockedUserProfile);\n            });\n          });\n\n          it('converts a single string into an array', async() => {\n            await clearRegistry();\n            await installInRegistry(arrayFromSingleStringDefaultsUserRegFile);\n            const profile = await readDeploymentProfiles(REGISTRY_PROFILE_PATHS);\n\n            expect(profile.defaults).toMatchObject({\n              containerEngine: { allowedImages: { patterns: ['hokey smoke!'] }, name: 'moby' },\n            });\n          });\n        });\n        describe('error paths', () => {\n          it('loads a bad profile, complains about all the errors, and keeps only valid entries', async() => {\n            await clearRegistry();\n            await installInRegistry(incorrectDefaultsUserRegFile);\n            const expectedErrors = [\n              `application\\\\adminAccess': expecting value of type boolean, got a registry object`,\n              `application\\\\Debug': expecting value of type boolean, got '\"should be a number\"'`,\n              `application\\\\Updater': expecting value of type object, got a DWORD, value: '0'`,\n              `containerEngine\\\\allowedImages\\\\patterns': expecting value of type array, got '25'`,\n              `containerEngine\\\\allowedImages\\\\enabled': expecting value of type boolean, got '\"should be a boolean\"'`,\n              `containerEngine\\\\name': expecting value of type string, got '5'`,\n              `diagnostics\\\\mutedChecks': expecting value of type object, got a DWORD, value: '66'`,\n              `images\\\\namespace': expecting value of type string, got an array '[\"busybox\",\"nginx\"]'`,\n              `kubernetes\\\\version': expecting value of type string, got a registry object`,\n              `WSL\\\\integrations': expecting value of type object, got a SZ, value: '\"should be a sub-object\"'`,\n            ].map(s => `Error for field '${ FULL_DEFAULTS_PATH_IN_MESSAGE }\\\\${ s }`);\n            let error: Error | undefined;\n\n            try {\n              await readDeploymentProfiles(REGISTRY_PROFILE_PATHS);\n            } catch (ex: any) {\n              error = ex;\n            }\n            expect(error).toBeInstanceOf(Error);\n            expectedErrors.unshift('Error in registry settings:');\n            expect((error?.message ?? '').split('\\n')).toEqual(expect.arrayContaining(expectedErrors));\n          });\n        });\n      });\n    });\n  });\n\n  describeNotWindows('non-windows deployment profiles', () => {\n    const invalidDefaultProfile = {\n      application: {\n        debug:                  'should be a boolean',\n        updater:                0,\n        pathManagementStrategy: 'goose',\n        adminAccess:            {\n          sudo: true,\n        },\n      },\n      containerEngine: {\n        name:          5,\n        allowedImages: {\n          patterns: 19,\n          enabled:  'should be a boolean',\n        },\n      },\n      images: {\n        namespace: ['busybox', 'nginx'],\n      },\n      kubernetes: {\n        port: {\n          zoo: ['possums', 'snakes', 'otters'],\n        },\n        version: { },\n        enabled: -7,\n      },\n      diagnostics: {\n        showMuted:   [true],\n        mutedChecks: 'should be an object',\n      },\n    };\n\n    test('complains about invalid default values', () => {\n      const expectedErrors = [\n        'Error in deployment file fake default profile:',\n        `Error for field 'application.debug': expecting value of type boolean, got '\"should be a boolean\"'`,\n        `Error for field 'application.updater': expecting value of type object, got '0'`,\n        `Error for field 'application.adminAccess': expecting value of type boolean, got '{\"sudo\":true}'`,\n        `Error for field 'containerEngine.name': expecting value of type string, got '5'`,\n        `Error for field 'containerEngine.allowedImages.patterns': expecting value of type array, got '19'`,\n        `Error for field 'containerEngine.allowedImages.enabled': expecting value of type boolean, got '\"should be a boolean\"'`,\n        `Error for field 'images.namespace': expecting value of type string, got an array [\"busybox\",\"nginx\"]`,\n        `Error for field 'kubernetes.port': expecting value of type number, got '{\"zoo\":[\"possums\",\"snakes\",\"otters\"]}'`,\n        `Error for field 'kubernetes.version': expecting value of type string, got '{}'`,\n        `Error for field 'kubernetes.enabled': expecting value of type boolean, got '-7'`,\n        `Error for field 'diagnostics.showMuted': expecting value of type boolean, got an array [true]`,\n        `Error for field 'diagnostics.mutedChecks': expecting value of type object, got '\"should be an object\"'`,\n      ];\n      let error: Error | undefined;\n\n      try {\n        validateDeploymentProfile('fake default profile', invalidDefaultProfile, settings.defaultSettings, []);\n      } catch (ex: any) {\n        error = ex;\n      }\n      expect(error).toBeInstanceOf(Error);\n      expect((error?.message ?? '').split('\\n')).toEqual(expect.arrayContaining(expectedErrors));\n    });\n    test('complains about invalid locked settings', () => {\n      const expectedErrors = [\n        'Error in deployment file fake locked profile:',\n        `Error for field 'containerEngine.allowedImages.patterns': expecting value of type array, got '19'`,\n        `Error for field 'containerEngine.allowedImages.enabled': expecting value of type boolean, got '\"should be a boolean\"'`,\n      ];\n      let error: Error | undefined;\n\n      try {\n        validateDeploymentProfile('fake locked profile', invalidDefaultProfile, settings.defaultSettings, []);\n      } catch (ex: any) {\n        error = ex;\n      }\n      expect(error).toBeInstanceOf(Error);\n      expect((error?.message ?? '').split('\\n')).toEqual(expect.arrayContaining(expectedErrors));\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/__tests__/ipcMain.spec.ts",
    "content": "import events from 'events';\n\nimport { jest } from '@jest/globals';\nimport Electron from 'electron';\n\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport { Log } from '@pkg/utils/logging';\n\ntype Handler = (event: Electron.IpcMainInvokeEvent, ...args: any) => Promise<unknown>;\n\ndescribe('IpcMainProxy', () => {\n  let log: Log;\n  let emitter: E;\n  let subject: ReturnType<typeof getIpcMainProxy>;\n\n  class E extends events.EventEmitter implements Electron.IpcMain {\n    protected handlers: Record<string, { once: boolean, handler: Handler }> = {};\n\n    handle(channel: string, handler: Handler) {\n      this.handleInternal(channel, handler, false);\n    }\n\n    handleOnce(channel: string, handler: Handler) {\n      this.handleInternal(channel, handler, true);\n    }\n\n    protected handleInternal(channel: string, handler: Handler, once: boolean) {\n      if (this.handlers[channel]) {\n        throw new Error(`Already have handler for ${ channel }`);\n      }\n      this.handlers[channel] = { handler, once };\n    }\n\n    removeHandler(channel: string) {\n      delete this.handlers[channel];\n    }\n\n    invoke(channel: string, ...args: any) {\n      const data = this.handlers[channel];\n\n      if (!data) {\n        return Promise.reject(new Error(`No handler for ${ channel }`));\n      }\n      const { handler, once } = data;\n\n      if (once) {\n        delete this.handlers[channel];\n      }\n\n      return new Promise((resolve, reject) => {\n        try {\n          handler(null as any, ...args).then(resolve).catch(reject);\n        } catch (ex) {\n          reject(ex);\n        }\n      });\n    }\n\n    clear() {\n      for (const ch of this.eventNames()) {\n        this.removeAllListeners(ch);\n      }\n      for (const ch of Object.keys(this.handlers)) {\n        this.removeHandler(ch);\n      }\n    }\n  }\n\n  beforeAll(() => {\n    emitter = new E();\n  });\n\n  afterAll(() => {\n    jest.restoreAllMocks();\n  });\n\n  beforeAll(() => {\n    log = new Log('ipc-main-test');\n    for (const meth of ['log', 'error', 'info', 'warn', 'debug', 'debugE'] as const) {\n      jest.spyOn(log, meth);\n    }\n    subject = getIpcMainProxy(log, emitter);\n  });\n\n  afterEach(() => {\n    jest.clearAllMocks();\n    emitter.clear();\n  });\n\n  it('should allow adding event listeners', () => {\n    const topic = 'get-app-version' as const;\n    const cb = jest.fn();\n\n    subject.on(topic, cb);\n    emitter.emit(topic);\n    expect(cb).toHaveBeenCalled();\n    expect(log.debug).toHaveBeenCalledWith(expect.stringContaining(topic));\n  });\n\n  it('should allow removing event listeners', () => {\n    const topic = 'get-app-version' as const;\n    const cb = jest.fn();\n\n    subject.on(topic, cb);\n    subject.removeListener(topic, cb);\n    emitter.emit(topic);\n    expect(cb).not.toHaveBeenCalled();\n    expect(log.debug).not.toHaveBeenCalled();\n  });\n\n  it('should allow single-use listeners', () => {\n    const topic = 'get-app-version' as const;\n    const cb = jest.fn();\n\n    subject.once(topic, cb);\n    emitter.emit(topic);\n    emitter.emit(topic);\n    expect(cb).toHaveBeenCalledTimes(1);\n    expect(log.debug).toHaveBeenCalledTimes(1);\n  });\n\n  it('should allow removing all listeners of a topic', () => {\n    const topic = 'get-app-version' as const;\n    const cb = jest.fn();\n\n    subject.on(topic, cb);\n    subject.removeAllListeners(topic);\n    emitter.emit(topic);\n    expect(cb).not.toHaveBeenCalled();\n    expect(log.debug).not.toHaveBeenCalled();\n  });\n\n  it('should reject missing handlers', async() => {\n    const topic = 'api-get-credentials' as const;\n\n    await expect(emitter.invoke(topic)).rejects.toThrow(topic);\n  });\n\n  it('should allow adding event handlers', async() => {\n    const topic = 'api-get-credentials' as const;\n\n    type cbType = Parameters<typeof subject.handle<typeof topic>>[1];\n    const cb = jest.fn().mockImplementation(() => Promise.resolve(1)) as cbType;\n\n    subject.handle(topic, cb);\n    await expect(emitter.invoke(topic)).resolves.not.toThrow();\n    expect(cb).toHaveBeenCalled();\n    expect(log.debug).toHaveBeenCalledWith(expect.stringContaining(topic));\n  });\n\n  it('should allow removing event handlers', async() => {\n    const topic = 'api-get-credentials' as const;\n\n    type cbType = Parameters<typeof subject.handle<typeof topic>>[1];\n    const cb = jest.fn().mockImplementation(() => Promise.resolve(1)) as cbType;\n\n    subject.handle(topic, cb);\n    subject.removeHandler(topic);\n    await expect(emitter.invoke(topic)).rejects.toThrow(topic);\n    expect(cb).not.toHaveBeenCalled();\n    expect(log.debug).not.toHaveBeenCalled();\n  });\n\n  it('should allow single-use event handlers', async() => {\n    const topic = 'api-get-credentials' as const;\n\n    type cbType = Parameters<typeof subject.handle<typeof topic>>[1];\n    const cb = jest.fn().mockImplementation(() => Promise.resolve(1)) as cbType;\n\n    subject.handleOnce(topic, cb);\n    await expect(emitter.invoke(topic)).resolves.not.toThrow();\n    await expect(emitter.invoke(topic)).rejects.toThrow(topic);\n    expect(cb).toHaveBeenCalledTimes(1);\n    expect(log.debug).toHaveBeenCalledTimes(1);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/commandServer/__tests__/settingsValidator.spec.ts",
    "content": "/* eslint object-curly-newline: [\"error\", {\"consistent\": true}] */\n\nimport os from 'os';\n\nimport { jest } from '@jest/globals';\nimport _ from 'lodash';\nimport { SemVer } from 'semver';\n\nimport * as settings from '@pkg/config/settings';\nimport { MountType, Theme, VMType } from '@pkg/config/settings';\nimport { getDefaultMemory } from '@pkg/config/settingsImpl';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst modules = mockModules({\n  os: {\n    arch:     jest.spyOn(os, 'arch'),\n    platform: jest.spyOn(os, 'platform'),\n  },\n  '@pkg/utils/osVersion': {\n    getMacOsVersion: jest.fn<() => SemVer>(() => new SemVer('13.5.0')),\n  },\n});\n\nconst cfg = _.merge(\n  {},\n  settings.defaultSettings,\n  {\n    kubernetes:  { version: '1.29.4' },\n    application: { pathManagementStrategy: PathManagementStrategy.Manual },\n  });\n\nconst subject = new (await import('../settingsValidator')).default();\n\nbeforeEach(() => {\n  modules.os.platform.mockReturnValue(process.platform);\n});\nafterEach(() => {\n  modules.os.platform.mockRestore();\n});\n\ncfg.virtualMachine.memoryInGB ||= getDefaultMemory();\nsubject.k8sVersions = ['1.29.4', '1.0.0'];\ndescribe('SettingsValidator', () => {\n  it('should do nothing when given existing settings', () => {\n    const [needToUpdate, errors] = subject.validateSettings(cfg, cfg);\n\n    expect({ needToUpdate, errors }).toEqual({\n      needToUpdate: false,\n      errors:       [],\n    });\n  });\n\n  it('should want to apply changes when valid new settings are proposed', () => {\n    const newEnabled = !cfg.kubernetes.enabled;\n    const newVersion = subject.k8sVersions[1];\n    const newEngine = cfg.containerEngine.name === 'moby' ? 'containerd' : 'moby';\n    const newFlannelEnabled = !cfg.kubernetes.options.flannel;\n    const newConfig = _.merge({}, cfg, {\n      containerEngine: { name: newEngine },\n      kubernetes:\n        {\n          enabled: newEnabled,\n          version: newVersion,\n          options: { flannel: newFlannelEnabled },\n        },\n    });\n    const [needToUpdate, errors] = subject.validateSettings(cfg, newConfig);\n\n    expect({ needToUpdate, errors }).toEqual({\n      needToUpdate: true,\n      errors:       [],\n    });\n  });\n\n  describe('all standard fields', () => {\n    // Special fields that cannot be checked here; this includes enums and maps.\n    const specialFields = [\n      ['application', 'pathManagementStrategy'],\n      ['application', 'theme'],\n      ['containerEngine', 'allowedImages', 'locked'],\n      ['containerEngine', 'mobyStorageDriver'],\n      ['containerEngine', 'name'],\n      ['experimental', 'kubernetes', 'options', 'spinkube'],\n      ['experimental', 'virtualMachine', 'diskSize'], // Special parsing.\n      ['experimental', 'virtualMachine', 'mount', '9p', 'cacheMode'],\n      ['experimental', 'virtualMachine', 'mount', '9p', 'msizeInKib'],\n      ['experimental', 'virtualMachine', 'mount', '9p', 'protocolVersion'],\n      ['experimental', 'virtualMachine', 'mount', '9p', 'securityModel'],\n      ['experimental', 'virtualMachine', 'proxy', 'noproxy'],\n      ['kubernetes', 'version'],\n      ['version'],\n      ['virtualMachine', 'mount', 'type'],\n      ['virtualMachine', 'type'],\n      ['virtualMachine', 'useRosetta'],\n      ['WSL', 'integrations'],\n    ];\n\n    // Fields that can only be set on specific platforms.\n    const platformSpecificFields: Record<string, ReturnType<typeof os.platform>> = {\n      'application.adminAccess':                      'linux',\n      'experimental.virtualMachine.proxy.enabled':    'win32',\n      'experimental.virtualMachine.proxy.address':    'win32',\n      'experimental.virtualMachine.proxy.password':   'win32',\n      'experimental.virtualMachine.proxy.port':       'win32',\n      'experimental.virtualMachine.proxy.username':   'win32',\n      'experimental.virtualMachine.sshPortForwarder': 'darwin',\n      'kubernetes.ingress.localhostOnly':             'win32',\n      'virtualMachine.memoryInGB':                    'darwin',\n      'virtualMachine.numberCPUs':                    'linux',\n    };\n\n    const spyValidateSettings = jest.spyOn(subject, 'validateSettings');\n\n    function checkSetting(path: string[], defaultSettings: any) {\n      const prefix = path.length === 0 ? '' : `${ path.join('.') }.`;\n      const props = [];\n\n      if (specialFields.some(specialField => _.isEqual(path, specialField))) {\n        return;\n      }\n\n      for (const key of Object.keys(defaultSettings)) {\n        if (typeof defaultSettings[key] === 'object') {\n          checkSetting(path.concat(key), defaultSettings[key]);\n        } else {\n          if (specialFields.some(specialField => _.isEqual(path.concat(key), specialField))) {\n            continue;\n          }\n          props.push(key);\n        }\n      }\n\n      if (props.length === 0) {\n        return;\n      }\n\n      describe.each(props.sort())(`${ prefix }%s`, (key) => {\n        const keyPath = path.concat(key);\n\n        if (keyPath.join('.') in platformSpecificFields) {\n          beforeEach(() => {\n            modules.os.platform.mockReturnValue(platformSpecificFields[keyPath.join('.')]);\n          });\n        }\n\n        it('should never complain when nothing is changed', () => {\n          const input = _.set({}, keyPath, _.get(cfg, keyPath));\n          const [needToUpdate, errors] = subject.validateSettings(cfg, input);\n\n          expect({ needToUpdate, errors }).toEqual({\n            needToUpdate: false,\n            errors:       [],\n          });\n        });\n\n        if (specialFields.some(specialField => _.isEqual(path.concat(key), specialField))) {\n          return;\n        }\n\n        it('should allow changing', () => {\n          let newValue: any;\n\n          switch (typeof defaultSettings[key]) {\n          case 'boolean':\n            newValue = !defaultSettings[key];\n            break;\n          case 'number':\n            newValue = defaultSettings[key] + 1;\n            break;\n          case 'string':\n            newValue = `${ defaultSettings[key] }!`;\n            break;\n          default:\n            expect(['boolean', 'number', 'string']).toContain(typeof defaultSettings[key]);\n          }\n\n          const input = _.set({}, keyPath, newValue);\n          const [needToUpdate, errors] = subject.validateSettings(cfg, input);\n\n          expect({ needToUpdate, errors }).toEqual({\n            needToUpdate: true,\n            errors:       [],\n          });\n        });\n\n        it('should disallow invalid values', () => {\n          let invalidValue: any;\n\n          if (typeof defaultSettings[key] !== 'string') {\n            invalidValue = 'invalid value';\n          } else {\n            invalidValue = 3;\n          }\n\n          const input = _.set({}, keyPath, invalidValue);\n          const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, input);\n\n          expect({ needToUpdate, errors, isFatal }).toEqual({\n            needToUpdate: false,\n            errors:       [`Invalid value for \"${ prefix }${ key }\": <${ JSON.stringify(invalidValue) }>`],\n            isFatal:      false,\n          });\n        });\n\n        if (typeof defaultSettings[key] === 'boolean') {\n          it('should accept string true', () => {\n            const orig = _.merge({}, cfg, _.set({}, keyPath, false));\n            const [needToUpdate, errors] = subject.validateSettings(orig, _.set({}, keyPath, 'true'));\n\n            expect({ needToUpdate, errors }).toEqual({\n              needToUpdate: true,\n              errors:       [],\n            });\n          });\n          it('should accept string false', () => {\n            const orig = _.merge({}, cfg, _.set({}, keyPath, true));\n            const [needToUpdate, errors] = subject.validateSettings(orig, _.set({}, keyPath, 'false'));\n\n            expect({ needToUpdate, errors }).toEqual({\n              needToUpdate: true,\n              errors:       [],\n            });\n          });\n        }\n      });\n    }\n\n    checkSetting([], cfg);\n\n    it('should have validated at least one setting', () => {\n      expect(spyValidateSettings).toHaveBeenCalled();\n    });\n  });\n\n  describe('containerEngine.name', () => {\n    function configWithValue(value: string | settings.ContainerEngine): settings.Settings {\n      return {\n        ...cfg,\n        containerEngine: {\n          ...cfg.containerEngine,\n          name: value as settings.ContainerEngine,\n        },\n      };\n    }\n\n    describe('should accept valid settings', () => {\n      const validKeys = Object.keys(settings.ContainerEngine).filter(x => x !== 'NONE');\n\n      test.each(validKeys)('%s', (key) => {\n        const typedKey = key as keyof typeof settings.ContainerEngine;\n        const [needToUpdate, errors] = subject.validateSettings(\n          configWithValue(settings.ContainerEngine.NONE),\n          { containerEngine: { name: settings.ContainerEngine[typedKey] } },\n        );\n\n        expect({ needToUpdate, errors }).toEqual({\n          needToUpdate: true,\n          errors:       [],\n        });\n      });\n    });\n\n    it('should reject setting to NONE', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, { containerEngine: { name: settings.ContainerEngine.NONE } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [expect.stringContaining('Invalid value for \"containerEngine.name\": <\"\">;')],\n        isFatal:      true,\n      });\n    });\n\n    describe('should accept aliases', () => {\n      const aliases = ['docker'];\n\n      it.each(aliases)('%s', (alias) => {\n        const [needToUpdate, errors] = subject.validateSettings(\n          configWithValue(settings.ContainerEngine.NONE),\n          { containerEngine: { name: alias as settings.ContainerEngine } },\n        );\n\n        expect({ needToUpdate, errors }).toEqual({\n          needToUpdate: true,\n          errors:       [],\n        });\n      });\n    });\n\n    it('should reject invalid values', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(\n        cfg,\n        { containerEngine: { name: 'pikachu' as settings.ContainerEngine } },\n      );\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [expect.stringContaining('Invalid value for \"containerEngine.name\": <\"pikachu\">; must be one of [\"containerd\",\"moby\",\"docker\"]')],\n        isFatal:      true,\n      });\n    });\n  });\n\n  describe('containerEngine.mobyStorageDriver', () => {\n    describe('should accept valid values', () => {\n      const validValues = ['classic', 'snapshotter', 'auto'] as const;\n      const pairs = validValues.flatMap(l => validValues.map(r => [l, r] as const));\n\n      test.each(pairs)('%s -> %s', (from, to) => {\n        const [needToUpdate, errors] = subject.validateSettings(\n          _.merge({}, cfg, { containerEngine: { mobyStorageDriver: from } }),\n          { containerEngine: { mobyStorageDriver: to } },\n        );\n\n        expect({ needToUpdate, errors }).toEqual({\n          needToUpdate: from !== to,\n          errors:       [],\n        });\n      });\n    });\n    it('should reject invalid values', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(\n        cfg,\n        { containerEngine: { mobyStorageDriver: 'pikachu' as any } },\n      );\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [expect.stringContaining('Invalid value for \"containerEngine.mobyStorageDriver\": <\"pikachu\">; must be one of [\"classic\",\"snapshotter\",\"auto\"]')],\n        isFatal:      true,\n      });\n    });\n  });\n\n  describe('WSL.integrations', () => {\n    beforeEach(() => {\n      modules.os.platform.mockReturnValue('win32');\n    });\n\n    it('should reject invalid values', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, { WSL: { integrations: 3 as unknown as Record<string, boolean> } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       ['Proposed field \"WSL.integrations\" should be an object, got <3>.'],\n        isFatal:      false,\n      });\n    });\n\n    it('should reject being set on non-Windows', () => {\n      modules.os.platform.mockReturnValue('haiku');\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, { WSL: { integrations: { foo: true } } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [`Changing field \"WSL.integrations\" via the API isn't supported.`],\n        isFatal:      true,\n      });\n    });\n\n    it('should reject invalid configuration', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, { WSL: { integrations: { distribution: 3 as unknown as boolean } } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       ['Invalid value for \"WSL.integrations.distribution\": <3>'],\n        isFatal:      false,\n      });\n    });\n\n    it('should allow being changed', () => {\n      const [needToUpdate, errors] = subject.validateSettings({\n        ...cfg,\n        WSL: { integrations: { distribution: false } },\n      }, { WSL: { integrations: { distribution: true } } });\n\n      expect({ needToUpdate, errors }).toEqual({\n        needToUpdate: true,\n        errors:       [],\n      });\n    });\n  });\n\n  describe('kubernetes.version', () => {\n    it('should accept a valid version', () => {\n      const [needToUpdate, errors] = subject.validateSettings(cfg, { kubernetes: { version: '1.0.0' } });\n\n      expect({ needToUpdate, errors }).toEqual({\n        needToUpdate: true,\n        errors:       [],\n      });\n    });\n\n    it('should reject an unknown version', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, {\n        kubernetes: {\n          version: '3.2.1',\n          enabled: true,\n        },\n      });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [`Kubernetes version \"3.2.1\" not found.`],\n        isFatal:      false,\n      });\n    });\n\n    it('should normalize the version', () => {\n      const [needToUpdate, errors] = subject.validateSettings(\n        cfg,\n        { kubernetes: { version: 'v1.0.0+k3s12345' } });\n\n      expect({ needToUpdate, errors }).toEqual({\n        needToUpdate: true,\n        errors:       [],\n      });\n    });\n\n    it('should reject a non-version value', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(\n        cfg,\n        {\n          kubernetes: {\n            version: 'pikachu',\n            enabled: true,\n          },\n        });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [`Kubernetes version \"pikachu\" not found.`],\n        isFatal:      false,\n      });\n    });\n  });\n\n  describe('application.theme', () => {\n    describe('should accept valid settings', () => {\n      test.each(Object.keys(Theme))('%s', (key) => {\n        const value = Theme[key as keyof typeof Theme];\n        const [needToUpdate, errors] = subject.validateSettings({\n          ...cfg,\n          application: { ...cfg.application, theme: Theme.SYSTEM },\n        }, { application: { theme: value } });\n\n        expect({ needToUpdate, errors }).toEqual({\n          needToUpdate: value !== Theme.SYSTEM,\n          errors:       [],\n        });\n      });\n    });\n\n    it('should reject invalid values', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg,\n        { application: { theme: 'invalid' as Theme } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [`Invalid value for \"application.theme\": <\"invalid\">; must be one of [\"system\",\"light\",\"dark\"]`],\n        isFatal:      true,\n      });\n    });\n  });\n\n  describe('pathManagementStrategy', () => {\n    beforeEach(() => {\n      modules.os.platform.mockReturnValue('linux');\n    });\n    describe('should accept valid settings', () => {\n      test.each(Object.keys(PathManagementStrategy))('%s', (strategy) => {\n        const value = PathManagementStrategy[strategy as keyof typeof PathManagementStrategy];\n        const [needToUpdate, errors] = subject.validateSettings({\n          ...cfg,\n          application: {\n            ...cfg.application,\n            pathManagementStrategy: PathManagementStrategy.Manual,\n          },\n        }, { application: { pathManagementStrategy: value } });\n\n        expect({ needToUpdate, errors }).toEqual({\n          needToUpdate: value !== PathManagementStrategy.Manual,\n          errors:       [],\n        });\n      });\n    });\n\n    it('should reject invalid values', () => {\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg,\n        { application: { pathManagementStrategy: 'invalid value' as PathManagementStrategy } });\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       [`Invalid value for \"application.pathManagementStrategy\": <\"invalid value\">; must be one of [\"manual\",\"rcfiles\"]`],\n        isFatal:      true,\n      });\n    });\n  });\n\n  describe('allowedImage lists', () => {\n    it('complains about a single duplicate', () => {\n      const input: RecursivePartial<settings.Settings> = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: ['pattern1', 'pattern2', 'pattern3', 'pattern2'],\n          },\n        },\n      };\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, input);\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       ['field \"containerEngine.allowedImages.patterns\" has duplicate entries: \"pattern2\"'],\n        isFatal:      false,\n      });\n    });\n    it('complains about multiple duplicates', () => {\n      const input: RecursivePartial<settings.Settings> = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: ['pattern1', 'Pattern2', 'pattern3', 'Pattern2', 'pattern1'],\n          },\n        },\n      };\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, input);\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       ['field \"containerEngine.allowedImages.patterns\" has duplicate entries: \"pattern1\", \"Pattern2\"'],\n        isFatal:      false,\n      });\n    });\n    it('complains about multiple duplicates that contain only whitespace lengths', () => {\n      const input: RecursivePartial<settings.Settings> = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: ['pattern1', '  ', 'pattern2', '\\t', 'pattern3', ''],\n          },\n        },\n      };\n      const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, input);\n\n      expect({ needToUpdate, errors, isFatal }).toEqual({\n        needToUpdate: false,\n        errors:       ['field \"containerEngine.allowedImages.patterns\" has duplicate entries: \"\", \"\\t\", \"  \"'],\n        isFatal:      false,\n      });\n    });\n    it('allows exactly one whitespace value', () => {\n      const input: RecursivePartial<settings.Settings> = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  true,\n            patterns: ['pattern1', 'pattern2', '\\t', 'pattern3'],\n          },\n        },\n      };\n      const [needToUpdate, errors] = subject.validateSettings(cfg, input);\n\n      expect({ needToUpdate, errors }).toEqual({\n        needToUpdate: true,\n        errors:       [],\n      });\n    });\n  });\n\n  describe('locked fields', () => {\n    describe('containerEngine.allowedImages', () => {\n      const allowedImageListConfig: settings.Settings = _.merge({}, cfg, {\n        containerEngine: {\n          allowedImages: {\n            enabled:  false,\n            patterns: ['pattern1', 'pattern2', 'pattern3'],\n          },\n        },\n      });\n\n      describe('when a field is locked', () => {\n        describe('locking allowedImages.enabled', () => {\n          const lockedSettings = { containerEngine: { allowedImages: { enabled: true } } };\n\n          it(\"can't be changed\", () => {\n            const input: RecursivePartial<settings.Settings> = { containerEngine: { allowedImages: { enabled: true } } };\n            const [needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n            expect({ needToUpdate, errors, isFatal }).toEqual({\n              needToUpdate: false,\n              errors:       ['field \"containerEngine.allowedImages.enabled\" is locked'],\n              isFatal:      true,\n            });\n          });\n          it('can be set to the same value', () => {\n            const currentEnabled = allowedImageListConfig.containerEngine.allowedImages.enabled;\n            const input: RecursivePartial<settings.Settings> = { containerEngine: { allowedImages: { enabled: currentEnabled } } };\n            const [needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n            expect({ needToUpdate, errors }).toEqual({\n              needToUpdate: false,\n              errors:       [],\n            });\n          });\n        });\n\n        describe('locking allowedImages.patterns', () => {\n          const lockedSettings = { containerEngine: { allowedImages: { patterns: true } } };\n\n          it(\"locked allowedImages:patterns-field can't be changed by adding a pattern\", () => {\n            const input: RecursivePartial<settings.Settings> = {\n              containerEngine: {\n                allowedImages: {\n                  patterns: allowedImageListConfig.containerEngine.allowedImages.patterns.concat('pattern4'),\n                },\n              },\n            };\n            const [needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n            expect({ needToUpdate, errors, isFatal }).toEqual({\n              needToUpdate: false,\n              errors:       ['field \"containerEngine.allowedImages.patterns\" is locked'],\n              isFatal:      true,\n            });\n          });\n\n          it(\"locked allowedImages:patterns-field can't be changed by removing a pattern\", () => {\n            const input: RecursivePartial<settings.Settings> = {\n              containerEngine: {\n                allowedImages: {\n                  patterns: allowedImageListConfig.containerEngine.allowedImages.patterns.slice(1),\n                },\n              },\n            };\n            const [needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n            expect({ needToUpdate, errors, isFatal }).toEqual({\n              needToUpdate: false,\n              errors:       ['field \"containerEngine.allowedImages.patterns\" is locked'],\n              isFatal:      true,\n            });\n          });\n\n          it('locked allowedImages:patterns-field can be set to the same value', () => {\n            const input: RecursivePartial<settings.Settings> = { containerEngine: { allowedImages: { patterns: allowedImageListConfig.containerEngine.allowedImages.patterns } } };\n            const [needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n            expect({ needToUpdate, errors }).toEqual({\n              needToUpdate: false,\n              errors:       [],\n            });\n          });\n        });\n      });\n    });\n\n    describe('checking locks', () => {\n      const ceSettings: RecursivePartial<settings.Settings> = {\n        containerEngine: {\n          allowedImages: {\n            enabled:  false,\n            patterns: ['pattern1', 'pattern2'],\n          },\n        },\n      };\n      const allowedImageListConfig: settings.Settings = _.merge({}, cfg, ceSettings);\n\n      describe('when unlocked', () => {\n        it('allows changes', () => {\n          const lockedSettings = { containerEngine: { allowedImages: { patterns: false } } };\n          let input: RecursivePartial<settings.Settings> = { containerEngine: { allowedImages: { enabled: true } } };\n          let [needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n          expect({ needToUpdate, errors }).toEqual({ needToUpdate: true, errors: [] });\n\n          input = { containerEngine: { allowedImages: { patterns: ['pattern1'] } } };\n          ([needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, input, lockedSettings));\n\n          expect({ needToUpdate, errors }).toEqual({\n            needToUpdate: true,\n            errors:       [],\n          });\n          input = { containerEngine: { allowedImages: { patterns: ['pattern1', 'pattern2', 'pattern3'] } } };\n          ([needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, input, lockedSettings));\n\n          expect({ needToUpdate, errors }).toEqual({\n            needToUpdate: true,\n            errors:       [],\n          });\n        });\n      });\n\n      describe('when locked', () => {\n        const lockedSettings = {\n          containerEngine: {\n            allowedImages: {\n              enabled:  true,\n              patterns: true,\n            },\n          },\n        };\n\n        it('disallows changes', () => {\n          const currentEnabled = allowedImageListConfig.containerEngine.allowedImages.enabled;\n          const currentPatterns = allowedImageListConfig.containerEngine.allowedImages.patterns;\n          let input: RecursivePartial<settings.Settings> = { containerEngine: { allowedImages: { enabled: !currentEnabled } } };\n          let [needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings);\n\n          expect({ needToUpdate, errors, isFatal }).toEqual({\n            needToUpdate: false,\n            errors:       ['field \"containerEngine.allowedImages.enabled\" is locked'],\n            isFatal:      true,\n          });\n\n          input = { containerEngine: { allowedImages: { patterns: ['picasso'].concat(currentPatterns) } } };\n          ([needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings));\n          expect({ needToUpdate, errors, isFatal }).toEqual({\n            needToUpdate: false,\n            errors:       ['field \"containerEngine.allowedImages.patterns\" is locked'],\n            isFatal:      true,\n          });\n\n          input = { containerEngine: { allowedImages: { patterns: currentPatterns.slice(1) } } };\n          ([needToUpdate, errors, isFatal] = subject.validateSettings(allowedImageListConfig, input, lockedSettings));\n\n          expect({ needToUpdate, errors, isFatal }).toEqual({\n            needToUpdate: false,\n            errors:       ['field \"containerEngine.allowedImages.patterns\" is locked'],\n            isFatal:      true,\n          });\n        });\n\n        it(\"doesn't complain when no locked fields change\", () => {\n          const [needToUpdate, errors] = subject.validateSettings(allowedImageListConfig, ceSettings, lockedSettings);\n\n          expect({ needToUpdate, errors }).toEqual({\n            needToUpdate: false,\n            errors:       [],\n          });\n        });\n      });\n    });\n  });\n\n  describe('application.extensions.installed', () => {\n    test('should accept already-invalid input', () => {\n      const changes = { application: { extensions: { installed: { '!invalid name!': '@invalid tag@' } } } };\n      const input = _.merge({}, cfg, changes);\n      const [changed, errors] = subject.validateSettings(input, changes);\n\n      expect({ changed, errors }).toEqual({ changed: false, errors: [] });\n    });\n\n    const longString = new Array(255).join('x');\n\n    test.each<[string, any, string[]]>([\n      ['should reject non-dict values', 123, ['application.extensions.installed: \"123\" is not a valid mapping']],\n      ['should reject non-string values', { a: 1 }, ['application.extensions.installed: \"a\" has non-string tag \"1\"']],\n      ['should reject invalid names', { '!!@': 'latest' }, ['application.extensions.installed: \"!!@\" is an invalid name']],\n      ['should accept names with a bare component', { image: 'tag' }, []],\n      ['should accept names with a domain', { 'registry.test/name': 'tag' }, []],\n      ['should accept names with multiple components', { 'registry.test/dir/name': 'tag' }, []],\n      ['should reject invalid tags', { image: 'hello world' }, ['application.extensions.installed: \"image\" has invalid tag \"hello world\"']],\n      ['should reject overly-long tags', { image: longString }, [`application.extensions.installed: \"image\" has invalid tag \"${ longString }\"`]],\n    ])('%s', (...[, input, expectedErrors]) => {\n      const [, errors] = subject.validateSettings(cfg, { application: { extensions: { installed: input } } });\n\n      expect(errors).toEqual(expectedErrors);\n    });\n  });\n\n  describe('experimental.virtual-machine.disk-size', () => {\n    describe('parsing inputs', () => {\n      test.each<['accept' | 'reject', string]>([\n        ['accept', '100GiB'], // default value\n        ['accept', '1.23GiB'], // with fractional number\n        ['reject', '1.GiB'], // Trailing dot on number\n        ['accept', '100 GiB'],  // with space between number and unit\n        ['accept', '1.23 GiB'], // fractional number with space\n        ['accept', '1234'], // no units\n        ['accept', '123k'], // kilobytes, no byte suffix\n        ['accept', '123mb'], // megabytes, with b suffix\n        ['accept', '1234 tib'], // terabytes (spellcheck-ignore-line)\n        ['reject', '1234 stuff'], // extra trailing text\n      ])('should %s %s', (outcome, input) => {\n        const errorMessage = `Invalid value for \"experimental.virtualMachine.diskSize\": <${ JSON.stringify(input) }>`;\n        const expectedErrors = outcome === 'accept' ? [] : [errorMessage];\n        const oldConfig = _.set(_.cloneDeep(cfg), 'experimental.virtualMachine.diskSize', 1);\n        const [, errors] = subject.validateSettings(oldConfig, { experimental: { virtualMachine: { diskSize: input } } });\n\n        expect(errors).toEqual(expectedErrors);\n      });\n    });\n    describe('rejecting smaller values', () => {\n      test.each<['accept' | 'reject', string, string]>([\n        ['accept', '100GiB', '100GiB'],\n        ['accept', '100GiB', '1TB'],\n        ['reject', '1G', '100M'],\n      ])('should %s going from %s to %s', (outcome, currentValue, desiredValue) => {\n        const errorMessage = `Cannot decrease \"experimental.virtualMachine.diskSize\" from ${ currentValue } to ${ desiredValue }`;\n        const expectedErrors = outcome === 'accept' ? [] : [errorMessage];\n        const oldConfig = _.set(_.cloneDeep(cfg), 'experimental.virtualMachine.diskSize', currentValue);\n        const [, errors] = subject.validateSettings(oldConfig, { experimental: { virtualMachine: { diskSize: desiredValue } } });\n\n        expect(errors).toEqual(expectedErrors);\n      });\n    });\n  });\n\n  it('should complain about unchangeable fields', () => {\n    const unchangeableFieldsAndValues = { version: settings.CURRENT_SETTINGS_VERSION + 1 };\n\n    // Check that we _don't_ ask for update when we have errors.\n    const input = { application: { telemetry: { enabled: !cfg.application.telemetry.enabled } } };\n\n    for (const [path, value] of Object.entries(unchangeableFieldsAndValues)) {\n      _.set(input, path, value);\n    }\n\n    const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, input);\n\n    expect({ needToUpdate, errors, isFatal }).toEqual({\n      needToUpdate: false,\n      errors:       Object.keys(unchangeableFieldsAndValues).map(key => `Changing field \"${ key }\" via the API isn't supported.`),\n      isFatal:      false,\n    });\n  });\n\n  it('complains about mismatches between objects and scalars', () => {\n    let [needToUpdate, errors] = subject.validateSettings(cfg, { kubernetes: 5 as unknown as Record<string, number> });\n\n    expect(needToUpdate).toBeFalsy();\n    expect(errors).toHaveLength(1);\n    expect(errors[0]).toContain('Setting \"kubernetes\" should wrap an inner object, but got <5>');\n\n    [needToUpdate, errors] = subject.validateSettings(cfg, {\n      containerEngine: { name: { expected: 'a string' } as unknown as settings.ContainerEngine },\n      kubernetes:      {\n        version: { expected: 'a string' } as unknown as string,\n        options: \"ceci n'est pas un objet\" as unknown as Record<string, boolean>,\n        enabled: true,\n      },\n    });\n    expect(needToUpdate).toBeFalsy();\n    expect(errors).toHaveLength(3);\n    expect(errors).toEqual([\n      `Invalid value for \"containerEngine.name\": <{\"expected\":\"a string\"}>; must be one of [\"containerd\",\"moby\",\"docker\"]`,\n      'Kubernetes version \"[object Object]\" not found.',\n      `Setting \"kubernetes.options\" should wrap an inner object, but got <ceci n'est pas un objet>.`,\n    ]);\n  });\n\n  // Add some fields that are very unlikely to ever collide with newly introduced fields.\n  it('should ignore unrecognized settings', () => {\n    const [needToUpdate, errors, isFatal] = subject.validateSettings(cfg, {\n      kubernetes: {\n        'durian-sharkanodo': 3,\n        version:             cfg.version,\n        'jackfruit otto':    12,\n        options:             {\n          'pitaya*paprika': false,\n          traefik:          cfg.kubernetes.options.traefik,\n        },\n        enabled: true,\n      },\n      portForwarding: {\n        'kiwano // 8 1/2':         'cows',\n        includeKubernetesServices: cfg.portForwarding.includeKubernetesServices,\n      },\n      'feijoa - Alps': [],\n    } as unknown as settings.Settings);\n\n    expect({ needToUpdate, errors, isFatal }).toEqual({\n      needToUpdate: false,\n      errors:       [expect.anything()],\n      isFatal:      false,\n    });\n  });\n\n  it('should allow empty Kubernetes version when Kubernetes is disabled', () => {\n    const [needToUpdate, errors] = subject.validateSettings(\n      cfg,\n      {\n        kubernetes: {\n          version: '',\n          enabled: false,\n        },\n      });\n\n    expect(needToUpdate).toBeTruthy();\n    expect(errors).toHaveLength(0);\n    expect(errors).toEqual([]);\n  });\n\n  it('should disallow empty Kubernetes version when Kubernetes is enabled', () => {\n    const [needToUpdate, errors] = subject.validateSettings(\n      cfg,\n      {\n        kubernetes: {\n          version: '',\n          enabled: true,\n        },\n      });\n\n    expect(needToUpdate).toBeFalsy();\n    expect(errors).toHaveLength(1);\n    expect(errors).toEqual([\n      'Kubernetes version \"\" not found.',\n    ]);\n  });\n\n  describe('virtualMachine.type', () => {\n    beforeEach(() => {\n      modules.os.platform.mockReturnValue('darwin');\n    });\n\n    afterEach(() => {\n      modules.os.arch.mockRestore();\n      modules['@pkg/utils/osVersion'].getMacOsVersion.mockRestore();\n    });\n\n    function checkForError(needToUpdate: boolean, errors: string[], errorMessage: string) {\n      expect(needToUpdate).toBeFalsy();\n      expect(errors).toHaveLength(1);\n      expect(errors).toEqual([\n        errorMessage,\n      ]);\n    }\n\n    function getVMTypeSetting(vmType: VMType): RecursivePartial<settings.Settings> {\n      return {\n        virtualMachine: {\n          type: vmType,\n        },\n      };\n    }\n\n    function getMountTypeSetting(mountType: MountType): RecursivePartial<settings.Settings> {\n      return {\n        virtualMachine: {\n          mount: {\n            type: mountType,\n          },\n        },\n      };\n    }\n\n    it('should reject VZ if architecture is arm and macOS version < 13.3.0', () => {\n      modules.os.arch.mockReturnValue('arm64');\n      modules['@pkg/utils/osVersion'].getMacOsVersion.mockReturnValue(new SemVer('13.2.0'));\n      const [needToUpdate, errors] = subject.validateSettings(\n        cfg, getVMTypeSetting(VMType.VZ));\n\n      checkForError(\n        needToUpdate, errors,\n        'Setting virtualMachine.type to \\\"vz\\\" on ARM requires macOS 13.3 (Ventura) or later.',\n      );\n    });\n\n    it('should reject VZ if architecture is Intel macOS version < 13.0.0', () => {\n      modules.os.arch.mockReturnValue('x64');\n      modules['@pkg/utils/osVersion'].getMacOsVersion.mockReturnValue(new SemVer('12.0.0'));\n      const [needToUpdate, errors] = subject.validateSettings(\n        cfg, getVMTypeSetting(VMType.VZ));\n\n      checkForError(\n        needToUpdate, errors,\n        'Setting virtualMachine.type to \\\"vz\\\" on Intel requires macOS 13.0 (Ventura) or later.',\n      );\n    });\n\n    it('should reject VZ if mount type is 9p', () => {\n      modules['@pkg/utils/osVersion'].getMacOsVersion.mockReturnValue(new SemVer('13.3.0'));\n      const [needToUpdate, errors] = subject.validateSettings(\n        _.merge({}, cfg, getMountTypeSetting(MountType.NINEP)), getVMTypeSetting(VMType.VZ));\n\n      checkForError(\n        needToUpdate, errors,\n        'Setting virtualMachine.type to \\\"vz\\\" requires that ' +\n        'virtual-machine.mount.type is \\\"reverse-sshfs\\\" or \\\"virtiofs\\\".',\n      );\n    });\n\n    it('should reject QEMU if mount type is virtiofs on macOS', () => {\n      const [needToUpdate, errors] = subject.validateSettings(\n        _.merge({}, cfg, getMountTypeSetting(MountType.VIRTIOFS)), getVMTypeSetting(VMType.QEMU));\n\n      checkForError(\n        needToUpdate, errors,\n        'Setting virtualMachine.type to \\\"qemu\\\" requires that ' +\n        'virtual-machine.mount.type is \\\"reverse-sshfs\\\" or \\\"9p\\\".',\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/commandServer/httpCommandServer.ts",
    "content": "import fs from 'fs';\nimport http from 'http';\nimport path from 'path';\nimport { URL } from 'url';\n\nimport express from 'express';\nimport _ from 'lodash';\n\nimport { State } from '@pkg/backend/backend';\nimport type { Settings } from '@pkg/config/settings';\nimport type { TransientSettings } from '@pkg/config/transientSettings';\nimport type { DiagnosticsResultCollection } from '@pkg/main/diagnostics/diagnostics';\nimport { ExtensionMetadata } from '@pkg/main/extensions/types';\nimport mainEvents from '@pkg/main/mainEvents';\nimport * as serverHelper from '@pkg/main/serverHelper';\nimport { Snapshot } from '@pkg/main/snapshots/types';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\n/**\n * Represents the current or desired state of the backend/main process.\n */\nexport interface BackendState {\n  // The state of the VM/backend.\n  vmState: State,\n  // Whether the backend is locked. If true, changes cannot\n  // be made by the user until it is unlocked.\n  locked:  boolean,\n}\n\nexport interface ServerState {\n  user:     string;\n  password: string;\n  port:     number;\n  pid:      number;\n}\n\ntype DispatchFunctionType = (request: express.Request, response: express.Response, context: commandContext) => Promise<void>;\ntype HttpMethod = 'get' | 'put' | 'post';\n\nconst console = Logging.server;\nconst SERVER_PORT = 6107;\nconst SERVER_FILE_BASENAME = 'rd-engine.json';\nconst MAX_REQUEST_BODY_LENGTH = 4194304; // 4MiB\n\nexport class HttpCommandServer {\n  protected server = http.createServer();\n  protected app = express();\n  protected readonly externalState: ServerState = {\n    user:     'user',\n    password: serverHelper.randomStr(),\n    port:     SERVER_PORT,\n    pid:      process.pid,\n  };\n\n  protected readonly interactiveState: ServerState = {\n    user:     'interactive-user',\n    password: serverHelper.randomStr(),\n    port:     SERVER_PORT,\n    pid:      process.pid,\n  };\n\n  protected commandWorker: CommandWorkerInterface;\n\n  protected dispatchTable: Record<HttpMethod, Record<string, readonly [number, DispatchFunctionType]>> = _.merge(\n    {\n      get: {\n        '/v1/about':                 [1, this.about],\n        '/v1/diagnostic_categories': [0, this.diagnosticCategories],\n        '/v1/diagnostic_ids':        [0, this.diagnosticIDsForCategory],\n        '/v1/diagnostic_checks':     [0, this.diagnosticChecks],\n        '/v1/settings':              [0, this.listSettings],\n        '/v1/settings/locked':       [0, this.listLockedSettings],\n        '/v1/transient_settings':    [0, this.listTransientSettings],\n        '/v1/backend_state':         [1, this.getBackendState],\n      },\n      post: { '/v1/diagnostic_checks': [0, this.diagnosticRunChecks] },\n      put:  {\n        '/v1/factory_reset':      [0, this.factoryReset],\n        '/v1/k8s_reset':          [0, this.k8sReset],\n        '/v1/propose_settings':   [0, this.proposeSettings],\n        '/v1/settings':           [0, this.updateSettings],\n        '/v1/shutdown':           [0, this.wrapShutdown],\n        '/v1/transient_settings': [0, this.updateTransientSettings],\n        '/v1/backend_state':      [1, this.setBackendState],\n      },\n    } as const,\n    {\n      get:  { '/v1/extensions': [1, this.listExtensions] },\n      post: {\n        '/v1/extensions/install':   [1, this.installExtension],\n        '/v1/extensions/uninstall': [1, this.uninstallExtension],\n      },\n    } as const,\n    {\n      get:  { '/v1/snapshots': [0, this.listSnapshots] },\n      post: {\n        '/v1/snapshots':        [0, this.createSnapshot],\n        '/v1/snapshot/restore': [0, this.restoreSnapshot],\n        '/v1/snapshots/cancel': [0, this.cancelSnapshot],\n      },\n      delete: { '/v1/snapshots': [0, this.deleteSnapshot] },\n    } as const,\n    {\n      post:   { '/v1/port_forwarding': [1, this.createPortForwarding] },\n      delete: { '/v1/port_forwarding': [1, this.deletePortForwarding] },\n    } as const,\n  );\n\n  constructor(commandWorker: CommandWorkerInterface) {\n    this.commandWorker = commandWorker;\n    mainEvents.handle('api-get-credentials', () => Promise.resolve(this.interactiveState));\n  }\n\n  async init() {\n    const localHost = '127.0.0.1';\n    const statePath = path.join(paths.appHome, SERVER_FILE_BASENAME);\n\n    await fs.promises.mkdir(paths.appHome, { recursive: true });\n    await fs.promises.writeFile(statePath,\n      jsonStringifyWithWhiteSpace(this.externalState),\n      { mode: 0o600 });\n\n    this.server = this.app\n      .disable('etag')\n      .disable('x-powered-by')\n      .use(this.handleCORS)\n      .use(this.checkAuth)\n      .listen(SERVER_PORT, localHost)\n      .on('error', (err) => {\n        console.log(`Error: ${ err }`);\n      });\n\n    this.setupRoutes();\n    console.log('CLI server is now ready.');\n  }\n\n  /**\n   * Set up HTTP routes for express.\n   * This takes the information from the route decorators and applies it to the\n   * express application, handling the extra routes for backwards compatibility\n   * and API listings.\n   */\n  protected setupRoutes() {\n    let maxVersion = 0;\n\n    for (const [untypedMethod, data] of Object.entries(this.dispatchTable)) {\n      const method = untypedMethod as HttpMethod;\n\n      for (const [route, [since, handler]] of Object.entries(data)) {\n        const [, versionString, path] = /^\\/v(\\d+)\\/(.*)$/.exec(route) ?? [];\n        const version = parseInt(versionString || '0', 10);\n\n        if (!versionString || !path) {\n          throw new Error(`Could not parse HTTP route ${ route }`);\n        }\n        maxVersion = Math.max(version, maxVersion);\n\n        this.app[method](`/v${ version }/${ path }`, (req, resp, next) => {\n          const context: commandContext = { interactive: resp.locals.interactive };\n\n          handler.call(this, req, resp, context).catch(next);\n        });\n\n        // Add routes for older API versions\n        for (let oldVersion = since; oldVersion < version; ++oldVersion) {\n          this.app[method](`/v${ oldVersion }/${ path }`, (req, resp, next) => {\n            this.invalidAPIVersionCall(version, req, resp).catch(next);\n          });\n        }\n      }\n    }\n\n    // Add versioned endpoints that list API endpoints\n    for (let listVersion = 0; listVersion <= maxVersion; ++listVersion) {\n      this.app.get(`/v${ listVersion }`, (req, resp) => {\n        this.listEndpoints(listVersion.toString(), req, resp);\n      });\n    }\n\n    this.app.get('/', (req, resp) => {\n      this.listEndpoints('', req, resp);\n    });\n    // Set up catch-all handler for customized HTTP 404 message.\n    this.app.all('*missing', ({ method, path }, resp) => {\n      console.log(`404: No handler for URL ${ method } ${ path }.`);\n      resp.status(404).type('txt').send(`Unknown command: ${ method } ${ path }`);\n    });\n\n    // The error handler must be set after everything else.\n    this.app.use(this.handleError.bind(this));\n  }\n\n  /** checkAuth is middleware to verify authentication. */\n  protected checkAuth = (request: express.Request, response: express.Response, next: express.NextFunction) => {\n    const authHeader = request.headers.authorization ?? '';\n    const userDB = {\n      [this.externalState.user]:    this.externalState.password,\n      [this.interactiveState.user]: this.interactiveState.password,\n    };\n\n    switch (serverHelper.basicAuth(userDB, authHeader)) {\n    case this.externalState.user:\n      response.locals.interactive = false;\n      break;\n    case this.interactiveState.user:\n      response.locals.interactive = true;\n      break;\n    default:\n      response.type('txt').sendStatus(401);\n\n      return;\n    }\n    next();\n  };\n\n  /**\n   * Calculate the headers needed for CORS, and set them on the response.\n   */\n  protected handleCORS(request: express.Request, response: express.Response, next: express.NextFunction): void {\n    response.set({\n      'Access-Control-Allow-Headers': 'Authorization',\n      'Access-Control-Allow-Methods': 'GET, PUT, DELETE',\n      'Access-Control-Allow-Origin':  '*',\n    });\n\n    if (request.method === 'OPTIONS') {\n      response.sendStatus(204);\n    } else {\n      next();\n    }\n  }\n\n  /**\n   * handleError is middleware to handle unexpected errors, logging the error to\n   * the log file and returning a simpler HTTP internal server error response.\n   */\n  protected handleError(err: Error, request: express.Request, response: express.Response, next: express.NextFunction): void {\n    if (!err) {\n      next();\n    }\n\n    console.log(`Error handling ${ request.path }`, err);\n    response.type('txt').sendStatus(500);\n  }\n\n  protected diagnosticCategories(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const categories = this.commandWorker.getDiagnosticCategories(context);\n\n    if (categories) {\n      console.debug('diagnosticCategories: succeeded 200');\n      response.type('json').status(200)\n        .send(jsonStringifyWithWhiteSpace(categories));\n    } else {\n      console.debug('diagnosticCategories: failed 404');\n      response.type('text').status(404)\n        .send('No diagnostic categories found');\n    }\n\n    return Promise.resolve();\n  }\n\n  protected diagnosticIDsForCategory(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const url = new URL(`http://${ request.url }`);\n    const searchParams = url.searchParams;\n    const category = searchParams.get('category');\n\n    if (!category) {\n      console.debug('diagnostic_ids: failed 400');\n      response.type('txt').status(400)\n        .send('diagnostic_ids: no category specified');\n\n      return Promise.resolve();\n    }\n    const checkIDs = this.commandWorker.getDiagnosticIdsByCategory(category, context);\n\n    if (checkIDs) {\n      console.debug('diagnostic_ids: succeeded 200');\n      response.type('json').status(200)\n        .send(jsonStringifyWithWhiteSpace(checkIDs));\n    } else {\n      console.debug('diagnostic_ids: failed 404');\n      response.type('txt').status(404)\n        .send(`No diagnostic checks found in category ${ category }`);\n    }\n\n    return Promise.resolve();\n  }\n\n  protected async diagnosticChecks(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const url = new URL(`http://localhost/${ request.url }`);\n    const searchParams = url.searchParams;\n    const category = searchParams.get('category');\n    const id = searchParams.get('id');\n    const checks = await this.commandWorker.getDiagnosticChecks(category, id, context);\n\n    console.debug('diagnostic_checks: succeeded 200');\n    response.type('json').status(200)\n      .send(jsonStringifyWithWhiteSpace(checks));\n  }\n\n  protected async diagnosticRunChecks(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const results = await this.commandWorker.runDiagnosticChecks(context);\n\n    console.debug('diagnostic_run: succeeded 200');\n    response.status(200).type('json')\n      .send(jsonStringifyWithWhiteSpace(results));\n  }\n\n  protected invalidAPIVersionCall(neededVersion: number, request: express.Request, response: express.Response): Promise<void> {\n    const method = request.method;\n    const path = request.path;\n    const pathParts = path.split('/');\n\n    const msg = `Invalid version \"/${ pathParts[1] }\" for endpoint \"${ method } ${ path }\" - use \"/v${ neededVersion }/${ pathParts.slice(2).join('/') }\"`;\n\n    console.log(`Error handling ${ request.url }`, msg);\n    response.status(400).type('txt').send(msg);\n\n    return Promise.resolve();\n  }\n\n  protected about(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const msg = 'The API is currently at version 1, but is still considered internal and experimental, and is subject to change without any advance notice.';\n\n    console.debug('about: succeeded 200');\n    response.status(200).type('txt').send(msg);\n\n    return Promise.resolve();\n  }\n\n  protected listSettings(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const settings = this.commandWorker.getSettings(context);\n\n    if (settings) {\n      console.debug('listSettings: succeeded 200');\n      response.status(200).type('txt').send(settings);\n    } else {\n      console.debug('listSettings: failed 200');\n      response.status(404).type('txt').send('No settings found');\n    }\n\n    return Promise.resolve();\n  }\n\n  protected listLockedSettings(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const settings = this.commandWorker.getLockedSettings(context);\n\n    if (settings) {\n      console.debug('listLockedSettings: succeeded 200');\n      response.status(200).type('txt').send(settings);\n    } else {\n      console.debug('listLockedSettings: failed 404');\n      response.status(404).type('txt').send('No locked settings found');\n    }\n\n    return Promise.resolve();\n  }\n\n  protected listEndpoints(version: string, request: express.Request, response: express.Response): Promise<void> {\n    // Determine all API paths, possibly filtered by the requested version.\n    const apiPaths: [Uppercase<HttpMethod>, string][] = [];\n    let maxVersion = 0;\n\n    for (const [method, data] of Object.entries(this.dispatchTable)) {\n      for (const route of Object.keys(data)) {\n        const [, commandVersion] = /\\/v(\\d+)\\//.exec(route) ?? [];\n\n        maxVersion = Math.max(parseInt(commandVersion, 10), maxVersion);\n        if (version && version !== commandVersion) {\n          continue;\n        }\n\n        apiPaths.push([method.toUpperCase() as Uppercase<HttpMethod>, route]);\n      }\n    }\n\n    if (version) {\n      // If version given, ensure the version endpoint itself is listed.\n      apiPaths.push(['GET', `/v${ version }`]);\n    } else {\n      // If no version is given, provide the unversioned API to list APIs.\n      apiPaths.push(['GET', '/']);\n      for (let listVersion = 0; listVersion <= maxVersion; ++listVersion) {\n        apiPaths.push(['GET', `/v${ listVersion }`]);\n      }\n    }\n\n    this.sortFavoringGetMethod(apiPaths);\n    console.debug('listEndpoints: succeeded 200');\n    response.status(200).type('json')\n      .send(JSON.stringify(apiPaths.map(entry => entry.join(' '))));\n\n    return Promise.resolve();\n  }\n\n  protected sortFavoringGetMethod(returnedPaths: [Uppercase<HttpMethod>, string][]) {\n    returnedPaths.sort(([methodA, pathA], [methodB, pathB]) => {\n      if (pathA === pathB) {\n        if (methodA === 'GET') {\n          return methodB === 'GET' ? 0 : -1;\n        } else if (methodB === 'GET') {\n          return 1;\n        } else {\n          return methodA.localeCompare(methodB);\n        }\n      }\n\n      return pathA.localeCompare(pathB);\n    });\n  }\n\n  protected async readRequestSettings<T>(\n    request: express.Request,\n    functionName: string,\n  ): Promise<[number, string] | RecursivePartial<T>> {\n    const [data, payloadError, payloadErrorCode] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n\n    if (payloadError) {\n      return [payloadErrorCode, payloadError];\n    }\n\n    if (data.length === 0) {\n      return [400, 'no settings specified in the request'];\n    }\n\n    try {\n      const result = JSON.parse(data) ?? {};\n\n      if (typeof result !== 'object') {\n        return [400, 'settings payload is not an object'];\n      }\n\n      return result;\n    } catch (err) {\n      // TODO: Revisit this log stmt if sensitive values (e.g. PII, IPs, creds) can be provided via this command\n      console.log(`${ functionName }: error processing JSON request block\\n${ data }\\n`, err);\n\n      return [400, 'error processing JSON request block'];\n    }\n  }\n\n  /**\n   * Handle `PUT /v?/settings` requests.\n   * Like the other methods, this method creates the request (here by reading the request body),\n   * submits it to the provided CommandWorker, and writes back the appropriate status code\n   * and data to the response object.\n   *\n   * The incoming payload is expected to be a subset of the settings.Settings object\n   */\n  async updateSettings(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    let error: string;\n    let errorCode = 400;\n    let result = '';\n    const body = await this.readRequestSettings(request, 'updateSettings');\n\n    if (Array.isArray(body)) {\n      [errorCode, error] = body;\n    } else {\n      try {\n        [result, error] = await this.commandWorker.updateSettings(context, body);\n      } catch (ex) {\n        console.error(`updateSettings: exception when updating:`, ex);\n        errorCode = 500;\n        error = 'internal error';\n      }\n    }\n\n    if (error) {\n      console.debug(`updateSettings: write back status ${ errorCode }, error: ${ error }`);\n      response.status(errorCode).type('txt').send(error);\n    } else {\n      console.debug(`updateSettings: write back status 202, result: ${ result }`);\n      response.status(202).type('txt').send(result);\n    }\n  }\n\n  async proposeSettings(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    let error: string;\n    let errorCode = 400;\n    let result = '';\n    const body = await this.readRequestSettings<Settings>(request, 'updateSettings');\n\n    try {\n      if (Array.isArray(body)) {\n        [errorCode, error] = body;\n      } else {\n        [result, error] = await this.commandWorker.proposeSettings(context, body);\n        console.error(`propose: ${ JSON.stringify(body) } -> ${ result }`);\n      }\n    } catch (ex) {\n      console.error('proposedSettings: internal error:', ex);\n      errorCode = 500;\n      error = 'internal error';\n    }\n    if (error) {\n      console.error(`proposeSettings: write back status ${ errorCode }, error: ${ error }`);\n      response.status(errorCode).type('txt').send(error);\n    } else {\n      console.error(`proposeSettings: write back status 200, result: ${ result }`);\n      response.status(200).type('json').send(result);\n    }\n  }\n\n  async factoryReset(request: express.Request, response: express.Response, _: commandContext): Promise<void> {\n    const [data, payloadError] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n    let error = '';\n    let keepSystemImages = false;\n\n    if (!payloadError) {\n      try {\n        console.debug(`Request data: ${ data }`);\n        const values: Record<string, any> = JSON.parse(data);\n        if ('keepSystemImages' in values) {\n          keepSystemImages = values.keepSystemImages;\n        }\n      } catch (err) {\n        // TODO: Revisit this log stmt if sensitive values (e.g. PII, IPs, creds) can be provided via this command\n        console.log(`updateSettings: error processing JSON request block\\n${ data }\\n`, err);\n        error = 'error processing JSON request block';\n      }\n    } else {\n      error = payloadError;\n    }\n    if (!error) {\n      console.debug('factory reset: succeeded 202');\n      response.status(202).type('txt').send('Doing a full factory reset....');\n      setImmediate(() => {\n        this.closeServer();\n        this.commandWorker.factoryReset(keepSystemImages);\n      });\n    } else {\n      console.debug(`factoryReset: write back status 400, error: ${ error }`);\n      response.status(400).type('txt').send(error);\n    }\n  }\n\n  async k8sReset(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const [data, payloadError] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n    let error = '';\n    let mode: 'fast' | 'wipe' = 'fast';\n\n    if (!payloadError) {\n      try {\n        console.debug(`Request data: ${ data }`);\n        const values: Record<string, any> = JSON.parse(data);\n        if ('mode' in values && typeof values.mode === 'string' && (values.mode === 'fast' || values.mode === 'wipe')) {\n          mode = values.mode;\n        }\n      } catch (err) {\n        console.log(`k8sReset: error processing JSON request block\\n${ data }\\n`, err);\n        error = 'error processing JSON request block';\n      }\n    } else {\n      error = payloadError;\n    }\n    if (!error) {\n      await this.commandWorker.k8sReset(context, mode);\n      console.debug(`k8sReset: ${ mode } succeeded`);\n      response.status(200).type('txt').send(`Rancher Desktop ${ mode } reset successful`);\n    } else {\n      console.debug(`k8sReset: write back status 400, error: ${ error }`);\n      response.status(400).type('txt').send(error);\n    }\n  }\n\n  protected async createPortForwarding(request: express.Request, response: express.Response, _: commandContext): Promise<void> {\n    const [data, payloadError] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n    let error = '';\n    let namespace = '';\n    let service = '';\n    let k8sPort: string | number = 0;\n    let hostPort = 0;\n\n    if (!payloadError) {\n      try {\n        console.debug(`Request data: ${ data }`);\n        const values: Record<string, any> = JSON.parse(data);\n        if ('namespace' in values && 'service' in values && 'k8sPort' in values && 'hostPort' in values) {\n          namespace = values.namespace;\n\n          service = values.service;\n\n          if (Number.isNaN(values.k8sPort)) {\n            k8sPort = values.k8sPort;\n          } else {\n            k8sPort = parseInt(values.k8sPort, 10);\n          }\n\n          hostPort = values.hostPort;\n        } else {\n          error = 'missing required parameters';\n        }\n      } catch (err) {\n        // TODO: Revisit this log stmt if sensitive values (e.g. PII, IPs, creds) can be provided via this command\n        console.log(`updateSettings: error processing JSON request block\\n${ data }\\n`, err);\n        error = 'error processing JSON request block';\n      }\n    } else {\n      error = payloadError;\n    }\n    if (!error) {\n      try {\n        const result = await this.commandWorker.forwardPort(namespace, service, k8sPort, hostPort);\n\n        if (typeof result === 'number') {\n          console.debug('createPortForwarding: succeeded 200');\n          response.status(200).type('txt').send(`${ result }`);\n        } else {\n          console.debug(`createPortForwarding: write back status 400, error forwarding port`);\n          response.status(400).type('txt').send('Could not forward port');\n        }\n      } catch (err: any) {\n        console.error(`createPortForwarding: error forwarding port:`, err);\n        response.status(400).type('txt').send(`Could not forward port; error code: ${ typeof err.code === 'string' ? err.code : 'unknown, check the logs' }`);\n      }\n    } else {\n      console.debug(`createPortForwarding: write back status 400, error: ${ error }`);\n      response.status(400).type('txt').send(error);\n    }\n  }\n\n  protected async deletePortForwarding(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const namespace = request.query.namespace ?? '';\n    const service = request.query.service ?? '';\n    const k8sPort = request.query.k8sPort ?? '';\n\n    if (!namespace) {\n      response.status(400).type('txt').send('Port forwarding namespace is required in query parameters');\n    } else if (!service) {\n      response.status(400).type('txt').send('Port forwarding service is required in query parameters');\n    } else if (!k8sPort) {\n      response.status(400).type('txt').send('Port forwarding k8sPort is required in query parameters');\n    } else if (typeof namespace !== 'string') {\n      response.status(400).type('txt').send(`Invalid port forwarding namespace ${ JSON.stringify(namespace) }: not a string.`);\n    } else if (typeof service !== 'string') {\n      response.status(400).type('txt').send(`Invalid port forwarding service ${ JSON.stringify(service) }: not a string.`);\n    } else if (typeof k8sPort !== 'string') {\n      response.status(400).type('txt').send(`Invalid port forwarding k8sPort ${ JSON.stringify(k8sPort) }: not a string.`);\n    } else {\n      const k8sPortResolved = Number.isNaN(k8sPort) ? k8sPort : parseInt(k8sPort, 10);\n\n      try {\n        await this.commandWorker.cancelForward(namespace, service, k8sPortResolved);\n\n        console.debug('deletePortForwarding: succeeded 200');\n        response.status(200).type('txt').send('Port forwarding successfully deleted');\n      } catch (error: any) {\n        console.error(`deletePortForwarding: error deleting port forwarding:`, error);\n        response.status(400).type('txt').send('Could not delete port forwarding');\n      }\n    }\n  }\n\n  wrapShutdown(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    console.debug('shutdown: succeeded 202');\n    response.status(202).type('txt').send('Shutting down.');\n    setImmediate(() => {\n      this.closeServer();\n      this.commandWorker.requestShutdown(context);\n    });\n\n    return Promise.resolve();\n  }\n\n  closeServer() {\n    this.server.close();\n  }\n\n  protected listTransientSettings(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const transientSettings = this.commandWorker.getTransientSettings(context);\n\n    response.status(200).type('json').send(transientSettings);\n\n    return Promise.resolve();\n  }\n\n  protected async updateTransientSettings(\n    request: express.Request,\n    response: express.Response,\n    context: commandContext,\n  ): Promise<void> {\n    let error: string;\n    let errorCode = 400;\n    let result = '';\n    const body = await this.readRequestSettings<TransientSettings>(request, 'updateTransientSettings');\n\n    if (Array.isArray(body)) {\n      [errorCode, error] = body;\n    } else {\n      try {\n        [result, error] = await this.commandWorker.updateTransientSettings(context, body);\n      } catch (ex) {\n        console.error(`updateTransientSettings: exception when updating:`, ex);\n        errorCode = 500;\n        error = 'internal error';\n      }\n    }\n\n    if (error) {\n      console.debug(`updateTransientSettings: write back status ${ errorCode }, error: ${ error }`);\n      response.status(errorCode).type('txt').send(error);\n    } else {\n      console.debug(`updateTransientSettings: write back status 202, result: ${ result }`);\n      response.status(202).type('txt').send(result);\n    }\n  }\n\n  protected async listExtensions(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const extensions = await this.commandWorker.listExtensions();\n\n    if (!extensions) {\n      response.status(503).type('txt').send('Extension manager is not ready yet.');\n    } else {\n      response.status(200).type('json').send(extensions);\n    }\n  }\n\n  protected async installExtension(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const id = request.query.id ?? '';\n\n    if (!id) {\n      response.status(400).type('txt').send('Extension ID is required in the id= parameter.');\n    } else if (typeof id !== 'string') {\n      response.status(400).type('txt').send(`Invalid extension id ${ JSON.stringify(id) }: not a string.`);\n    } else {\n      response.writeProcessing();\n      const { status, data } = await this.commandWorker.installExtension(id, 'install');\n\n      if (data) {\n        if (typeof data === 'string') {\n          response.status(status).type('txt').send(data);\n        } else {\n          response.status(status).type('json').send(data);\n        }\n      } else {\n        response.sendStatus(status);\n      }\n    }\n  }\n\n  protected async uninstallExtension(request: express.Request, response: express.Response): Promise<void> {\n    const id = request.query.id ?? '';\n\n    if (!id) {\n      response.status(400).type('txt').send('Extension ID is required in the id= parameter.');\n    } else if (typeof id !== 'string') {\n      response.status(400).type('txt').send(`Invalid extension id ${ JSON.stringify(id) }: not a string.`);\n    } else {\n      response.writeProcessing();\n      const { status, data: rawData } = await this.commandWorker.installExtension(id, 'uninstall');\n      const data = rawData || `Deleted ${ id }`;\n\n      if (data) {\n        if (typeof data === 'string') {\n          response.status(status).type('txt').send(data);\n        } else {\n          response.status(status).type('json').send(data);\n        }\n      } else {\n        response.sendStatus(status);\n      }\n    }\n  }\n\n  protected async getBackendState(_: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const backendState = await this.commandWorker.getBackendState();\n\n    console.debug('GET backend_state: succeeded 200');\n    response.status(200).json(backendState);\n\n    return Promise.resolve();\n  }\n\n  protected async setBackendState(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    let result = 'received backend state';\n    let statusCode = 202;\n    const [data] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n    const state = JSON.parse(data);\n\n    try {\n      await this.commandWorker.setBackendState(state);\n    } catch (ex) {\n      console.error(`error in setBackendState:`, ex);\n      statusCode = 500;\n      result = `internal error: ${ ex }`;\n    }\n    console.debug(`setBackendState: write back status ${ statusCode }, result: ${ result }`);\n    response.status(statusCode).type('txt').send(result);\n\n    return Promise.resolve();\n  }\n\n  protected async listSnapshots(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const snapshots = await this.commandWorker.listSnapshots(context);\n\n    response.status(200).type('json').send(snapshots);\n  }\n\n  protected async createSnapshot(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    try {\n      const [data, payloadError] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n\n      if (payloadError) {\n        response.status(400).type('txt').send('The snapshot is invalid');\n\n        return;\n      }\n\n      const snapshot = JSON.parse(data);\n\n      if (!snapshot.name) {\n        response.status(400).type('txt').send('The name field is required');\n      } else {\n        await this.commandWorker.createSnapshot(context, snapshot);\n\n        response.status(200).type('txt').send('Snapshot successfully created');\n      }\n    } catch (error: any) {\n      if (error.isSnapshotError) {\n        response.status(400).type('txt').send(error.message);\n      } else {\n        throw error;\n      }\n    }\n  }\n\n  protected async restoreSnapshot(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const name = request.query.name ?? '';\n\n    if (!name) {\n      response.status(400).type('txt').send('Snapshot name is required in query parameters');\n    } else if (typeof name !== 'string') {\n      response.status(400).type('txt').send(`Invalid snapshot name ${ JSON.stringify(name) }: not a string.`);\n    } else {\n      try {\n        await this.commandWorker.restoreSnapshot(context, name);\n\n        response.status(200).type('txt').send('Snapshot successfully restored');\n      } catch (error: any) {\n        if (error.isSnapshotError) {\n          response.status(400).type('txt').send(error.message);\n        } else {\n          throw error;\n        }\n      }\n    }\n  }\n\n  protected async cancelSnapshot(_request: express.Request, response: express.Response, _context: commandContext): Promise<void> {\n    await this.commandWorker.cancelSnapshot();\n\n    try {\n      response.status(200).type('txt').send('Snapshot operation canceled');\n    } catch (error: any) {\n      if (error.isSnapshotError) {\n        response.status(400).type('txt').send(error.message);\n      } else {\n        throw error;\n      }\n    }\n  }\n\n  protected async deleteSnapshot(request: express.Request, response: express.Response, context: commandContext): Promise<void> {\n    const name = request.query.name ?? '';\n\n    if (!name) {\n      response.status(400).type('txt').send('Snapshot name is required in query parameters');\n    } else if (typeof name !== 'string') {\n      response.status(400).type('txt').send(`Invalid snapshot name ${ JSON.stringify(name) }: not a string.`);\n    } else {\n      try {\n        await this.commandWorker.deleteSnapshot(context, name);\n\n        response.status(200).type('txt').send('Snapshot successfully deleted');\n      } catch (error: any) {\n        if (error.isSnapshotError) {\n          response.status(400).type('txt').send(error.message);\n        } else {\n          throw error;\n        }\n      }\n    }\n  }\n}\n\ninterface commandContext {\n  interactive: boolean;\n}\n\n/**\n * Description of the methods which the HttpCommandServer uses to interact with the backend.\n * There's no need to use events because the server and the core backend run in the same process.\n * The HttpCommandServer is passed an instance of this interface, and calls the methods on it\n * in order to carry out the business logic for the requests it receives.\n */\nexport interface CommandWorkerInterface {\n  factoryReset:               (keepSystemImages: boolean) => void;\n  k8sReset:                   (context: commandContext, mode: 'fast' | 'wipe') => Promise<void>;\n  getSettings:                (context: commandContext) => string;\n  getLockedSettings:          (context: commandContext) => string;\n  updateSettings:             (context: commandContext, newSettings: RecursivePartial<Settings>) => Promise<[string, string]>;\n  proposeSettings:            (context: commandContext, newSettings: RecursivePartial<Settings>) => Promise<[string, string]>;\n  requestShutdown:            (context: commandContext) => void;\n  getDiagnosticCategories:    (context: commandContext) => string[] | undefined;\n  getDiagnosticIdsByCategory: (category: string, context: commandContext) => string[] | undefined;\n  getDiagnosticChecks:        (category: string | null, checkID: string | null, context: commandContext) => Promise<DiagnosticsResultCollection>;\n  runDiagnosticChecks:        (context: commandContext) => Promise<DiagnosticsResultCollection>;\n  getTransientSettings:       (context: commandContext) => string;\n  updateTransientSettings:    (context: commandContext, newTransientSettings: RecursivePartial<TransientSettings>) => Promise<[string, string]>;\n  /** Get the state of the backend */\n  getBackendState:            () => Promise<BackendState>;\n  /** Set the desired state of the backend */\n  setBackendState:            (state: BackendState) => Promise<void>;\n\n  // #region extensions\n  /**\n   * List the installed extensions with their versions.\n   * If the extension manager is not ready, returns undefined.\n   */\n  listExtensions(): Promise<Record<string, { version: string, metadata: ExtensionMetadata, labels: Record<string, string> }> | undefined>;\n  /**\n   * Install or uninstall the given extension, returning an appropriate HTTP status code.\n   * @param state Whether to install or uninstall the extension.\n   * @returns The HTTP status code, possibly with arbitrary response body data.\n   */\n  installExtension(id: string, state: 'install' | 'uninstall'): Promise<{ status: number, data?: any }>;\n  // #endregion\n  listSnapshots:   (context: commandContext) => Promise<Snapshot[]>;\n  createSnapshot:  (context: commandContext, snapshot: Snapshot) => Promise<void>;\n  deleteSnapshot:  (context: commandContext, name: string) => Promise<void>;\n  restoreSnapshot: (context: commandContext, name: string) => Promise<void>;\n  cancelSnapshot:  () => Promise<void>;\n\n  forwardPort:   (namespace: string, service: string, k8sPort: string | number, hostPort: number) => Promise<number | undefined>;\n  cancelForward: (namespace: string, service: string, k8sPort: string | number) => Promise<void>;\n}\n\n// Extend CommandWorkerInterface to have extra types, as these types are used by\n// things that would need to use the interface.  ESLint doesn't like using\n// namespaces; but in this case we're extending an existing interface.\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport namespace CommandWorkerInterface {\n  export type CommandContext = commandContext;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/commandServer/settingsValidator.ts",
    "content": "import os from 'os';\n\nimport _ from 'lodash';\nimport semver from 'semver';\n\nimport {\n  CacheMode,\n  ContainerEngine,\n  defaultSettings,\n  LockedSettingsType,\n  MountType,\n  ProtocolVersion,\n  SecurityModel,\n  Settings,\n  VMType,\n} from '@pkg/config/settings';\nimport { NavItemName, navItemNames, TransientSettings } from '@pkg/config/transientSettings';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport { parseImageReference, validateImageName, validateImageTag } from '@pkg/utils/dockerUtils';\nimport { getMacOsVersion } from '@pkg/utils/osVersion';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\nimport { preferencesNavItems } from '@pkg/window/preferenceConstants';\n\ntype settingsLike = Record<string, any>;\n\n/**\n * ValidatorFunc describes a validation function; it is used to check if a\n * given proposed setting is compatible.\n * @param mergedSettings The root of the merged settings object.\n * @param currentValue The value of the setting, before changing.\n * @param desiredValue The new value that the user is setting.\n * @param errors An array that any validation errors should be appended to.\n * @param fqname The fully qualified name of the setting, for formatting in error messages.\n * @returns boolean - true if the setting has been changed otherwise false.\n */\ntype ValidatorFunc<S, C, D, N extends string> =\n  (mergedSettings: S, currentValue: C, desiredValue: D, errors: string[], fqname: N) => boolean;\n\n/**\n * SettingsValidationMapEntry describes validators that are valid for some\n * subtree of the full settings object.  The value must be either a ValidatorFunc\n * for that subtree, or an object containing validators for each member of the\n * subtree.\n */\ntype SettingsValidationMapEntry<S, T, N extends string = ''> = {\n  [k in keyof T]:\n  k extends string ?\n    T[k] extends string | string[] | number | boolean ?\n      ValidatorFunc<S, T[k], T[k], N extends '' ? k : `${ N }.${ k }`> :\n      T[k] extends Record<string, infer V> ?\n        SettingsValidationMapEntry<S, T[k], N extends '' ? k : `${ N }.${ k }`> |\n          ValidatorFunc<S, T[k], Record<string, V>, N extends '' ? k : `${ N }.${ k }`> :\n        never :\n    never;\n};\n\n/**\n * SettingsValidationMap describes the full set of validators that will be used\n * for all settings.\n */\ntype SettingsValidationMap = SettingsValidationMapEntry<Settings, Settings>;\n\ntype TransientSettingsValidationMap = SettingsValidationMapEntry<TransientSettings, TransientSettings>;\n\nexport default class SettingsValidator {\n  k8sVersions:              string[] = [];\n  allowedSettings:          SettingsValidationMap | null = null;\n  allowedTransientSettings: TransientSettingsValidationMap | null = null;\n  synonymsTable:            settingsLike | null = null;\n  lockedSettings:           LockedSettingsType = { };\n  protected isFatal = false;\n\n  validateSettings(\n    currentSettings: Settings,\n    newSettings: RecursivePartial<Settings>,\n    lockedSettings: LockedSettingsType = {},\n  ): [boolean, string[], boolean] {\n    this.lockedSettings = lockedSettings;\n    this.isFatal = false;\n    this.allowedSettings ||= {\n      version:     this.checkUnchanged,\n      application: {\n        adminAccess: this.checkLima(this.checkBoolean),\n        debug:       this.checkBoolean,\n        extensions:  {\n          allowed: {\n            enabled: this.checkBoolean,\n            list:    this.checkExtensionAllowList,\n          },\n          installed: this.checkInstalledExtensions,\n        },\n        pathManagementStrategy: this.checkLima(this.checkEnum(...Object.values(PathManagementStrategy))),\n        telemetry:              { enabled: this.checkBoolean },\n        /** Whether we should check for updates and apply them. */\n        updater:                { enabled: this.checkBoolean },\n        autoStart:              this.checkBoolean,\n        startInBackground:      this.checkBoolean,\n        hideNotificationIcon:   this.checkBoolean,\n        window:                 { quitOnClose: this.checkBoolean },\n        theme:                  this.checkEnum('system', 'light', 'dark'),\n      },\n      containerEngine: {\n        allowedImages: {\n          enabled:  this.checkBoolean,\n          patterns: this.checkUniqueStringArray,\n        },\n        mobyStorageDriver: this.checkMulti(\n          this.checkEnum('classic', 'snapshotter', 'auto'),\n          this.checkWASMWithMobyStorage,\n        ),\n        name: this.checkMulti(\n          // 'docker' has been canonicalized to 'moby' already, but we want to include it as a valid value in the error message\n          this.checkEnum('containerd', 'moby', 'docker'),\n          this.checkWASMWithMobyStorage,\n        ),\n      },\n      virtualMachine: {\n        memoryInGB: this.checkLima(this.checkNumber(1, Number.POSITIVE_INFINITY)),\n        numberCPUs: this.checkLima(this.checkNumber(1, Number.POSITIVE_INFINITY)),\n        useRosetta: this.checkPlatform('darwin', this.checkRosetta),\n        type:       this.checkPlatform('darwin', this.checkMulti(\n          this.checkEnum(...Object.values(VMType)),\n          this.checkVMType),\n        ),\n        mount: {\n          type: this.checkLima(this.checkMulti(\n            this.checkEnum(...Object.values(MountType)),\n            this.checkMountType),\n          ),\n        },\n      },\n      experimental: {\n        containerEngine: { webAssembly: { enabled: this.checkMulti(this.checkBoolean, this.checkWASMWithMobyStorage) } },\n        kubernetes:      { options: { spinkube: this.checkMulti(this.checkBoolean, this.checkSpinkube) } },\n        virtualMachine:  {\n          diskSize: this.checkLima(this.checkByteUnits),\n          mount:    {\n            '9p': {\n              securityModel:   this.checkLima(this.check9P(this.checkEnum(...Object.values(SecurityModel)))),\n              protocolVersion: this.checkLima(this.check9P(this.checkEnum(...Object.values(ProtocolVersion)))),\n              msizeInKib:      this.checkLima(this.check9P(this.checkNumber(4, Number.POSITIVE_INFINITY))),\n              cacheMode:       this.checkLima(this.check9P(this.checkEnum(...Object.values(CacheMode)))),\n            },\n          },\n          proxy: {\n            enabled:  this.checkPlatform('win32', this.checkBoolean),\n            address:  this.checkPlatform('win32', this.checkString),\n            password: this.checkPlatform('win32', this.checkString),\n            port:     this.checkPlatform('win32', this.checkNumber(1, 65535)),\n            username: this.checkPlatform('win32', this.checkString),\n            noproxy:  this.checkPlatform('win32', this.checkUniqueStringArray),\n          },\n          sshPortForwarder: this.checkLima(this.checkBoolean),\n        },\n      },\n      WSL:        { integrations: this.checkPlatform('win32', this.checkBooleanMapping) },\n      kubernetes: {\n        version: this.checkKubernetesVersion,\n        port:    this.checkNumber(1, 65535),\n        enabled: this.checkBoolean,\n        options: { traefik: this.checkBoolean, flannel: this.checkBoolean },\n        ingress: { localhostOnly: this.checkPlatform('win32', this.checkBoolean) },\n      },\n      portForwarding: { includeKubernetesServices: this.checkBoolean },\n      images:         {\n        showAll:   this.checkBoolean,\n        namespace: this.checkString,\n      },\n      containers: {\n        showAll:   this.checkBoolean,\n        namespace: this.checkString,\n      },\n      diagnostics: {\n        mutedChecks:  this.checkBooleanMapping,\n        showMuted:    this.checkBoolean,\n        connectivity: {\n          interval: this.checkNumber(0, 2 ** 31 - 1),\n          timeout:  this.checkNumber(1, 2 ** 31 - 1),\n        },\n      },\n    };\n    this.canonicalizeSynonyms(newSettings);\n    const errors: string[] = [];\n    const needToUpdate = this.checkProposedSettings(\n      _.merge({}, currentSettings, newSettings),\n      this.allowedSettings,\n      currentSettings,\n      newSettings,\n      errors,\n      '',\n    );\n\n    return [needToUpdate && errors.length === 0, errors, this.isFatal];\n  }\n\n  validateTransientSettings(\n    currentTransientSettings: TransientSettings,\n    newTransientSettings: RecursivePartial<TransientSettings>,\n  ): [boolean, string[]] {\n    this.allowedTransientSettings ||= {\n      noModalDialogs: this.checkBoolean,\n      preferences:    {\n        navItem: {\n          current:     this.checkPreferencesNavItemCurrent,\n          currentTabs: this.checkPreferencesNavItemCurrentTabs,\n        },\n      },\n    };\n\n    this.canonicalizeSynonyms(currentTransientSettings);\n    const errors: string[] = [];\n    const needToUpdate = this.checkProposedSettings(\n      _.merge({}, currentTransientSettings, newTransientSettings),\n      this.allowedTransientSettings,\n      currentTransientSettings,\n      newTransientSettings,\n      errors,\n      '',\n    );\n\n    return [needToUpdate && errors.length === 0, errors];\n  }\n\n  /**\n   * The core function for checking proposed user settings.\n   * Walks the input: the user-provided object holding the new (and existing settings) against a verifier:\n   * 1. Complains about any fields in the input that aren't in the verifier\n   * 2. Recursively walks child-objects in the input and verifier\n   * 3. Calls validation functions off the verifier\n   * @param mergedSettings - The root object of the merged current and new settings\n   * @param allowedSettings - The verifier\n   * @param currentSettings - The current preferences object\n   * @param newSettings - User's proposed new settings\n   * @param errors - Builds this list up as new errors are encountered, so multiple errors can be reported.\n   * @param prefix - For error messages only, e.g. '' for root, 'kubernetes.options', etc.\n   * @returns boolean - true if there are changes that need to be applied.\n   */\n  protected checkProposedSettings<S>(\n    mergedSettings: S,\n    allowedSettings: settingsLike,\n    currentSettings: settingsLike,\n    newSettings: settingsLike,\n    errors: string[],\n    prefix: string): boolean {\n    let changeNeeded = false; // can only be set to true once we have a change to make, never back to false\n\n    for (const k in newSettings) {\n      let changeNeededHere = false;\n      const fqname = prefix ? `${ prefix }.${ k }` : k;\n\n      if (!(k in allowedSettings)) {\n        continue;\n      }\n      if (typeof (allowedSettings[k]) === 'object') {\n        if (typeof (newSettings[k]) === 'object') {\n          changeNeeded = this.checkProposedSettings(mergedSettings, allowedSettings[k], currentSettings[k], newSettings[k], errors, fqname) || changeNeeded;\n        } else {\n          errors.push(`Setting \"${ fqname }\" should wrap an inner object, but got <${ newSettings[k] }>.`);\n        }\n      } else if (typeof (newSettings[k]) === 'object') {\n        if (typeof allowedSettings[k] === 'function') {\n          // Special case for things like `.WSLIntegrations` which have unknown fields.\n          const validator: ValidatorFunc<S, any, any, any> = allowedSettings[k];\n\n          changeNeededHere = validator.call(this, mergedSettings, currentSettings[k], newSettings[k], errors, fqname);\n        } else {\n          // newSettings[k] should be valid JSON because it came from `JSON.parse(incoming-payload)`.\n          // It's an internal error (HTTP Status 500) if it isn't.\n          errors.push(`Setting \"${ fqname }\" should be a simple value, but got <${ JSON.stringify(newSettings[k]) }>.`);\n        }\n      } else if (typeof allowedSettings[k] === 'function') {\n        const validator: ValidatorFunc<S, any, any, any> = allowedSettings[k];\n\n        changeNeededHere = validator.call(this, mergedSettings, currentSettings[k], newSettings[k], errors, fqname);\n      } else {\n        errors.push(this.notSupported(fqname));\n      }\n      if (changeNeededHere) {\n        const isLocked = _.get(this.lockedSettings, `${ prefix }.${ k }`);\n\n        if (isLocked) {\n          // A delayed error condition, raised only if we try to change a field in a locked object\n          errors.push(`field \"${ prefix }.${ k }\" is locked`);\n          this.isFatal = true;\n        } else {\n          changeNeeded = true;\n        }\n      }\n    }\n\n    return changeNeeded;\n  }\n\n  protected invalidSettingMessage(fqname: string, desiredValue: any): string {\n    return `Invalid value for \"${ fqname }\": <${ JSON.stringify(desiredValue) }>`;\n  }\n\n  /**\n   * checkLima ensures that the given parameter is only set on Lima-based platforms.\n   * @note This should not be used for things with default values.\n   */\n  protected checkLima<C, D, N extends string>(validator: ValidatorFunc<Settings, C, D, N>) {\n    return (mergedSettings: Settings, currentValue: C, desiredValue: D, errors: string[], fqname: N) => {\n      if (!['darwin', 'linux'].includes(os.platform())) {\n        if (!_.isEqual(currentValue, desiredValue)) {\n          this.isFatal = true;\n          errors.push(this.notSupported(fqname));\n        }\n\n        return false;\n      }\n\n      return validator.call(this, mergedSettings, currentValue, desiredValue, errors, fqname);\n    };\n  }\n\n  protected checkRosetta(mergedSettings: Settings, currentValue: boolean, desiredValue: boolean, errors: string[], fqname: string): boolean {\n    if (desiredValue && !currentValue) {\n      if (mergedSettings.virtualMachine.type !== VMType.VZ) {\n        errors.push(`Setting ${ fqname } can only be enabled when virtual-machine.type is \"${ VMType.VZ }\".`);\n        this.isFatal = true;\n\n        return false;\n      }\n      if (process.arch !== 'arm64') {\n        errors.push(`Setting ${ fqname } can only be enabled on aarch64 systems.`);\n        this.isFatal = true;\n\n        return false;\n      }\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  protected checkVMType(mergedSettings: Settings, currentValue: string, desiredValue: string, errors: string[], fqname: string): boolean {\n    if (desiredValue === VMType.VZ) {\n      if (os.arch() === 'arm64' && semver.gt('13.3.0', getMacOsVersion())) {\n        this.isFatal = true;\n        errors.push(`Setting ${ fqname } to \"${ VMType.VZ }\" on ARM requires macOS 13.3 (Ventura) or later.`);\n\n        return false;\n      } else if (semver.gt('13.0.0', getMacOsVersion())) {\n        this.isFatal = true;\n        errors.push(`Setting ${ fqname } to \"${ VMType.VZ }\" on Intel requires macOS 13.0 (Ventura) or later.`);\n\n        return false;\n      }\n      if (mergedSettings.virtualMachine.mount.type === MountType.NINEP) {\n        errors.push(\n          `Setting ${ fqname } to \"${ VMType.VZ }\" requires that virtual-machine.mount.type is ` +\n          `\"${ MountType.REVERSE_SSHFS }\" or \"${ MountType.VIRTIOFS }\".`);\n\n        return false;\n      }\n    }\n    if (desiredValue === VMType.QEMU) {\n      if (mergedSettings.virtualMachine.mount.type === MountType.VIRTIOFS && os.platform() === 'darwin') {\n        errors.push(\n          `Setting ${ fqname } to \"${ VMType.QEMU }\" requires that virtual-machine.mount.type is ` +\n          `\"${ MountType.REVERSE_SSHFS }\" or \"${ MountType.NINEP }\".`);\n\n        return false;\n      }\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  protected checkMountType(mergedSettings: Settings, currentValue: string, desiredValue: string, errors: string[], fqname: string): boolean {\n    if (desiredValue === MountType.VIRTIOFS && mergedSettings.virtualMachine.type !== VMType.VZ && os.platform() === 'darwin') {\n      errors.push(`Setting ${ fqname } to \"${ MountType.VIRTIOFS }\" requires that virtual-machine.type is \"${ VMType.VZ }\".`);\n      this.isFatal = true;\n\n      return false;\n    }\n    if (desiredValue === MountType.VIRTIOFS && mergedSettings.virtualMachine.type !== VMType.QEMU && os.platform() === 'linux') {\n      errors.push(`Setting ${ fqname } to \"${ MountType.VIRTIOFS }\" requires that virtual-machine.type is \"${ VMType.QEMU }\".`);\n      this.isFatal = true;\n\n      return false;\n    }\n    if (desiredValue === MountType.NINEP && mergedSettings.virtualMachine.type !== VMType.QEMU) {\n      errors.push(`Setting ${ fqname } to \"${ MountType.NINEP }\" requires that virtual-machine.type is \"${ VMType.QEMU }\".`);\n      this.isFatal = true;\n\n      return false;\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  protected checkSpinkube(mergedSettings: Settings, currentValue: boolean, desiredValue: boolean, errors: string[], fqname: string): boolean {\n    if (mergedSettings.kubernetes.enabled && desiredValue) {\n      if (!mergedSettings.experimental.containerEngine.webAssembly.enabled) {\n        errors.push(`Setting ${ fqname } can only be set when experimental.container-engine.web-assembly.enabled is set as well.`);\n        this.isFatal = true;\n\n        return false;\n      }\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  // checkWASMWithMobyStorage checks that we can't use classic storage for moby\n  // in combination with WASM.\n  protected checkWASMWithMobyStorage<\n    T,\n    N extends 'containerEngine.name' | 'containerEngine.mobyStorageDriver' | 'experimental.containerEngine.webAssembly.enabled',\n  >(mergedSettings: Settings, currentValue: T, desiredValue: T, errors: string[], fqname: N): boolean {\n    if (mergedSettings.containerEngine.name === ContainerEngine.MOBY &&\n        mergedSettings.experimental.containerEngine.webAssembly.enabled &&\n        mergedSettings.containerEngine.mobyStorageDriver === 'classic'\n    ) {\n      const message: string = {\n        'containerEngine.name':                             'Cannot switch to moby container engine with classic storage when WebAssembly is enabled.',\n        'experimental.containerEngine.webAssembly.enabled': 'Cannot enable WebAssembly with classic storage for moby.',\n        'containerEngine.mobyStorageDriver':                'Cannot switch to classic storage for moby when WebAssembly is enabled.',\n      }[fqname];\n\n      if (currentValue !== desiredValue) {\n        errors.push(message);\n        this.isFatal = true;\n      }\n    }\n    return currentValue !== desiredValue;\n  }\n\n  protected checkPlatform<C, D, N extends string>(platform: NodeJS.Platform, validator: ValidatorFunc<Settings, C, D, N>) {\n    return (mergedSettings: Settings, currentValue: C, desiredValue: D, errors: string[], fqname: N) => {\n      if (os.platform() !== platform) {\n        if (!_.isEqual(currentValue, desiredValue)) {\n          errors.push(this.notSupported(fqname));\n          this.isFatal = true;\n        }\n\n        return false;\n      }\n\n      return validator.call(this, mergedSettings, currentValue, desiredValue, errors, fqname);\n    };\n  }\n\n  protected check9P<C, D, N extends string>(validator: ValidatorFunc<Settings, C, D, N>) {\n    return (mergedSettings: Settings, currentValue: C, desiredValue: D, errors: string[], fqname: N) => {\n      if (mergedSettings.virtualMachine.mount.type !== MountType.NINEP) {\n        if (!_.isEqual(currentValue, desiredValue)) {\n          errors.push(`Setting ${ fqname } can only be changed when virtualMachine.mount.type is \"${ MountType.NINEP }\".`);\n          this.isFatal = true;\n        }\n\n        return false;\n      }\n\n      return validator.call(this, mergedSettings, currentValue, desiredValue, errors, fqname);\n    };\n  }\n\n  protected checkMulti<S, C, D, N extends string>(...validators: ValidatorFunc<S, C, D, N>[]) {\n    return (mergedSettings: S, currentValue: C, desiredValue: D, errors: string[], fqname: N) => {\n      let retval = false;\n\n      for (const validator of validators) {\n        retval = validator.call(this, mergedSettings, currentValue, desiredValue, errors, fqname) || retval;\n      }\n\n      return retval;\n    };\n  }\n\n  /**\n   * checkBoolean is a generic checker for simple boolean values.\n   */\n  protected checkBoolean<S>(mergedSettings: S, currentValue: boolean, desiredValue: boolean, errors: string[], fqname: string): boolean {\n    if (typeof desiredValue !== 'boolean') {\n      errors.push(this.invalidSettingMessage(fqname, desiredValue));\n\n      return false;\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  /**\n   * checkNumber returns a checker for a number in the given range, inclusive.\n   */\n  protected checkNumber(min: number, max: number) {\n    return <S>(mergedSettings: S, currentValue: number, desiredValue: number, errors: string[], fqname: string) => {\n      if (typeof desiredValue !== 'number') {\n        errors.push(this.invalidSettingMessage(fqname, desiredValue));\n\n        return false;\n      }\n      if (desiredValue < min || desiredValue > max) {\n        errors.push(this.invalidSettingMessage(fqname, desiredValue));\n\n        return false;\n      }\n\n      return currentValue !== desiredValue;\n    };\n  }\n\n  protected checkEnum(...validValues: string[]) {\n    return <S>(mergedSettings: S, currentValue: string, desiredValue: string, errors: string[], fqname: string) => {\n      const explanation = `must be one of ${ JSON.stringify(validValues) }`;\n\n      if (typeof desiredValue !== 'string') {\n        errors.push(`${ this.invalidSettingMessage(fqname, desiredValue) }; ${ explanation }`);\n\n        return false;\n      }\n      if (!validValues.includes(desiredValue)) {\n        errors.push(`Invalid value for \"${ fqname }\": <${ JSON.stringify(desiredValue) }>; ${ explanation }`);\n        this.isFatal = true;\n\n        return false;\n      }\n\n      return currentValue !== desiredValue;\n    };\n  }\n\n  protected checkString<S>(mergedSettings: S, currentValue: string, desiredValue: string, errors: string[], fqname: string): boolean {\n    if (typeof desiredValue !== 'string') {\n      errors.push(this.invalidSettingMessage(fqname, desiredValue));\n\n      return false;\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  /**\n   * Parse a string representing a number of bytes into a number, in a way that\n   * is compatible with `github.com/docker/go-units`.\n   * @param input The string to parse.\n   * @returns The parsed number, or `undefined` if the input is not valid.\n   */\n  protected parseByteUnits(input: string): number | undefined {\n    const expression = /^(\\d+(?:\\.\\d+)?) ?([kmgtpezy]?)(i?b)?$/i; // spellcheck-ignore-line\n    const prefix = ['', 'k', 'm', 'g', 't', 'p', 'e', 'z', 'y'];\n    const match = expression.exec(input);\n\n    if (!match) {\n      return undefined;\n    }\n\n    const [, number, scale, unit] = match;\n    const base = unit?.startsWith('i') ? 1_024 : 1_000;\n    const exponent = prefix.indexOf(scale.toLowerCase() ?? '');\n\n    return parseFloat(number) * base ** exponent;\n  }\n\n  /**\n   * Check that the setting is a valid number of bytes, per `github.com/docker/go-units`.\n   */\n  protected checkByteUnits(_: Settings, currentValue: string, desiredValue: string, errors: string[], fqname: string): boolean {\n    const current = this.parseByteUnits(currentValue);\n    const desired = this.parseByteUnits(desiredValue);\n\n    if (typeof desired === 'undefined') {\n      errors.push(this.invalidSettingMessage(fqname, desiredValue));\n    } else if (typeof current !== 'undefined' && desired < current) {\n      errors.push(`Cannot decrease \"${ fqname }\" from ${ currentValue } to ${ desiredValue }`);\n    } else {\n      return currentValue !== desiredValue;\n    }\n\n    return false;\n  }\n\n  protected checkKubernetesVersion(mergedSettings: Settings, currentValue: string, desiredVersion: string, errors: string[], _: string): boolean {\n    /**\n     * desiredVersion can be an empty string when Kubernetes is disabled, but otherwise it must be a valid version.\n    */\n    if ((mergedSettings.kubernetes.enabled || desiredVersion !== '') && !this.k8sVersions.includes(desiredVersion)) {\n      errors.push(`Kubernetes version \"${ desiredVersion }\" not found.`);\n\n      return false;\n    }\n\n    return currentValue !== desiredVersion;\n  }\n\n  protected notSupported(fqname: string) {\n    return `Changing field \"${ fqname }\" via the API isn't supported.`;\n  }\n\n  protected checkUnchanged<S>(mergedSettings: S, currentValue: any, desiredValue: any, errors: string[], fqname: string): boolean {\n    if (currentValue !== desiredValue) {\n      errors.push(this.notSupported(fqname));\n    }\n\n    return false;\n  }\n\n  /**\n   * Ensures settings that are objects adhere to their type of\n   * Record<string, boolean>. This is useful for checking that values other than\n   * booleans are not unintentionally added to settings like WSLIntegrations\n   * and mutedChecks.\n   */\n  protected checkBooleanMapping<S>(mergedSettings: S, currentValue: Record<string, boolean>, desiredValue: Record<string, boolean>, errors: string[], fqname: string): boolean {\n    if (typeof (desiredValue) !== 'object') {\n      errors.push(`Proposed field \"${ fqname }\" should be an object, got <${ desiredValue }>.`);\n\n      return false;\n    }\n\n    let changed = Object.keys(currentValue).some(k => !(k in desiredValue));\n\n    for (const [key, value] of Object.entries(desiredValue)) {\n      if (typeof value !== 'boolean' && value !== null) {\n        errors.push(this.invalidSettingMessage(`${ fqname }.${ key }`, desiredValue[key]));\n      } else {\n        changed ||= currentValue[key] !== value;\n      }\n    }\n\n    return errors.length === 0 && changed;\n  }\n\n  protected checkUniqueStringArray<S>(mergedSettings: S, currentValue: string[], desiredValue: string[], errors: string[], fqname: string): boolean {\n    if (!Array.isArray(desiredValue) || desiredValue.some(s => typeof (s) !== 'string')) {\n      errors.push(this.invalidSettingMessage(fqname, desiredValue));\n\n      return false;\n    }\n    const duplicateValues = this.findDuplicates(desiredValue);\n\n    if (duplicateValues.length > 0) {\n      duplicateValues.sort(Intl.Collator().compare);\n      errors.push(`field \"${ fqname }\" has duplicate entries: \"${ duplicateValues.join('\", \"') }\"`);\n\n      return false;\n    }\n\n    return currentValue.length !== desiredValue.length || currentValue.some((v, i) => v !== desiredValue[i]);\n  }\n\n  protected findDuplicates(list: string[]): string[] {\n    let whiteSpaceMembers = [];\n    const firstInstance = new Set<string>();\n    const duplicates = new Set<string>();\n    const isWhiteSpaceRE = /^\\s*$/;\n\n    for (const member of list) {\n      if (isWhiteSpaceRE.test(member)) {\n        whiteSpaceMembers.push(member);\n      } else if (!firstInstance.has(member)) {\n        firstInstance.add(member);\n      } else {\n        duplicates.add(member);\n      }\n    }\n    if (whiteSpaceMembers.length === 1) {\n      whiteSpaceMembers = [];\n    }\n\n    return Array.from(duplicates).concat(whiteSpaceMembers);\n  }\n\n  protected checkInstalledExtensions(\n    mergedSettings: Settings,\n    currentValue: Record<string, string>,\n    desiredValue: any,\n    errors: string[],\n    fqname: string,\n  ): boolean {\n    if (_.isEqual(desiredValue, currentValue)) {\n      // Accept no-op changes\n      return false;\n    }\n\n    if (typeof desiredValue !== 'object' || !desiredValue) {\n      errors.push(`${ fqname }: \"${ desiredValue }\" is not a valid mapping`);\n\n      return false;\n    }\n\n    for (const [name, tag] of Object.entries(desiredValue)) {\n      if (!validateImageName(name)) {\n        errors.push(`${ fqname }: \"${ name }\" is an invalid name`);\n      }\n      if (typeof tag !== 'string') {\n        errors.push(`${ fqname }: \"${ name }\" has non-string tag \"${ tag }\"`);\n      } else if (!validateImageTag(tag)) {\n        errors.push(`${ fqname }: \"${ name }\" has invalid tag \"${ tag }\"`);\n      }\n    }\n\n    return !_.isEqual(desiredValue, currentValue);\n  }\n\n  protected checkExtensionAllowList(\n    mergedSettings: Settings,\n    currentValue: string[],\n    desiredValue: any,\n    errors: string[],\n    fqname: string,\n  ): boolean {\n    if (_.isEqual(desiredValue, currentValue)) {\n      // Accept no-op changes\n      return false;\n    }\n\n    const changed = this.checkUniqueStringArray(mergedSettings, currentValue, desiredValue, errors, fqname);\n\n    if (errors.length) {\n      return changed;\n    }\n\n    for (const pattern of desiredValue as string[]) {\n      if (!parseImageReference(pattern, true)) {\n        errors.push(`${ fqname }: \"${ pattern }\" does not describe an image reference`);\n      }\n    }\n\n    return errors.length === 0 && changed;\n  }\n\n  protected checkPreferencesNavItemCurrent(\n    mergedSettings: TransientSettings,\n    currentValue: NavItemName,\n    desiredValue: NavItemName,\n    errors: string[],\n    fqname: string,\n  ): boolean {\n    if (!desiredValue || !navItemNames.includes(desiredValue)) {\n      errors.push(`${ fqname }: \"${ desiredValue }\" is not a valid page name for Preferences Dialog`);\n\n      return false;\n    }\n\n    return currentValue !== desiredValue;\n  }\n\n  protected checkPreferencesNavItemCurrentTabs(\n    mergedSettings: TransientSettings,\n    currentValue: Record<NavItemName, string | undefined>,\n    desiredValue: any,\n    errors: string[],\n    fqname: string,\n  ): boolean {\n    for (const k of Object.keys(desiredValue)) {\n      if (!navItemNames.includes(k as NavItemName)) {\n        errors.push(`${ fqname }: \"${ k }\" is not a valid page name for Preferences Dialog`);\n\n        return false;\n      }\n\n      if (_.isEqual(currentValue[k as NavItemName], desiredValue[k])) {\n        // If the setting is unchanged, allow any value.  This is needed if some\n        // settings are not applicable for a platform.\n        continue;\n      }\n\n      const navItem = preferencesNavItems.find(item => item.name === k);\n\n      if (!navItem?.tabs?.includes(desiredValue[k])) {\n        errors.push(`${ fqname }: tab name \"${ desiredValue[k] }\" is not a valid tab name for \"${ k }\" Preference page`);\n\n        return false;\n      }\n    }\n\n    return !_.isEqual(currentValue, desiredValue);\n  }\n\n  canonicalizeSynonyms(newSettings: settingsLike): void {\n    this.synonymsTable ||= {\n      containerEngine: { name: this.canonicalizeContainerEngine },\n      kubernetes:      { version: this.canonicalizeKubernetesVersion },\n    };\n    this.canonicalizeSettings(this.synonymsTable, newSettings, []);\n  }\n\n  protected canonicalizeSettings(synonymsTable: settingsLike, newSettings: settingsLike, prefix: string[]): void {\n    for (const k in newSettings) {\n      if (typeof newSettings[k] === 'object') {\n        this.canonicalizeSettings(synonymsTable[k] ?? {}, newSettings[k], prefix.concat(k));\n      } else if (typeof synonymsTable[k] === 'function') {\n        synonymsTable[k].call(this, newSettings, k);\n      } else if (typeof _.get(defaultSettings, prefix.concat(k)) === 'boolean') {\n        this.canonicalizeBool(newSettings, k);\n      } else if (typeof _.get(defaultSettings, prefix.concat(k)) === 'number') {\n        this.canonicalizeNumber(newSettings, k);\n      }\n    }\n  }\n\n  protected canonicalizeKubernetesVersion(newSettings: settingsLike, index: string): void {\n    const desiredValue: string = newSettings[index];\n    const ptn = /^(v?)(\\d+\\.\\d+\\.\\d+)((?:\\+k3s\\d+)?)$/;\n    const m = ptn.exec(desiredValue);\n\n    if (m && (m[1] || m[3])) {\n      newSettings[index] = m[2];\n    }\n  }\n\n  protected canonicalizeContainerEngine(newSettings: settingsLike, index: string): void {\n    if (newSettings[index] === 'docker') {\n      newSettings[index] = 'moby';\n    }\n  }\n\n  protected canonicalizeBool(newSettings: settingsLike, index: string): void {\n    const desiredValue: boolean | string = newSettings[index];\n\n    if (desiredValue === 'true') {\n      newSettings[index] = true;\n    } else if (desiredValue === 'false') {\n      newSettings[index] = false;\n    }\n  }\n\n  protected canonicalizeNumber(newSettings: settingsLike, index: string): void {\n    const desiredValue: number | string = newSettings[index];\n\n    if (typeof desiredValue === 'string') {\n      const parsedValue = parseInt(desiredValue, 10);\n\n      // Ignore NaN; we'll fail validation later.\n      if (!Number.isNaN(parsedValue)) {\n        newSettings[index] = parsedValue;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/containerExec.ts",
    "content": "/**\n * This module handles interactive container exec sessions for the Shell tab.\n * It manages bidirectional IPC between the renderer (xterm.js) and docker exec.\n *\n * Sessions survive frontend navigation: on \"detach\" the process keeps running\n * and stdout is buffered (ring buffer, 50 KB).  On reconnect the buffer is\n * replayed so the user sees the full terminal history.\n */\n\nimport Electron from 'electron';\n\nimport type { ContainerEngineClient } from '@pkg/backend/containerClient';\nimport type { WritableReadableProcess } from '@pkg/backend/containerClient/types';\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport type { IpcRendererEvents } from '@pkg/typings/electron-ipc';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.containerExec;\nconst ipcMainProxy = getIpcMainProxy(console);\n\nconst MAX_OUTPUT_BUF = 50 * 1024; // 50 KB ring buffer\n\ninterface ExecSession {\n  process:   WritableReadableProcess;\n  sender:    Electron.WebContents | null;\n  outputBuf: string;\n  detached:  boolean;\n}\n\nexport class ContainerExecHandler {\n  protected sessions = new Map<string, ExecSession>(); // containerId → session\n\n  constructor(protected client: ContainerEngineClient) {\n    this.initHandlers();\n  }\n\n  updateClient(client: ContainerEngineClient) {\n    this.client = client;\n    this.killAll();\n  }\n\n  killAll() {\n    for (const [containerId, session] of this.sessions) {\n      try {\n        session.process.kill('SIGTERM');\n      } catch (ex) {\n        console.debug(`Error killing exec session ${ containerId }:`, ex);\n      }\n    }\n    this.sessions.clear();\n  }\n\n  protected async checkScriptAvailable(containerId: string, namespace: string | undefined): Promise<boolean> {\n    try {\n      // Use 'ignore' stdio so all streams go to /dev/null — no pipes to block\n      // on, no stdin that keeps the Docker multiplexed connection open.\n      // spawnFile resolves on exit code 0 and rejects otherwise.\n      await this.client.runClient(\n        ['exec', containerId, 'sh', '-c', 'command -v script'],\n        'ignore',\n        { namespace },\n      );\n\n      return true;\n    } catch {\n      return false;\n    }\n  }\n\n  protected initHandlers() {\n    ipcMainProxy.on('container-exec/start', async(event, containerId, namespace) => {\n      const sendToFrame = <ch extends keyof IpcRendererEvents>(channel: ch, ...args: Parameters<IpcRendererEvents[ch]>) => {\n        try {\n          event.sender.send(channel, ...args);\n        } catch (ex) {\n          console.debug(`Failed to send ${ channel } to frame:`, ex);\n        }\n      };\n\n      // Reconnect path: an existing session for this container is alive.\n      const session = this.sessions.get(containerId);\n\n      if (session) {\n        console.debug(`[ContainerExec] reconnecting existing session for ${ containerId }`);\n        session.sender = event.sender;\n        session.detached = false;\n        sendToFrame('container-exec/ready', containerId, session.outputBuf);\n\n        return;\n      }\n\n      // New session path.\n      try {\n        // Pre-check: verify `script` is available in the container before\n        // starting the session.  If it isn't, we cannot offer a good shell\n        // experience (no PTY line discipline), so we surface a clear message\n        // instead of falling back to a degraded mode.\n        const scriptAvailable = await this.checkScriptAvailable(containerId, namespace);\n\n        if (!scriptAvailable) {\n          sendToFrame('container-exec/unsupported');\n\n          return;\n        }\n\n        const proc = this.client.runClient(\n          ['exec', '-i', containerId, 'script', '-q', '-c', 'sh', '/dev/null'],\n          'interactive',\n          { namespace },\n        );\n\n        const newSession: ExecSession = {\n          process:   proc,\n          sender:    event.sender,\n          outputBuf: '',\n          detached:  false,\n        };\n\n        this.sessions.set(containerId, newSession);\n\n        const sendToSession = <ch extends keyof IpcRendererEvents>(channel: ch, ...args: Parameters<IpcRendererEvents[ch]>) => {\n          try {\n            newSession.sender?.send(channel, ...args);\n          } catch (ex) {\n            console.debug(`Failed to send ${ channel } to frame:`, ex);\n          }\n        };\n\n        sendToSession('container-exec/ready', containerId, '');\n\n        proc.stdout.on('data', (data: Buffer) => {\n          const text = data.toString('utf-8');\n\n          // Accumulate in ring buffer.\n          newSession.outputBuf += text;\n          if (newSession.outputBuf.length > MAX_OUTPUT_BUF) {\n            newSession.outputBuf = newSession.outputBuf.slice(-MAX_OUTPUT_BUF);\n          }\n\n          sendToSession('container-exec/output', containerId, text);\n        });\n\n        proc.stderr.on('data', (data: Buffer) => {\n          const text = data.toString('utf-8');\n\n          sendToSession('container-exec/output', containerId, text);\n          newSession.outputBuf += text;\n          if (newSession.outputBuf.length > MAX_OUTPUT_BUF) {\n            newSession.outputBuf = newSession.outputBuf.slice(-MAX_OUTPUT_BUF);\n          }\n        });\n\n        proc.on('exit', (code: number | null) => {\n          sendToSession('container-exec/exit', containerId, code ?? -1);\n          this.sessions.delete(containerId);\n        });\n\n        proc.on('error', (err: Error) => {\n          console.error(`Exec session for ${ containerId } error:`, err);\n          sendToSession('container-exec/output', containerId, `\\r\\nError: ${ err.message }\\r\\n`);\n          sendToSession('container-exec/exit', containerId, -1);\n          this.sessions.delete(containerId);\n        });\n      } catch (ex) {\n        console.error(`Failed to start exec session for ${ containerId }:`, ex);\n        try {\n          sendToFrame('container-exec/exit', '', -1);\n        } catch {}\n      }\n    });\n\n    ipcMainProxy.on('container-exec/input', (_, containerId, data) => {\n      const session = this.sessions.get(containerId);\n\n      try {\n        session?.process.stdin?.write(data);\n      } catch (ex) {\n        console.debug(`Failed to write to exec session ${ containerId }:`, ex);\n      }\n    });\n\n    ipcMainProxy.on('container-exec/kill', (_, containerId) => {\n      const session = this.sessions.get(containerId);\n\n      if (session) {\n        try {\n          session.process.kill('SIGTERM');\n        } catch (ex) {\n          console.debug(`Failed to kill exec session ${ containerId }:`, ex);\n        }\n        this.sessions.delete(containerId);\n      }\n    });\n\n    ipcMainProxy.on('container-exec/detach', (_, containerId) => {\n      const session = this.sessions.get(containerId);\n\n      if (session) {\n        session.sender = null;\n        session.detached = true;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/credentialServer/README.md",
    "content": "# Credential Helper Server protocol\n\nThis server isn't intended to be used publicly;\nit's for other tools that we use in order to find docker credentials.\n\nThe protocol is described at [https://github.com/docker/docker-credential-helpers#development](https://github.com/docker/docker-credential-helpers#development).\n"
  },
  {
    "path": "pkg/rancher-desktop/main/credentialServer/__tests__/credentialUtils.spec.ts",
    "content": "/** @jest-environment node */\n\nimport fs from 'fs';\nimport path from 'path';\nimport stream from 'stream';\n\nimport { jest } from '@jest/globals';\nimport { findHomeDir } from '@kubernetes/client-node';\n\nimport type { spawnFile as spawnFileType } from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nconst modules = mockModules({\n  fs: {\n    ...fs,\n    promises: {\n      ...fs.promises,\n      readFile: jest.spyOn(fs.promises, 'readFile'),\n    },\n  },\n  '@pkg/utils/childProcess': { spawnFile: jest.fn<(command: string, args: string[], options: object) => Promise<{ stdout?: string }>>() },\n});\n\njest.mock('@pkg/utils/childProcess');\n\nconst { default: runCommand, list } = await import('@pkg/main/credentialServer/credentialUtils');\nconst spawnFile = modules['@pkg/utils/childProcess'].spawnFile;\n\ndescribe('runCommand', () => {\n  afterEach(() => {\n    jest.restoreAllMocks();\n    jest.resetAllMocks();\n  });\n\n  it('runs the command', async() => {\n    const expected = `Some output`;\n\n    modules.fs.promises.readFile.mockImplementation((filepath) => {\n      const home = findHomeDir() ?? '';\n\n      expect(filepath).toEqual(path.join(home, '.docker', 'config.json'));\n\n      return Promise.resolve(JSON.stringify({ credsStore: 'pikachu' }));\n    });\n    spawnFile.mockImplementation((command, args, options) => {\n      const resourcesPath = path.join(paths.resources, process.platform, 'bin');\n\n      expect(command).toEqual('docker-credential-pikachu');\n      expect(args).toEqual(['pika']);\n      expect(options).toMatchObject({\n        env:   { PATH: expect.stringContaining(resourcesPath) },\n        stdio: [expect.anything(), 'pipe', expect.anything()],\n      });\n\n      return Promise.resolve({ stdout: expected }) as any;\n    });\n    await expect(runCommand('pika')).resolves.toEqual(expected);\n  });\n\n  it('errors out on failing to read config', async() => {\n    const error = new Error('Some error');\n\n    modules.fs.promises.readFile.mockImplementation((filepath) => {\n      const home = findHomeDir() ?? '';\n\n      expect(filepath).toEqual(path.join(home, '.docker', 'config.json'));\n\n      return Promise.reject(error);\n    });\n\n    spawnFile.mockImplementation(() => Promise.resolve({}));\n\n    await expect(runCommand('pika')).rejects.toBe(error);\n    expect(spawnFile).not.toHaveBeenCalled();\n  });\n\n  // Check managing credentials, for the case where there's a per-host override\n  // in the `credHelpers` key, as well as the case where there is no such\n  // override.\n  describe.each([\n    {\n      description: 'overridden', host: 'override.test', executable: 'bulbasaur',\n    },\n    {\n      description: 'not overridden', host: 'default.test', executable: 'pikachu',\n    },\n  ])('helper $description', ({ host, executable }) => {\n    beforeEach(() => {\n      modules.fs.promises.readFile.mockImplementation((filepath) => {\n        const home = findHomeDir() ?? '';\n\n        expect(filepath).toEqual(path.join(home, '.docker', 'config.json'));\n\n        return Promise.resolve(JSON.stringify({\n          credsStore:  'pikachu',\n          credHelpers: { 'override.test': 'bulbasaur' },\n        }));\n      });\n    });\n\n    // Check each action, `get`, `erase`, `store`, and an unknown action.\n    // We need per-command checks here as our logic varies per command.\n    test.each([\n      { command: 'get', input: host },\n      { command: 'erase', input: host },\n      { command: 'store', input: JSON.stringify({ ServerURL: host, arg: 'x' }) },\n      {\n        command: 'unknown command', input: host, override: 'pikachu',\n      },\n    ])('on $command', async({ command, input, override }) => {\n      const expected = 'password';\n\n      spawnFile.mockImplementation((file, args, options) => {\n        expect(file).toEqual(`docker-credential-${ override ?? executable }`);\n        expect(args).toEqual([command]);\n        expect(options).toMatchObject({ stdio: [expect.any(stream.Readable), expect.anything(), expect.anything()] });\n\n        return Promise.resolve({ stdout: expected }) as any;\n      });\n\n      await expect(runCommand(command, input)).resolves.toEqual(expected);\n    });\n  });\n});\n\ndescribe('list', () => {\n  let config: { credsStore: string, credHelpers?: Record<string, string> } = { credsStore: 'unset' };\n  let helpers: Record<string, any> = {};\n\n  beforeEach(() => {\n    modules.fs.promises.readFile.mockImplementation((filepath) => {\n      const home = findHomeDir() ?? '';\n\n      expect(filepath).toEqual(path.join(home, '.docker', 'config.json'));\n\n      return Promise.resolve(JSON.stringify(config));\n    });\n    spawnFile.mockImplementation((file, args) => {\n      const helper = file.replace(/^docker-credential-/, '');\n\n      expect(file).toMatch(/^docker-credential-/);\n      expect(args).toEqual(['list']);\n\n      return Promise.resolve({ stdout: JSON.stringify(helpers[helper] ?? {}) }) as any;\n    });\n  });\n\n  it('uses the default helper', async() => {\n    config = { credsStore: 'pikachu' };\n    helpers = { pikachu: { 'host.test': 'stuff' } };\n    await expect(list()).resolves.toEqual({ 'host.test': 'stuff' });\n  });\n\n  it('runs additional helpers', async() => {\n    config = { credsStore: 'pikachu', credHelpers: { 'example.test': 'bulbasaur' } };\n    helpers = {\n      pikachu:   { 'host.test': 'stuff' },\n      bulbasaur: { 'example.test': 'moar stuff' },\n    };\n    await expect(list()).resolves.toEqual({\n      'host.test':    'stuff',\n      'example.test': 'moar stuff',\n    });\n  });\n\n  it('only returns matching results', async() => {\n    config = { credsStore: 'pikachu', credHelpers: { 'example.test': 'bulbasaur' } };\n    helpers = {\n      pikachu:   { 'host.test': 'stuff' },\n      bulbasaur: {\n        'example.test': 'moar stuff', 'host.test': 'ignored', 'extra.test': 'also ignored',\n      },\n    };\n    await expect(list()).resolves.toEqual({\n      'host.test':    'stuff',\n      'example.test': 'moar stuff',\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/credentialServer/credentialUtils.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport stream from 'stream';\n\nimport { findHomeDir } from '@kubernetes/client-node';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\ninterface credHelperInfo {\n  /** The name of the credential helper to use (a suffix of `docker-credential-`) */\n  credsStore:  string;\n  /** hash of URLs to credential-helper-name */\n  credHelpers: Record<string, string>\n}\n\nconst console = Logging.server;\n\n/**\n * Run the credential helper with the given command.\n * @param command The one-word command to run.\n * @param input Any input to be provided to the command (as standard input).\n */\nexport default async function runCommand(command: string, input?: string): Promise<string> {\n  if (command === 'list') {\n    // List requires special treatment.\n    return JSON.stringify(await list());\n  }\n\n  const { credsStore } = await getCredentialHelperInfo(command, input ?? '');\n\n  try {\n    return runCredHelper(credsStore, command, input);\n  } catch (ex: any) {\n    ex.helper = credsStore;\n    throw ex;\n  }\n}\n\n/**\n * Run the `list` command.\n * This command needs special treatment as we need information from multiple\n * cred helpers, based on the settings found in the `credHelpers` section of\n * the configuration.\n *\n * Modeled after https://github.com/docker/cli/blob/d0bd373986b6678bfe1a0eb6989ce13907247a85/cli/config/configfile/file.go#L285\n */\nexport async function list(): Promise<Record<string, string>> {\n  // Return the creds list from the default helper, plus any data from\n  // additional credential helpers as listed in the `credHelpers` section.\n  const { credsStore, credHelpers } = await getCredentialHelperInfo('list', '');\n  const results = JSON.parse(await runCredHelper(credsStore, 'list'));\n  const helperNames = new Set(Object.values(credHelpers ?? {}));\n\n  for (const helperName of helperNames) {\n    try {\n      const additionalResults = JSON.parse(await runCredHelper(helperName, 'list'));\n\n      for (const [url, username] of Object.entries(additionalResults)) {\n        if (credHelpers[url] === helperName) {\n          results[url] = username;\n        }\n      }\n    } catch (err) {\n      console.debug(`Failed to get credential list for helper ${ helperName }: ${ err }`);\n    }\n  }\n\n  return results;\n}\n\n/**\n * Returns the name of the credential-helper to use (which is a suffix of the helper `docker-credential-`).\n *\n * Note that callers are responsible for catching exceptions, which usually happens if the\n * `$HOME/docker/config.json` doesn't exist, its JSON is corrupt, or it doesn't have a `credsStore` field.\n */\nasync function getCredentialHelperInfo(command: string, payload: string): Promise<credHelperInfo> {\n  const home = findHomeDir();\n  const dockerConfig = path.join(home ?? '', '.docker', 'config.json');\n  const contents = JSON.parse(await fs.promises.readFile(dockerConfig, { encoding: 'utf-8' }));\n  const credHelpers = contents.credHelpers;\n  const credsStore = contents.credsStore;\n\n  if (credHelpers) {\n    let credsStoreOverride = '';\n\n    switch (command) {\n    case 'erase':\n    case 'get':\n      credsStoreOverride = credHelpers[payload.trim()];\n      break;\n    case 'store': {\n      const obj = JSON.parse(payload);\n\n      credsStoreOverride = obj.ServerURL ? credHelpers[obj.ServerURL] : '';\n    }\n    }\n    if (credsStoreOverride) {\n      return { credsStore: credsStoreOverride, credHelpers: { } };\n    }\n  }\n\n  return { credsStore, credHelpers };\n}\n\n/**\n * Run the credential helper, with minimal checking.\n * @param helper The name of the credential helper to use (a suffix of `docker-credential-`)\n * @param command The one-word command to run\n * @param input Any input to the helper, to be sent as standard input.\n */\nasync function runCredHelper(helper: string, command: string, input?: string): Promise<string> {\n  // The PATH needs to contain our resources directory (on macOS that would\n  // not be in the application's PATH).\n  // NOTE: This needs to match DockerDirManager.spawnFileWithExtraPath\n  const pathVar = (process.env.PATH ?? '').split(path.delimiter).filter(x => x);\n\n  pathVar.push(path.join(paths.resources, process.platform, 'bin'));\n\n  const helperName = `docker-credential-${ helper }`;\n  const body = stream.Readable.from(input ?? '');\n  const { stdout } = await spawnFile(helperName, [command], {\n    env:   { ...process.env, PATH: pathVar.join(path.delimiter) },\n    stdio: [body, 'pipe', console],\n  });\n\n  return stdout;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/credentialServer/httpCredentialHelperServer.ts",
    "content": "import fs from 'fs';\nimport http from 'http';\nimport path from 'path';\nimport { URL } from 'url';\n\nimport runCredentialHelper from './credentialUtils';\n\nimport * as serverHelper from '@pkg/main/serverHelper';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\n\nexport interface ServerState {\n  user:     string;\n  password: string;\n  port:     number;\n  pid:      number;\n}\n\nconst SERVER_PORT = 6109;\nconst console = Logging.server;\nconst SERVER_USERNAME = 'user';\nconst SERVER_FILE_BASENAME = 'credential-server.json';\nconst MAX_REQUEST_BODY_LENGTH = 4194304; // 4MiB\n\ntype checkerFnType = (stdout: string) => boolean;\n\nfunction requireNoOutput(stdout: string): boolean {\n  return !stdout;\n}\n\nfunction requireNonEmptyOutput(stdout: string): boolean {\n  return !!stdout.length;\n}\n\nfunction requireJSONOutput(stdout: string): boolean {\n  try {\n    JSON.parse(stdout);\n\n    return true;\n  } catch {\n  }\n\n  return false;\n}\n\nexport function getServerCredentialsPath(): string {\n  return path.join(paths.appHome, SERVER_FILE_BASENAME);\n}\n\nfunction ensureEndsWithNewline(s: string) {\n  return !s || s.endsWith('\\n') ? s : `${ s }\\n`;\n}\n\nexport class HttpCredentialHelperServer {\n  protected server = http.createServer();\n  protected password = serverHelper.randomStr();\n  protected stateInfo: ServerState = {\n    user:     SERVER_USERNAME,\n    password: this.password,\n    port:     SERVER_PORT,\n    pid:      process.pid,\n  };\n\n  protected listenAddr = '127.0.0.1';\n\n  async init() {\n    const statePath = getServerCredentialsPath();\n\n    await fs.promises.writeFile(statePath,\n      jsonStringifyWithWhiteSpace(this.stateInfo),\n      { mode: 0o600 });\n    this.server.on('request', this.handleRequest.bind(this));\n    this.server.on('error', (err) => {\n      console.error(`Error writing out ${ statePath }`, err);\n    });\n    this.server.listen(SERVER_PORT, this.listenAddr);\n    console.log('Credentials server is now ready.');\n  }\n\n  protected async handleRequest(request: http.IncomingMessage, response: http.ServerResponse) {\n    try {\n      if (serverHelper.basicAuth({ [SERVER_USERNAME]: this.password }, request.headers.authorization ?? '') !==\n        SERVER_USERNAME) {\n        response.writeHead(401, { 'Content-Type': 'text/plain' });\n\n        return;\n      }\n      const url = new URL(request.url ?? '', `http://${ request.headers.host }`);\n      const path = url.pathname;\n      const pathParts = path.split('/');\n\n      if (pathParts.shift()) {\n        console.debug(`FAILURE: Processing request ${ request.method } ${ path }`);\n        response.writeHead(400, { 'Content-Type': 'text/plain' });\n        response.write(`Unexpected data before first / in URL ${ path }`);\n\n        return;\n      }\n      const commandName = pathParts[0];\n      const [data, error, errorCode] = await serverHelper.getRequestBody(request, MAX_REQUEST_BODY_LENGTH);\n\n      if (error) {\n        console.debug(`FAILURE: Processing request ${ request.method } ${ path }`);\n        console.debug(`${ path }: write back status ${ errorCode }, error: ${ error }`);\n        response.writeHead(errorCode, { 'Content-Type': 'text/plain' });\n        response.write(error);\n\n        return;\n      }\n\n      await this.doCommand(commandName, data, request, response);\n    } catch (err) {\n      console.debug(`FAILURE: Processing request ${ request.method } ${ path }`);\n      console.log(`Error handling ${ request.url }`, err);\n      response.writeHead(500, { 'Content-Type': 'text/plain' });\n      response.write('Error processing request.');\n    } finally {\n      response.end();\n    }\n  }\n\n  protected async doCommand(\n    commandName: string,\n    data: string,\n    request: http.IncomingMessage,\n    response: http.ServerResponse): Promise<void> {\n    try {\n      const stdout = await this.runCommand(commandName, data, request);\n\n      response.writeHead(200, { 'Content-Type': 'text/plain' });\n      response.write(ensureEndsWithNewline(stdout));\n    } catch (err: any) {\n      const stderr = (err.stderr || err.stdout) ?? err.toString();\n      const helperName = err.helper ?? '<unknown>';\n\n      console.debug(`FAILURE: Processing request ${ commandName } with credential helper ${ helperName }`);\n      response.writeHead(400, { 'Content-Type': 'text/plain' });\n      response.write(ensureEndsWithNewline(stderr));\n    }\n  }\n\n  protected async runCommand(\n    commandName: string,\n    data: string,\n    request: http.IncomingMessage): Promise<string> {\n    let requestCheckError: any = null;\n    const checkers: Record<string, checkerFnType> = {\n      list:  requireJSONOutput,\n      // When pass starts throwing an exception for a failed 'get', this can change from\n      // requireNonEmptyOutput to requireJSONOutput, and requireNonEmptyOutput can be deleted.\n      get:   requireNonEmptyOutput,\n      erase: requireNoOutput,\n      store: requireNoOutput,\n    };\n    const checkerFn: checkerFnType | undefined = checkers[commandName];\n\n    if (request.method !== 'POST') {\n      requestCheckError = `Expecting a POST method for the credential-server list request, received ${ request.method }`;\n    } else if (!checkerFn) {\n      requestCheckError = `Unknown credential action '${ commandName }' for the credential-server, must be one of [${ Object.keys(checkers).sort().join('|') }]`;\n    }\n    if (requestCheckError) {\n      throw new Error(requestCheckError);\n    }\n\n    const output = await runCredentialHelper(commandName, data);\n\n    if (!checkerFn(output)) {\n      throw new Error(`Invalid output for ${ commandName } command.`);\n    }\n\n    return output;\n  }\n\n  closeServer() {\n    this.server.close();\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/dashboardServer/index.ts",
    "content": "import { Server } from 'http';\nimport net from 'net';\nimport path from 'path';\n\nimport express from 'express';\nimport { createProxyMiddleware, Options } from 'http-proxy-middleware';\n\nimport { proxyWsOpts, proxyOpts } from './proxyUtils';\n\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst ProxyKeys = ['/k8s', '/pp', '/api', '/apis', '/v1', '/v3', '/v3-public', '/api-ui', '/meta', '/v1-*etc'] as const;\n\ntype ProxyKeys = typeof ProxyKeys[number];\n\nconst console = Logging.dashboardServer;\n\n/**\n * Singleton that manages the lifecycle of the Dashboard server.\n */\nexport class DashboardServer {\n  private static instance: DashboardServer;\n\n  private dashboardServer = express();\n  private dashboardApp: Server = new Server();\n  private host = '127.0.0.1';\n  private port = 6120;\n  private api = 'https://127.0.0.1:9443';\n\n  private proxies = (() => {\n    const proxy: Record<ProxyKeys, Options> = {\n      '/k8s':       proxyWsOpts, // Straight to a remote cluster (/k8s/clusters/<id>/)\n      '/pp':        proxyWsOpts, // For (epinio) standalone API\n      '/api':       proxyWsOpts, // Management k8s API\n      '/apis':      proxyWsOpts, // Management k8s API\n      '/v1':        proxyWsOpts, // Management Steve API\n      '/v3':        proxyWsOpts, // Rancher API\n      '/api-ui':    proxyOpts, // Browser API UI\n      '/v3-public': proxyOpts, // Rancher Unauthed API\n      '/meta':      proxyOpts, // Browser API UI\n      '/v1-*etc':   proxyOpts, // SAML, KDM, etc\n    };\n    const entries = Object.entries(proxy).map(([key, options]) => {\n      return [key, createProxyMiddleware({ ...options, target: this.api + key })] as const;\n    });\n\n    return Object.fromEntries(entries);\n  })();\n\n  /**\n   * Checks for an existing instance of Dashboard server.\n   * Instantiate a new one if it does not exist.\n   */\n  public static getInstance(): DashboardServer {\n    DashboardServer.instance ??= new DashboardServer();\n\n    return DashboardServer.instance;\n  }\n\n  /**\n   * Starts the Dashboard server if one is not already running.\n   */\n  public init() {\n    if (this.dashboardApp.address()) {\n      console.log(`Dashboard Server is already listening on ${ this.host }:${ this.port }`);\n\n      return;\n    }\n\n    ProxyKeys.forEach((key) => {\n      this.dashboardServer.use(key, this.proxies[key]);\n    });\n\n    this.dashboardApp = this.dashboardServer\n      // handle static assets, e.g. image, icons, fonts, and index.html\n      .use(\n        express.static(\n          path.join(paths.resources, 'rancher-dashboard'),\n        ))\n      /**\n       * Handle all routes that we don't account for, return index.html and let\n       * Vue router take over.\n       */\n      .get(\n        '*missing',\n        (_req, res) => {\n          // Send the dashboard index file relative to the resources path, to\n          // avoid Express checking the (not in our case) user-controlled path\n          // containing hidden directories.  We do not need a rate limit here\n          // because this is all the local user triggering requests.\n          res.sendFile('rancher-dashboard/index.html', { root: paths.resources });\n        })\n      .listen(this.port, this.host)\n      .on('upgrade', (req, socket, head) => {\n        if (!(socket instanceof net.Socket)) {\n          console.log(`Invalid upgrade for ${ req.url }`);\n\n          return;\n        }\n\n        if (req.url?.startsWith('/v1')) {\n          return this.proxies['/v1'].upgrade(req, socket, head);\n        } else if (req.url?.startsWith('/v3')) {\n          return this.proxies['/v3'].upgrade(req, socket, head);\n        } else if (req.url?.startsWith('/k8s/')) {\n          return this.proxies['/k8s'].upgrade(req, socket, head);\n        } else if (req.url?.startsWith('/api/')) {\n          return this.proxies['/api'].upgrade(req, socket, head);\n        } else {\n          console.log(`Unknown Web socket upgrade request for ${ req.url }`);\n        }\n      });\n  }\n\n  /**\n   * Stop the Dashboard server.\n   */\n  public stop() {\n    if (!this.dashboardApp.address()) {\n      return;\n    }\n\n    this.dashboardApp.close();\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/dashboardServer/proxyUtils.ts",
    "content": "import { ClientRequest } from 'http';\nimport { Socket } from 'net';\n\nimport { Options } from 'http-proxy-middleware';\n\nimport Logging from '@pkg/utils/logging';\n\nimport type { ErrorCallback, ProxyReqCallback, ProxyReqWsCallback } from 'http-proxy';\n\nconst console = Logging.dashboardServer;\n\nconst onProxyReq: ProxyReqCallback = (clientReq, req) => {\n  const actualClientReq: ClientRequest | undefined = (clientReq as any)._currentRequest;\n\n  if (!actualClientReq?.headersSent) {\n    if (req.headers.host) {\n      clientReq.setHeader('x-api-host', req.headers.host);\n    }\n    clientReq.setHeader('x-forwarded-proto', 'https');\n  }\n};\n\nconst onProxyReqWs: ProxyReqWsCallback = (clientReq, req, socket, options) => {\n  const target = options?.target as Partial<URL> | undefined;\n\n  if (!target?.href) {\n    console.error(`onProxyReqWs: No target href, aborting`);\n    req.destroy(new Error(`onProxyReqWs: no target href`));\n\n    return;\n  }\n  if (target.pathname && clientReq.path.startsWith(target.pathname)) {\n    // `options.prependPath` is required for non-websocket requests to be routed\n    // correctly; this means that we end up with the prepended path here, but\n    // that does not work in this case.  Therefore we need to manually strip off\n    // the prepended path here before passing it to the backend.\n    clientReq.path = clientReq.path.substring(target.pathname.length);\n  }\n  req.headers.origin = target.href;\n  clientReq.setHeader('origin', target.href);\n  if (req.headers.host) {\n    clientReq.setHeader('x-api-host', req.headers.host);\n  }\n  clientReq.setHeader('x-forwarded-proto', 'https');\n\n  socket.on('error', err => console.error('Proxy WS Error:', err));\n};\n\nconst onError: ErrorCallback = (err, req, res) => {\n  console.error('Proxy Error:', err);\n  if (res instanceof Socket) {\n    res.destroy(err);\n  } else {\n    res.statusCode = 598; // (Informal) Network read timeout error\n    res.write(JSON.stringify(err));\n  }\n};\n\nexport const proxyOpts: Omit<Options, 'target'> = {\n  followRedirects: true,\n  secure:          false,\n  logger:          console,\n  on:              {\n    proxyReq:   onProxyReq,\n    proxyReqWs: onProxyReqWs,\n    error:      onError,\n  },\n};\n\nexport const proxyWsOpts: Omit<Options, 'target'> = {\n  ...proxyOpts,\n  ws:           false,\n  changeOrigin: true,\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/main/deploymentProfiles.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport { join } from 'path';\nimport stream from 'stream';\n\nimport _ from 'lodash';\nimport * as nativeReg from 'native-reg';\n\nimport * as settings from '@pkg/config/settings';\nimport * as settingsImpl from '@pkg/config/settingsImpl';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nconst console = Logging.deploymentProfile;\n\nexport class DeploymentProfileError extends Error {\n  toString() {\n    // This is needed on linux. Without it, we get a randomish replacement\n    // for 'DeploymentProfileError' (like 'ys Error')\n    return `DeploymentProfileError: ${ this.message }`;\n  }\n}\n\nconst REGISTRY_PROFILE_PATHS = [\n  ['SOFTWARE', 'Policies', 'Rancher Desktop'], // Recommended (default) location\n  ['SOFTWARE', 'Rancher Desktop', 'Profile'], // Old location for backward-compatibility\n];\n\n/**\n * Read and validate deployment profiles, giving system level profiles\n * priority over user level profiles.  If the system directory contains a\n * defaults or locked profile, the user directory will not be read.\n * @returns type validated defaults and locked deployment profiles, and throws\n *          an error if there is an error parsing the locked profile.\n * NOTE: The renderer process cannot access the 'native-reg' library, so the\n *       win32 portions of the deployment profile reader functions must be\n *       located in the main process.\n */\n\nexport async function readDeploymentProfiles(registryProfilePath = REGISTRY_PROFILE_PATHS): Promise<settings.DeploymentProfileType> {\n  if (process.platform === 'win32') {\n    const win32DeploymentReader = new Win32DeploymentReader(registryProfilePath);\n\n    return Promise.resolve(win32DeploymentReader.readProfile());\n  }\n  const profiles: settings.DeploymentProfileType = {\n    defaults: {},\n    locked:   {},\n  };\n  let defaults: undefined | RecursivePartial<settings.Settings>;\n  let locked: undefined | RecursivePartial<settings.Settings>;\n  let fullDefaultPath = '';\n  let fullLockedPath = '';\n\n  switch (os.platform()) {\n  case 'linux': {\n    const linuxPaths = {\n      [paths.deploymentProfileSystem]:    ['defaults.json', 'locked.json'],\n      [paths.altDeploymentProfileSystem]: ['defaults.json', 'locked.json'],\n      [paths.deploymentProfileUser]:      ['rancher-desktop.defaults.json', 'rancher-desktop.locked.json'],\n    };\n\n    for (const configDir in linuxPaths) {\n      const [defaultPath, lockedPath] = linuxPaths[configDir];\n\n      [defaults, locked] = parseJsonFiles(configDir, defaultPath, lockedPath);\n      fullDefaultPath = join(configDir, defaultPath);\n      fullLockedPath = join(configDir, lockedPath);\n      if (typeof defaults !== 'undefined' || typeof locked !== 'undefined') {\n        break;\n      }\n    }\n    break;\n  }\n\n  case 'darwin':\n    for (const rootPath of [paths.deploymentProfileSystem, paths.altDeploymentProfileSystem, paths.deploymentProfileUser]) {\n      [defaults, locked] = await parseJsonFromPlists(rootPath, 'io.rancherdesktop.profile.defaults.plist', 'io.rancherdesktop.profile.locked.plist');\n      fullDefaultPath = join(rootPath, 'io.rancherdesktop.profile.defaults.plist');\n      fullLockedPath = join(rootPath, 'io.rancherdesktop.profile.locked.plist');\n      if (typeof defaults !== 'undefined' || typeof locked !== 'undefined') {\n        break;\n      }\n    }\n    break;\n  }\n  if (defaults) {\n    if (!('version' in defaults)) {\n      throw new DeploymentProfileError(`Invalid deployment file ${ fullDefaultPath }: no version specified. You'll need to add a version field to make it valid (current version is ${ settings.CURRENT_SETTINGS_VERSION }).`);\n    }\n    defaults = settingsImpl.migrateSpecifiedSettingsToCurrentVersion(defaults, false);\n  }\n  if (locked) {\n    if (!('version' in locked)) {\n      throw new DeploymentProfileError(`Invalid deployment file ${ fullLockedPath }: no version specified. You'll need to add a version field to make it valid (current version is ${ settings.CURRENT_SETTINGS_VERSION }).`);\n    }\n    locked = settingsImpl.migrateSpecifiedSettingsToCurrentVersion(locked, true);\n  }\n\n  profiles.defaults = validateDeploymentProfile(fullDefaultPath, defaults, settings.defaultSettings, []) ?? {};\n  profiles.locked = validateDeploymentProfile(fullLockedPath, locked, settings.defaultSettings, []) ?? {};\n\n  return profiles;\n}\n\n// This function can't call `plutil` directly with `inputPath`, because unit-testing mocks `fs.readFileSync`\n// So read the text into a string variable, and have `plutil` read it via stdin.\n// It's no error if a deployment profile doesn't exist.\n// Any other error needs to show up in a dialog box and terminate processing.\nasync function convertAndParsePlist(inputPath: string): Promise<undefined | RecursivePartial<settings.Settings>> {\n  let plutilResult: { stdout?: string, stderr?: string };\n  let body: stream.Readable;\n  const args = ['-convert', 'json', '-r', '-o', '-', '-'];\n  const getErrorString = (error: any) => error.stdout || error.stderr || error.toString();\n\n  try {\n    body = stream.Readable.from(fs.readFileSync(inputPath));\n  } catch (error: any) {\n    if (error.code === 'ENOENT') {\n      return;\n    }\n    console.log(`Error reading file ${ inputPath }\\n${ error }`);\n    throw new DeploymentProfileError(`Error reading file ${ inputPath }: ${ getErrorString(error) }`);\n  }\n  try {\n    plutilResult = await spawnFile('plutil', args, { stdio: [body, 'pipe', 'pipe'] });\n  } catch (error: any) {\n    console.log(`Error parsing deployment profile plist file ${ inputPath }`, error);\n    const msg = `Error loading plist file ${ inputPath }: ${ getErrorString(error) }`;\n\n    throw new DeploymentProfileError(msg);\n  }\n\n  try {\n    return JSON.parse(plutilResult.stdout ?? '');\n  } catch (error: any) {\n    console.log(`Error parsing deployment profile JSON object ${ inputPath }\\n${ error }`);\n    throw new DeploymentProfileError(`Error parsing deployment profile JSON object from ${ inputPath }: ${ getErrorString(error) }`);\n  }\n}\n\n/**\n * Read and parse plutil deployment profile files.\n * @param rootPath the system or user directory containing profiles.\n * @param defaultsPath the file path to the 'defaults' file.\n * @param lockedPath the file path to the 'locked' file.\n * @returns the defaults and/or locked objects if they exist, or\n *          throws an exception if there is an error parsing the locked file.\n */\n\nasync function parseJsonFromPlists(rootPath: string, defaultsPath: string, lockedPath: string): Promise<(undefined | RecursivePartial<settings.Settings>)[]> {\n  return [\n    await convertAndParsePlist(join(rootPath, defaultsPath)),\n    await convertAndParsePlist(join(rootPath, lockedPath)),\n  ];\n}\n\n/**\n * Read and parse deployment profile files.\n * @param rootPath the system or user directory containing profiles.\n * @param defaultsPath the file path to the 'defaults' file.\n * @param lockedPath the file path to the 'locked' file.\n * @returns the defaults and/or locked objects if they exist, or\n *          throws an exception if there is an error parsing the locked file.\n */\nfunction parseJsonFiles(rootPath: string, defaultsPath: string, lockedPath: string): (undefined | RecursivePartial<settings.Settings>)[] {\n  return [defaultsPath, lockedPath].map((configPath) => {\n    const fullPath = join(rootPath, configPath);\n\n    try {\n      return JSON.parse(fs.readFileSync(fullPath, 'utf-8'));\n    } catch (ex: any) {\n      if (ex.code !== 'ENOENT') {\n        throw new DeploymentProfileError(`Error parsing deployment profile from ${ fullPath }: ${ ex }`);\n      }\n    }\n  });\n}\n\n/**\n * Win32DeploymentReader - encapsulate details about the registry in this class.\n */\nclass Win32DeploymentReader {\n  protected registryPathProfiles: string[][];\n  protected registryPathCurrent:  string[];\n  protected keyName = '';\n  protected errors:               string[] = [];\n\n  constructor(registryPathProfiles: string[][]) {\n    this.registryPathProfiles = registryPathProfiles;\n    this.registryPathCurrent = [];\n  }\n\n  readProfile(): settings.DeploymentProfileType {\n    const DEFAULTS_HIVE_NAME = 'Defaults';\n    const LOCKED_HIVE_NAME = 'Locked';\n    let defaults: RecursivePartial<settings.Settings> = {};\n    let locked: RecursivePartial<settings.Settings> = {};\n\n    this.errors = [];\n    for (this.registryPathCurrent of this.registryPathProfiles) {\n      for (const keyName of ['HKLM', 'HKCU'] as const) {\n        this.keyName = keyName;\n        const key = nativeReg[keyName];\n        const registryKey = nativeReg.openKey(key, this.registryPathCurrent.join('\\\\'), nativeReg.Access.READ);\n\n        if (!registryKey) {\n          continue;\n        }\n        const defaultsKey = nativeReg.openKey(registryKey, DEFAULTS_HIVE_NAME, nativeReg.Access.READ);\n        const lockedKey = nativeReg.openKey(registryKey, LOCKED_HIVE_NAME, nativeReg.Access.READ);\n\n        try {\n          defaults = defaultsKey ? this.readRegistryUsingSchema(settings.defaultSettings, defaultsKey, [DEFAULTS_HIVE_NAME]) : {};\n          locked = lockedKey ? this.readRegistryUsingSchema(settings.defaultSettings, lockedKey, [LOCKED_HIVE_NAME]) : {};\n        } catch (err) {\n          console.error('Error reading deployment profile: ', err);\n        } finally {\n          nativeReg.closeKey(registryKey);\n          nativeReg.closeKey(defaultsKey);\n          nativeReg.closeKey(lockedKey);\n        }\n\n        // Don't bother with the validator, because the registry-based reader validates as it reads.\n        if (this.errors.length) {\n          throw new DeploymentProfileError(`Error in registry settings:\\n${ this.errors.join('\\n') }`);\n        }\n\n        // If we found something in the HKLM Defaults or Locked registry hive, don't look at the user's\n        // Alternatively, if the keys work, we could break, even if both hives are empty.\n        if (!_.isEmpty(defaults) || !_.isEmpty(locked)) {\n          if (!_.isEmpty(defaults)) {\n            if (!('version' in defaults)) {\n              const registryPath = [keyName, ...this.registryPathCurrent, DEFAULTS_HIVE_NAME].join('\\\\');\n\n              throw new DeploymentProfileError(`Invalid default-deployment: no version specified at ${ registryPath }. You'll need to add a version field to make it valid (current version is ${ settings.CURRENT_SETTINGS_VERSION }).`);\n            }\n            defaults = settingsImpl.migrateSpecifiedSettingsToCurrentVersion(defaults, false);\n          }\n          if (!_.isEmpty(locked)) {\n            if (!('version' in locked)) {\n              const registryPath = [keyName, ...this.registryPathCurrent, LOCKED_HIVE_NAME].join('\\\\');\n\n              throw new DeploymentProfileError(`Invalid locked-deployment: no version specified at ${ registryPath }. You'll need to add a version field to make it valid (current version is ${ settings.CURRENT_SETTINGS_VERSION }).`);\n            }\n            locked = settingsImpl.migrateSpecifiedSettingsToCurrentVersion(locked, true);\n          }\n\n          return { defaults, locked };\n        }\n      }\n    }\n\n    return { defaults, locked };\n  }\n\n  protected fullRegistryPath(...pathParts: string[]): string {\n    return `${ this.keyName }\\\\${ this.registryPathCurrent.join('\\\\') }\\\\${ pathParts.join('\\\\') }`;\n  }\n\n  protected msgFieldExpectingReceived(field: string, expected: string, received: string) {\n    return `Error for field '${ field }': expecting ${ expected }, got ${ received }`;\n  }\n\n  protected msgFieldExpectingTypeReceived(field: string, expectedType: string, received: string) {\n    return this.msgFieldExpectingReceived(field, `value of type ${ expectedType }`, received);\n  }\n\n  /**\n   * Windows only. Read settings values from registry using schemaObj as a template.\n   * @param schemaObj the object used as a template for navigating registry.\n   * @param regKey the registry key obtained from nativeReg.openKey().\n   * @param pathParts the relative path to the current registry key, starting at 'Defaults' or 'Locked'\n   * @returns null, or the registry data as an object.\n   */\n  protected readRegistryUsingSchema(schemaObj: Record<string, any>, regKey: nativeReg.HKEY, pathParts: string[]): Record<string, any> {\n    const newObject: Record<string, any> = {};\n    const schemaKeys = Object.keys(schemaObj);\n    const commonKeys: { schemaKey: string, registryKey: string }[] = [];\n    const unknownKeys: string[] = [];\n    const userDefinedObjectKeys: { schemaKey: string, registryKey: string }[] = [];\n    let regValue: any;\n\n    // Drop the initial 'defaults' or 'locked' field\n    const pathPartsWithoutHiveType = pathParts.slice(1);\n\n    for (const registryKey of nativeReg.enumKeyNames(regKey)) {\n      const schemaKey = fixProfileKeyCase(registryKey, schemaKeys);\n      // \"fixed case\" means mapping existing keys in the registry (which typically supports case-insensitive lookups)\n      // to the actual case in the schema.\n\n      if (schemaKey === null) {\n        unknownKeys.push(registryKey);\n      } else if (haveUserDefinedObject(pathPartsWithoutHiveType.concat(schemaKey))) {\n        userDefinedObjectKeys.push({ schemaKey, registryKey });\n      } else {\n        commonKeys.push({ schemaKey, registryKey });\n      }\n    }\n    if (unknownKeys.length) {\n      unknownKeys.sort(caseInsensitiveComparator.compare);\n      console.error(`Unrecognized keys in registry at ${ this.fullRegistryPath(...pathParts) }: [${ unknownKeys.join(', ') }]`);\n    }\n\n    // First process the nested keys, then process any values\n    for (const { schemaKey, registryKey } of commonKeys) {\n      const schemaVal = schemaObj[schemaKey];\n\n      if ((typeof schemaVal) !== 'object' || schemaVal === null) {\n        const valueType = schemaVal === null ? 'null' : (typeof schemaVal);\n        const msg = this.msgFieldExpectingTypeReceived(this.fullRegistryPath(...pathParts, registryKey), valueType, 'a registry object');\n\n        console.error(msg);\n        this.errors.push(msg);\n        continue;\n      }\n      const innerKey = nativeReg.openKey(regKey, registryKey, nativeReg.Access.READ);\n\n      if (!innerKey) {\n        continue;\n      }\n      try {\n        regValue = this.readRegistryUsingSchema(schemaVal, innerKey, pathParts.concat([schemaKey]));\n      } finally {\n        nativeReg.closeKey(innerKey);\n      }\n      if (Object.keys(regValue).length) {\n        newObject[schemaKey] = regValue;\n      }\n    }\n    for (const { schemaKey, registryKey } of userDefinedObjectKeys) {\n      const innerKey = nativeReg.openKey(regKey, registryKey, nativeReg.Access.READ);\n\n      if (innerKey === null) {\n        console.error(`No value for registry object ${ this.fullRegistryPath(...pathParts, registryKey) }`);\n        continue;\n      }\n      try {\n        regValue = this.readRegistryObject(innerKey, pathParts.concat([schemaKey]), true);\n      } catch (err: any) {\n        const msg = `Error getting registry object for ${ this.fullRegistryPath(...pathParts, registryKey) }`;\n\n        console.error(msg, err);\n        this.errors.push(msg);\n      } finally {\n        nativeReg.closeKey(innerKey);\n      }\n      if (regValue) {\n        newObject[schemaKey] = regValue;\n      }\n    }\n    const unknownValueNames: string[] = [];\n\n    for (const originalName of nativeReg.enumValueNames(regKey)) {\n      const schemaKey = fixProfileKeyCase(originalName, schemaKeys);\n\n      if (schemaKey === null) {\n        unknownValueNames.push(originalName);\n      } else {\n        regValue = this.readRegistryValue(schemaObj[schemaKey], regKey, pathParts, originalName);\n        if (regValue !== null) {\n          newObject[schemaKey] = regValue;\n        }\n      }\n    }\n    if (unknownValueNames.length > 0) {\n      unknownValueNames.sort(caseInsensitiveComparator.compare);\n      console.error(`Unrecognized value names in registry at ${ this.fullRegistryPath(...pathParts) }: [${ unknownValueNames.join(', ') }]`);\n    }\n\n    return newObject;\n  }\n\n  protected readRegistryObject(regKey: nativeReg.HKEY, pathParts: string[], isUserDefinedObject = false) {\n    const newObject: Record<string, string[] | string | boolean | number> = {};\n\n    for (const k of nativeReg.enumValueNames(regKey)) {\n      let newValue = this.readRegistryValue(undefined, regKey, pathParts, k, isUserDefinedObject);\n\n      if (newValue !== null) {\n        if (isUserDefinedObject && (typeof newValue) === 'number') {\n          // Currently all user-defined objects are either\n          // Record<string, string> or Record<string, boolean>\n          // The registry can't store boolean values, only numbers, so we assume true and false\n          // are stored as 1 and 0, respectively. Any other numeric values are considered errors.\n          switch (newValue) {\n          case 0:\n            newValue = false;\n            break;\n          case 1:\n            newValue = true;\n            break;\n          default: {\n            const msg = this.msgFieldExpectingTypeReceived(this.fullRegistryPath(...pathParts), 'boolean', `'${ newValue }'`);\n\n            console.error(msg);\n            this.errors.push(msg);\n          }\n          }\n        }\n        newObject[k] = newValue;\n      }\n    }\n\n    return newObject;\n  }\n\n  protected readRegistryValue(schemaVal: any, regKey: nativeReg.HKEY, pathParts: string[], valueName: string, isUserDefinedObject = false): string[] | string | boolean | number | null {\n    const fullPath = `${ this.fullRegistryPath(...pathParts, valueName) }`;\n    const valueTypeNames = [\n      'NONE', // 0\n      'SZ',\n      'EXPAND_SZ',\n      'BINARY',\n      'DWORD',\n      'DWORD_BIG_ENDIAN',\n      'LINK',\n      'MULTI_SZ',\n      'RESOURCE_LIST',\n      'FULL_RESOURCE_DESCRIPTOR',\n      'RESOURCE_REQUIREMENTS_LIST',\n      'QWORD',\n    ];\n    const rawValue = nativeReg.queryValueRaw(regKey, valueName);\n    let parsedValueForErrorMessage = nativeReg.queryValue(regKey, valueName);\n\n    try {\n      parsedValueForErrorMessage = JSON.stringify(parsedValueForErrorMessage);\n    } catch { }\n\n    if (rawValue === null) {\n      // This shouldn't happen\n      return null;\n    } else if (!isUserDefinedObject && schemaVal && typeof schemaVal === 'object' && !Array.isArray(schemaVal)) {\n      const msg = this.msgFieldExpectingTypeReceived(fullPath, 'object', `a ${ valueTypeNames[rawValue.type] }, value: '${ parsedValueForErrorMessage }'`);\n\n      console.error(msg);\n      this.errors.push(msg);\n\n      return null;\n    }\n    const expectingArray = Array.isArray(schemaVal);\n\n    switch (rawValue.type) {\n    case nativeReg.ValueType.SZ:\n      if (isUserDefinedObject || (typeof schemaVal) === 'string') {\n        return nativeReg.parseString(rawValue);\n      } else if (expectingArray) {\n        return [nativeReg.parseString(rawValue)];\n      } else {\n        const msg = this.msgFieldExpectingTypeReceived(fullPath, typeof schemaVal, `'${ parsedValueForErrorMessage }'`);\n\n        console.error(msg);\n        this.errors.push(msg);\n      }\n      break;\n    case nativeReg.ValueType.DWORD:\n    case nativeReg.ValueType.DWORD_LITTLE_ENDIAN:\n    case nativeReg.ValueType.DWORD_BIG_ENDIAN:\n      if (expectingArray) {\n        const msg = this.msgFieldExpectingTypeReceived(fullPath, 'array', `'${ parsedValueForErrorMessage }'`);\n\n        console.error(msg);\n        this.errors.push(msg);\n      } else if (isUserDefinedObject || (typeof schemaVal) === 'boolean' || (typeof schemaVal) === 'number') {\n        // Otherwise the schema type is number or boolean. If it's boolean, reduce it to true/false\n        const parsedValue = nativeReg.parseValue(rawValue) as number;\n\n        return (typeof schemaVal === 'boolean') ? !!parsedValue : parsedValue;\n      } else {\n        const msg = this.msgFieldExpectingTypeReceived(fullPath, typeof schemaVal, `'${ parsedValueForErrorMessage }'`);\n\n        console.error(msg);\n        this.errors.push(msg);\n      }\n      break;\n    case nativeReg.ValueType.MULTI_SZ:\n      if (expectingArray) {\n        return nativeReg.parseMultiString(rawValue);\n      } else {\n        const msg = this.msgFieldExpectingTypeReceived(fullPath, typeof schemaVal, `an array '${ parsedValueForErrorMessage }'`);\n\n        console.error(msg);\n        this.errors.push(msg);\n      }\n      break;\n    default: {\n      const msg = `Error for field '${ fullPath }': don't know how to process a registry entry of type ${ valueTypeNames[rawValue.type] }`;\n\n      console.error(msg);\n      this.errors.push(msg);\n    }\n    }\n\n    return null;\n  }\n}\n\n/**\n * Do simple type validation of a deployment profile\n * @param inputPath Used for error messages only\n * @param profile The profile to be validated\n * @param schema The structure (usually defaultSettings) used as a template\n * @param parentPathParts The parent path for the current schema key.\n * @returns The original profile, less any invalid fields\n */\nexport function validateDeploymentProfile(inputPath: string, profile: any, schema: any, parentPathParts: string[]): RecursivePartial<settings.Settings> {\n  const errors: string[] = [];\n\n  validateDeploymentProfileWithErrors(profile, errors, schema, parentPathParts);\n  if (errors.length) {\n    throw new DeploymentProfileError(`Error in deployment file ${ inputPath }:\\n${ errors.join('\\n') }`);\n  }\n\n  return profile;\n}\n\n/**\n * Do simple type validation of a deployment profile\n * @param profile The profile to be validated, modified in place\n * @param errors An array of error messages, built up in place\n * @param schema The structure (usually defaultSettings) used as a template\n * @param parentPathParts The parent path for the current schema key.\n * @returns The original profile, less any invalid fields\n */\nfunction validateDeploymentProfileWithErrors(profile: any, errors: string[], schema: any, parentPathParts: string[]) {\n  if (typeof profile !== 'object') {\n    return profile;\n  }\n  const fullPath = (key: string) => {\n    return [...parentPathParts, key].join('.');\n  };\n\n  for (const key in profile) {\n    if (!(key in schema)) {\n      console.log(`Deployment Profile ignoring '${ fullPath(key) }': not in schema.`);\n      delete profile[key];\n      continue;\n    }\n    const schemaVal = schema[key];\n    const profileVal = profile[key];\n\n    if (Array.isArray(profileVal) || Array.isArray(schemaVal)) {\n      if (Array.isArray(profileVal) !== Array.isArray(schemaVal)) {\n        if (Array.isArray(schemaVal)) {\n          errors.push(`Error for field '${ fullPath(key) }': expecting value of type array, got '${ JSON.stringify(profileVal) }'`);\n        } else {\n          errors.push(`Error for field '${ fullPath(key) }': expecting value of type ${ typeof schemaVal }, got an array ${ JSON.stringify(profileVal) }`);\n        }\n      }\n    } else if (typeof profileVal !== 'object') {\n      if (typeof profileVal !== typeof schemaVal) {\n        errors.push(`Error for field '${ fullPath(key) }': expecting value of type ${ typeof schemaVal }, got '${ JSON.stringify(profileVal) }'`);\n      }\n    } else if (haveUserDefinedObject(parentPathParts.concat(key))) {\n      // Keep this part of the profile\n    } else if (typeof profileVal !== typeof schemaVal) {\n      errors.push(`Error for field '${ fullPath(key) }': expecting value of type ${ typeof schemaVal }, got '${ JSON.stringify(profileVal) }'`);\n    } else {\n      // Finally recurse and compare the schema sub-object with the specified sub-object\n      validateDeploymentProfileWithErrors(profileVal, errors, schemaVal, [...parentPathParts, key]);\n    }\n  }\n\n  return profile;\n}\n\nconst caseInsensitiveComparator = new Intl.Collator('en', { sensitivity: 'base' });\n\nfunction isEquivalentIgnoreCase(a: string, b: string): boolean {\n  return caseInsensitiveComparator.compare(a, b) === 0;\n}\n\nfunction fixProfileKeyCase(key: string, schemaKeys: string[]): string | null {\n  return schemaKeys.find(val => isEquivalentIgnoreCase(key, val)) ?? null;\n}\n\nconst userDefinedKeys = [\n  'application.extensions.installed',\n  'WSL.integrations',\n  'diagnostics.mutedChecks',\n].map(s => s.split('.'));\n\n/**\n * A \"user-defined object\" from the schema's point of view is an object that contains user-defined keys.\n * For example, `WSL.integrations` points to a user-defined object, while\n * `WSL` alone points to an object that contains only one key, `integrations`.\n *\n * @param pathParts - On Windows, the parts of the registry path below KEY\\Software\\Rancher Desktop\\Profile\\{defaults|locked|}\n *                    The first field is always either 'defaults' or 'locked' and can be ignored\n *                    On other platforms it is the path-parts up to but not including the root (which is unnamed anyway).\n * @returns boolean\n */\nfunction haveUserDefinedObject(pathParts: string[]): boolean {\n  return userDefinedKeys.some(userDefinedKey => _.isEqual(userDefinedKey, pathParts));\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/__tests__/diagnostics.spec.ts",
    "content": "import dayjs from 'dayjs';\nimport relativeTime from 'dayjs/plugin/relativeTime';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nimport type { DiagnosticsResult } from '../diagnostics';\nimport type { DiagnosticsChecker } from '../types';\n\nmockModules({\n  '@pkg/utils/logging': undefined,\n  electron:             undefined,\n});\n\nconst { DiagnosticsManager } = await import('../diagnostics');\nconst { DiagnosticsCategory } = await import('../types');\n\ndescribe(DiagnosticsManager, () => {\n  const mockDiagnostics: DiagnosticsChecker[] = [\n    {\n      id:       'RD_BIN_IN_BASH_PATH',\n      category: DiagnosticsCategory.Utilities,\n      applicable() {\n        return Promise.resolve(true);\n      },\n      check: () => Promise.resolve({\n        documentation: 'path#rd_bin_bash',\n        description:   'The ~/.rd/bin directory has not been added to the PATH, so command-line utilities are not configured in your bash shell.',\n        passed:        true,\n        fixes:         [],\n      }),\n    },\n    {\n      id:       'RD_BIN_SYMLINKS',\n      category: DiagnosticsCategory.Utilities,\n      applicable() {\n        return Promise.resolve(true);\n      },\n      check: () => Promise.resolve({\n        documentation: 'path#rd_bin_symlinks',\n        description:   'Are the files under ~/.docker/cli-plugins symlinks to ~/.rd/bin?',\n        passed:        false,\n        fixes:         [],\n      }),\n    },\n    {\n      id:       'CONNECTED_TO_INTERNET',\n      category: DiagnosticsCategory.Networking,\n      applicable() {\n        return Promise.resolve(true);\n      },\n      check: () => Promise.resolve({\n        documentation: 'path#connected_to_internet',\n        description:   'The application cannot reach the general internet for updated kubernetes versions and other components, but can still operate.',\n        passed:        false,\n        fixes:         [],\n      }),\n    },\n  ];\n  const diagnostics = new DiagnosticsManager(mockDiagnostics);\n\n  test('it finds the categories', () => {\n    expect(diagnostics.getCategoryNames()).toEqual(expect.arrayContaining(['Utilities', 'Networking']));\n  });\n\n  test('it finds the IDs', () => {\n    expect(diagnostics.getIdsForCategory('Utilities')).toEqual(expect.arrayContaining(['RD_BIN_IN_BASH_PATH', 'RD_BIN_SYMLINKS']));\n    expect(diagnostics.getIdsForCategory('Networking')).toEqual(expect.arrayContaining(['CONNECTED_TO_INTERNET']));\n    expect(diagnostics.getIdsForCategory('Tennessee Tuxedo')).toBeUndefined();\n  });\n\n  test('it finds the checks', async() => {\n    await expect(diagnostics.runChecks()).resolves.toEqual(({\n      last_update: expect.stringMatching(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z/),\n      checks:      expect.arrayContaining<DiagnosticsResult>([\n        {\n          id:            mockDiagnostics[0].id,\n          category:      mockDiagnostics[0].category,\n          documentation: 'path#rd_bin_bash',\n          description:   'The ~/.rd/bin directory has not been added to the PATH, so command-line utilities are not configured in your bash shell.',\n          passed:        true,\n          mute:          false,\n          fixes:         [\n          // { description: 'You have selected manual PATH configuration. You can let Rancher Desktop automatically configure it.' },\n          ],\n        },\n        {\n          id:            mockDiagnostics[1].id,\n          category:      mockDiagnostics[1].category,\n          documentation: 'path#rd_bin_symlinks',\n          description:   'Are the files under ~/.docker/cli-plugins symlinks to ~/.rd/bin?',\n          passed:        false,\n          mute:          false,\n          fixes:         [\n          // { description: 'Replace existing files in ~/.rd/bin with symlinks to the application\\'s internal utility directory' },\n          ],\n        },\n        {\n          id:            mockDiagnostics[2].id,\n          category:      mockDiagnostics[2].category,\n          documentation: 'path#connected_to_internet',\n          description:   'The application cannot reach the general internet for updated kubernetes versions and other components, but can still operate.',\n          passed:        false,\n          mute:          false,\n          fixes:         [],\n        },\n      ]),\n    }));\n    await expect(diagnostics.getChecks('Chummily', 'CONNECTED_TO_INTERNET')).resolves.toMatchObject({ checks: [] });\n    await expect(diagnostics.getChecks('Utilities', 'gallop the friendly purple')).resolves.toMatchObject({ checks: [] });\n    await expect(diagnostics.getChecks('Utilities', 'RD_BIN_IN_BASH_PATH')).resolves.toMatchObject({\n      checks: [{\n        documentation: 'path#rd_bin_bash',\n        description:   'The ~/.rd/bin directory has not been added to the PATH, so command-line utilities are not configured in your bash shell.',\n        mute:          false,\n        fixes:         [/* { description: 'You have selected manual PATH configuration. You can let Rancher Desktop automatically configure it.' } */],\n      }],\n    });\n    await expect(diagnostics.getChecks('Utilities', 'RD_BIN_SYMLINKS')).resolves.toMatchObject({\n      checks: [{\n        documentation: 'path#rd_bin_symlinks',\n        description:   'Are the files under ~/.docker/cli-plugins symlinks to ~/.rd/bin?',\n        mute:          false,\n        fixes:         [/* { description: \"Replace existing files in ~/.rd/bin with symlinks to the application's internal utility directory\" } */],\n      }],\n    });\n    const internetCheck = expect(diagnostics.getChecks('Networking', 'CONNECTED_TO_INTERNET')).resolves;\n\n    await internetCheck.toMatchObject({\n      checks: {\n        0: {\n          documentation: 'path#connected_to_internet',\n          description:   'The application cannot reach the general internet for updated kubernetes versions and other components, but can still operate.',\n          mute:          false,\n        },\n      },\n    });\n    await internetCheck.not.toMatchObject({ checks: { 0: { fixes: { description: expect.any(String) } } } });\n  });\n});\n\ndayjs.extend(relativeTime);\n\ndescribe('dayjs', () => {\n  it('rounds sub-seconds up', () => {\n    const time1 = dayjs(new Date());\n    const time2 = dayjs(time1.valueOf() + 100);\n\n    expect(time1.to(time2)).toEqual('in a few seconds');\n    expect(time2.to(time1)).toEqual('a few seconds ago');\n  });\n  it('treats equality as a few seconds ago', () => {\n    const time1 = dayjs(new Date());\n\n    expect(time1.to(time1))\n      .toEqual('a few seconds ago');\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/__tests__/dockerCliSymlinks.spec.ts",
    "content": "import fs from 'fs';\nimport { mkdtemp, rm } from 'fs/promises';\nimport os from 'os';\nimport path from 'path';\n\nimport { jest } from '@jest/globals';\n\nimport paths from '@pkg/utils/paths';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\n// The (mock) application directory.\nlet appDir = process.cwd();\n\nconst modules = mockModules({\n  // Mock Electron.app.getAppPath() to return appDir.\n  electron: {\n    app: {\n      isPackaged: false,\n      getAppPath: () => appDir,\n    },\n  },\n  // Mock fs.promises.readdir() for the default export.\n  fs: {\n    ...fs,\n    promises: {\n      ...fs.promises,\n      readdir: jest.spyOn(fs.promises, 'readdir').mockImplementation((dir, encoding) => {\n        expect(dir).toEqual(path.join(modules['@pkg/utils/paths'].resources, os.platform(), 'docker-cli-plugins'));\n        expect(encoding).toEqual('utf-8');\n\n        return Promise.resolve([]);\n      }),\n    },\n  },\n  '@pkg/utils/paths': {\n    ...paths,\n    resources: '',\n  },\n});\n\nconst { CheckerDockerCLISymlink } = await import('../dockerCliSymlinks');\nconst describeUnix = process.platform === 'win32' ? describe.skip : describe;\nconst describeWin32 = process.platform === 'win32' ? describe : describe.skip;\n\ndescribeUnix(CheckerDockerCLISymlink, () => {\n  const executable = 'test-executable';\n  const cliPluginsDir = path.join(os.homedir(), '.docker', 'cli-plugins');\n  const rdBinDir = path.join(os.homedir(), '.rd', 'bin');\n  const rdBinExecutable = path.join(rdBinDir, executable);\n  let appDirExecutable = '';\n\n  beforeAll(async() => {\n    appDir = await mkdtemp(path.join(os.tmpdir(), 'rd-diag-'));\n    const resourcesDir = path.join(appDir, 'resources');\n\n    await fs.promises.mkdir(resourcesDir);\n    appDirExecutable = path.join(resourcesDir, os.platform(), 'docker-cli-plugins', executable);\n    modules['@pkg/utils/paths'].resources = resourcesDir;\n  });\n  afterAll(async() => {\n    await rm(appDir, { recursive: true, force: true });\n  });\n\n  it('should be applicable', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    await expect(subject.applicable()).resolves.toBeTruthy();\n  });\n\n  it('should pass', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink').mockImplementationOnce((filepath, options) => {\n      expect(options).toBeUndefined();\n      expect(filepath).toEqual(path.join(cliPluginsDir, executable));\n\n      return Promise.resolve(rdBinExecutable);\n    }).mockImplementationOnce((filepath, options) => {\n      expect(options).toBeUndefined();\n      expect(filepath).toEqual(rdBinExecutable);\n\n      return Promise.resolve(appDirExecutable);\n    });\n    jest.spyOn(subject, 'access').mockImplementation((filepath, mode) => {\n      expect(filepath).toEqual(appDirExecutable);\n      expect(mode).toEqual(fs.constants.X_OK);\n\n      return Promise.resolve();\n    });\n\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(new RegExp(`\\`${ path.join('~/\\\\.docker/cli-plugins', executable) }\\` is a symlink to \\`${ appDirExecutable }\\` through .*\\.rd/bin/.*\\.`)),\n      passed:      true,\n    }));\n  });\n\n  function wrongFirstLinkError(desc: string) {\n    return new RegExp(`${ executable }\\` should be a symlink to \\`~/\\\\.rd/bin/${ executable }\\`, ${ desc }\\\\.`);\n  }\n\n  function badFirstLinkError(desc: string) {\n    return new RegExp(`${ executable }\\` ${ desc }\\\\.\\\\s+It should be a symlink to \\`~/\\\\.rd/bin/${ executable }\\`\\\\.$`);\n  }\n\n  function badSecondLinkError(desc: string) {\n    return new RegExp(`${ executable }\\` should be a symlink to \\`${ appDirExecutable }\\`, ${ desc }\\\\.$`);\n  }\n\n  function problematicSecondLinkError(desc: string) {\n    return new RegExp(`${ executable }\\` is a symlink to \\`${ appDirExecutable }\\`, ${ desc }\\\\.`);\n  }\n\n  function intermediateFileNotSymlinkError(desc: string) {\n    return new RegExp(\n      `${ executable }\\` ${ desc }\\\\. It should be a symlink to \\`${ appDirExecutable }\\`\\\\.`);\n  }\n\n  it('should catch missing link', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink').mockRejectedValue({ code: 'ENOENT' });\n    jest.spyOn(subject, 'access');\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(badFirstLinkError('does not exist')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).not.toHaveBeenCalled();\n  });\n\n  it('should catch not a symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink').mockRejectedValue({ code: 'EINVAL' });\n    jest.spyOn(subject, 'access');\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(badFirstLinkError('is not a symlink')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).not.toHaveBeenCalled();\n  });\n\n  it('should catch generic errors', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink').mockRejectedValue({ code: 'EPONY' });\n    jest.spyOn(subject, 'access');\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(badFirstLinkError('cannot be read')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).not.toHaveBeenCalled();\n  });\n\n  it('should catch incorrect link', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce('/usr/bin/true')\n      .mockRejectedValue({ code: 'EPONY' });\n    jest.spyOn(subject, 'access');\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(wrongFirstLinkError('but points to `/usr/bin/true`')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).not.toHaveBeenCalled();\n  });\n\n  it('should catch incorrect second symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce(rdBinExecutable)\n      .mockResolvedValueOnce('/usr/bin/true');\n    jest.spyOn(subject, 'access');\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(badSecondLinkError('but points to `/usr/bin/true`')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).not.toHaveBeenCalled();\n  });\n\n  it('should catch nonexistent second symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce(rdBinExecutable)\n      .mockResolvedValueOnce(appDirExecutable);\n    jest.spyOn(subject, 'access')\n      .mockRejectedValue({ code: 'ENOENT' });\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(problematicSecondLinkError('which does not exist')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).toHaveBeenCalledTimes(1);\n  });\n\n  it('should catch looping second symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce(rdBinExecutable)\n      .mockResolvedValueOnce(appDirExecutable);\n    jest.spyOn(subject, 'access')\n      .mockRejectedValue({ code: 'ELOOP' });\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(intermediateFileNotSymlinkError('is a symlink with a loop')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).toHaveBeenCalledTimes(1);\n  });\n\n  it('should catch inaccessible second symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce(rdBinExecutable)\n      .mockResolvedValueOnce(appDirExecutable);\n    jest.spyOn(subject, 'access')\n      .mockRejectedValue({ code: 'EACCES' });\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(problematicSecondLinkError('which is not executable')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).toHaveBeenCalledTimes(1);\n  });\n\n  it('should catch error reading second symlink', async() => {\n    const subject = new CheckerDockerCLISymlink(executable);\n\n    jest.spyOn(subject, 'readlink')\n      .mockResolvedValueOnce(rdBinExecutable)\n      .mockResolvedValueOnce(appDirExecutable);\n    jest.spyOn(subject, 'access')\n      .mockRejectedValue({ code: 'EPONY' });\n    await expect(subject.check()).resolves.toEqual(expect.objectContaining({\n      description: expect.stringMatching(problematicSecondLinkError('but cannot be read \\\\(EPONY\\\\)')),\n      passed:      false,\n    }));\n    expect(jest.spyOn(subject, 'access')).toHaveBeenCalledTimes(1);\n  });\n});\n\ndescribeWin32(CheckerDockerCLISymlink, () => {\n  test('should not apply', async() => {\n    const subject = new CheckerDockerCLISymlink('blah');\n\n    await expect(subject.applicable()).resolves.toBeFalsy();\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/__tests__/rdBinInShell.spec.ts",
    "content": "import { RDBinInShellPath } from '../rdBinInShell';\n\ndescribe(RDBinInShellPath, () => {\n  it('should remove trailing slashes', () => {\n    expect(RDBinInShellPath.removeTrailingSlash('/Users/mikey/.rd/bin/')).toBe('/Users/mikey/.rd/bin');\n    expect(RDBinInShellPath.removeTrailingSlash('/Users/mikey/.rd/bin////')).toBe('/Users/mikey/.rd/bin');\n    expect(RDBinInShellPath.removeTrailingSlash('/Users/mikey/.rd/bin')).toBe('/Users/mikey/.rd/bin');\n    expect(RDBinInShellPath.removeTrailingSlash('/')).toBe('/');\n    expect(RDBinInShellPath.removeTrailingSlash('//')).toBe('/');\n    expect(RDBinInShellPath.removeTrailingSlash('/////')).toBe('/');\n    expect(RDBinInShellPath.removeTrailingSlash('')).toBe('');\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/connectedToInternet.ts",
    "content": "import { net } from 'electron';\n\nimport { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.diagnostics;\n\nlet pollingInterval: NodeJS.Timeout;\nlet timeout = 5_000;\n\n// Since this is just a status check, it's fine to just reset the timer every\n// time _any_ setting has been updated.\nmainEvents.on('settings-update', settings => {\n  clearInterval(pollingInterval);\n\n  const { timeout: localTimeout, interval } = settings.diagnostics.connectivity;\n\n  timeout = localTimeout;\n  if (interval > 0) {\n    pollingInterval = setInterval(() => {\n      mainEvents.invoke('diagnostics-trigger', CheckConnectedToInternet.id);\n    }, interval);\n  }\n});\n\n/**\n * Checks whether we can perform an HTTP request to a host on the internet,\n * with a reasonably short timeout.\n */\nasync function checkNetworkConnectivity(): Promise<boolean> {\n  const request = net.request({\n    method:      'HEAD',\n    url:         'https://docs.rancherdesktop.io/',\n    credentials: 'omit',\n    cache:       'no-cache',\n  });\n  const timeoutId = setTimeout(() => {\n    console.log(`${ CheckConnectedToInternet.id }: aborting due to timeout after ${ timeout } milliseconds.`);\n    request.abort();\n  }, timeout);\n  try {\n    return await new Promise<boolean>(resolve => {\n      request.on('response', () => resolve(true));\n      request.on('redirect', () => resolve(true));\n      request.on('error', () => resolve(false));\n      request.on('abort', () => resolve(false));\n      request.end();\n    });\n  } finally {\n    clearTimeout(timeoutId);\n  }\n}\n\n/**\n * CheckConnectedToInternet checks whether the machine is connected to the\n * internet (which is required for most operations).\n */\nconst CheckConnectedToInternet: DiagnosticsChecker = {\n  id:       'CONNECTED_TO_INTERNET',\n  category: DiagnosticsCategory.Networking,\n  applicable() {\n    return Promise.resolve(true);\n  },\n  async check() {\n    const connected = await checkNetworkConnectivity();\n    mainEvents.emit('diagnostics-event', { id: 'network-connectivity', connected });\n    if (connected) {\n      return {\n        description: 'The application can reach the internet successfully.',\n        passed:      true,\n        fixes:       [],\n      };\n    }\n    return {\n      description: 'The application cannot reach the general internet for ' +\n      'updated kubernetes versions and other components, but can still operate.',\n      passed: false,\n      fixes:  [],\n    };\n  },\n};\n\nexport default CheckConnectedToInternet;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/diagnostics.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport { send } from '@pkg/window';\n\nconst console = Logging.diagnostics;\n\n/**\n * DiagnosticsResult is the data structure that will be returned to clients (as\n * part of a DiagnosticsResultCollection) over the HTTP API.\n */\nexport type DiagnosticsResult = DiagnosticsCheckerResult & {\n  /** The diagnostics checker that produced this result. */\n  id:       string,\n  /** Whether to avoid notifying the user about failures for this check. */\n  mute:     boolean,\n  category: DiagnosticsCategory,\n};\n\n/**\n * DiagnosticsResultCollection is the data structure that will be returned to\n * clients over the HTTP API.\n */\nexport interface DiagnosticsResultCollection {\n  last_update: string,\n  checks:      DiagnosticsResult[],\n}\n\n/**\n * DiagnosticsManager manages the collection of diagnostics checkers, and is\n * used to run checks and fetch results.\n */\nexport class DiagnosticsManager {\n  /** Checkers capable of running individual diagnostics. */\n  readonly checkers: Promise<DiagnosticsChecker[]>;\n\n  /** Time stamp of when the last check occurred. */\n  lastUpdate = new Date(0);\n\n  /** Last known applicable state, for message limiting. */\n  applicable: Record<DiagnosticsChecker['id'], boolean> = {};\n\n  /** Last known check results, indexed by the checker id. */\n  results: Record<DiagnosticsChecker['id'], DiagnosticsCheckerResult | DiagnosticsCheckerSingleResult[]> = {};\n\n  updateTimeout: ReturnType<typeof setTimeout> | undefined;\n\n  /** Mapping of category name to diagnostic ids */\n  readonly checkerIdByCategory: Partial<Record<DiagnosticsCategory, string[]>> = {};\n\n  constructor(diagnostics?: DiagnosticsChecker[]) {\n    this.checkers = diagnostics\n      ? Promise.resolve(diagnostics)\n      : (async() => {\n        const imports = (await Promise.all([\n          import('./connectedToInternet'),\n          import('./dockerCliSymlinks'),\n          import('./dockerContext'),\n          import('./integrationsWindows'),\n          import('./kubeConfigSymlink'),\n          import('./kubeContext'),\n          import('./kubeVersionsAvailable'),\n          import('./limaDarwin'),\n          import('./limaOverrides'),\n          import('./mobyImageStore'),\n          import('./mockForScreenshots'),\n          import('./pathManagement'),\n          import('./rdBinInShell'),\n          import('./testCheckers'),\n          import('./wslDistros'),\n          import('./wslInfo'),\n        ])).map(obj => obj.default);\n\n        // Only some of the imports return promises.\n        // eslint-disable-next-line @typescript-eslint/await-thenable\n        return (await Promise.all(imports)).flat();\n      })();\n    this.checkers.then((checkers) => {\n      for (const checker of checkers) {\n        this.checkerIdByCategory[checker.category] ??= [];\n        this.checkerIdByCategory[checker.category]?.push(checker.id);\n      }\n    });\n\n    mainEvents.handle('diagnostics-trigger', async(id) => {\n      const checker = (await this.checkers).find(checker => checker.id === id);\n\n      if (checker) {\n        await this.runChecker(checker);\n\n        return this.results[checker.id];\n      }\n    });\n  }\n\n  /**\n   * Returns the list of currently known category names.\n   */\n  getCategoryNames(): string[] {\n    return Object.keys(this.checkerIdByCategory);\n  }\n\n  /**\n   * Returns undefined if the categoryName isn't known, the list of IDs in that category otherwise.\n   */\n  getIdsForCategory(categoryName: string): string[] | undefined {\n    return this.checkerIdByCategory[categoryName as DiagnosticsCategory];\n  }\n\n  protected async applicableCheckers(categoryName: string | null, id: string | null): Promise<DiagnosticsChecker[]> {\n    const checkerId = id?.split(':', 1)[0];\n    const checkers = (await this.checkers)\n      .filter(checker => categoryName ? checker.category === categoryName : true)\n      .filter(checker => checkerId ? checker.id === checkerId : true);\n\n    return (await Promise.all(checkers.map(async(checker) => {\n      try {\n        return [checker, await checker.applicable()] as const;\n      } catch (ex) {\n        console.error(`Failed to check ${ checker.id }: ${ ex }`);\n\n        return [checker, false] as const;\n      }\n    })))\n      .map(([checker, applicable]) => {\n        if (applicable !== this.applicable[checker.id]) {\n          const verb = this.applicable[checker.id] === undefined ? 'is' : 'turned';\n          console.debug(`${ checker.id } ${ verb } ${ applicable ? '' : 'not ' }applicable`);\n          this.applicable[checker.id] = applicable;\n        }\n\n        return [checker, applicable] as const;\n      })\n      .filter(([, applicable]) => applicable)\n      .map(([checker]) => checker);\n  }\n\n  /**\n   * Fetch the last known results, filtered by given category and id.\n   */\n  async getChecks(categoryName: string | null, id: string | null): Promise<DiagnosticsResultCollection> {\n    const checkers = (await this.applicableCheckers(categoryName, id))\n      .filter(checker => checker.id in this.results);\n\n    return {\n      last_update: this.lastUpdate.toISOString(),\n      checks:      checkers\n        .flatMap((checker) => {\n          const result = this.results[checker.id];\n\n          if (Array.isArray(result)) {\n            return result.map(result => ({\n              ...result,\n              id:       `${ checker.id }:${ result.id }`,\n              category: checker.category,\n              mute:     false,\n            }));\n          } else {\n            return {\n              ...result,\n              id:       checker.id,\n              category: checker.category,\n              mute:     false,\n            };\n          }\n        }),\n    };\n  }\n\n  /**\n   * Run the given diagnostics checker, updating its result.\n   */\n  protected async runChecker(checker: DiagnosticsChecker) {\n    console.debug(`Running check ${ checker.id }`);\n    try {\n      const result = await checker.check();\n\n      this.results[checker.id] = result;\n      if (Array.isArray(result)) {\n        for (const singleResult of result) {\n          console.debug(`Check ${ checker.id }:${ singleResult.id } result: ${ JSON.stringify(singleResult) }`);\n        }\n      } else {\n        console.debug(`Check ${ checker.id } result: ${ JSON.stringify(result) }`);\n      }\n    } catch (e) {\n      console.error(`ERROR checking ${ checker.id }`, { e });\n    }\n\n    if (this.updateTimeout !== undefined) {\n      clearTimeout(this.updateTimeout);\n    }\n    this.updateTimeout = setTimeout(() => send('diagnostics/update'), 500);\n  }\n\n  /**\n   * Run all checks, and return the results.\n   */\n  async runChecks(): Promise<DiagnosticsResultCollection> {\n    await Promise.all((await this.applicableCheckers(null, null)).map(async(checker) => {\n      await this.runChecker(checker);\n    }));\n    this.lastUpdate = new Date();\n\n    return this.getChecks(null, null);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/dockerCliSymlinks.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst console = Logging.diagnostics;\n\n/** Given a path, replace the user's home directory with \"~\". */\nfunction replaceHome(input: string) {\n  if (input.startsWith(os.homedir() + path.sep)) {\n    return input.replace(os.homedir(), '~');\n  }\n\n  return input;\n}\n\nexport class CheckerDockerCLISymlink implements DiagnosticsChecker {\n  constructor(name: string) {\n    this.name = name;\n  }\n\n  readonly name: string;\n  get id() {\n    return `RD_BIN_DOCKER_CLI_SYMLINK_${ this.name.toUpperCase() }`;\n  }\n\n  readonly category = DiagnosticsCategory.Utilities;\n  applicable() {\n    return Promise.resolve(['darwin', 'linux'].includes(os.platform()));\n  }\n\n  trigger?: ((checker: DiagnosticsChecker) => void) | undefined;\n\n  // For testing use\n  readonly readlink = fs.promises.readlink;\n  readonly access = fs.promises.access;\n\n  async check() {\n    const dockerCliPluginDir = path.join(os.homedir(), '.docker', 'cli-plugins');\n    const startingPath = path.join(dockerCliPluginDir, this.name);\n    const displayableStartingPath = replaceHome(startingPath);\n    const rdBinPath = path.join(paths.integration, this.name);\n    const displayableRDBinPath = replaceHome(rdBinPath);\n    const finalTarget = path.join(paths.resources, os.platform(), 'docker-cli-plugins', this.name);\n    const displayableFinalTarget = replaceHome(finalTarget);\n    let state;\n    let description = `The file \\`${ displayableStartingPath }\\``;\n    let finalDescription = '';\n\n    try {\n      const link = await this.readlink(startingPath);\n\n      console.debug(`${ this.id }: first-level symlink ${ displayableStartingPath }: points to: ${ link } (expect ${ displayableRDBinPath })`);\n\n      if (link !== rdBinPath) {\n        return {\n          description: `${ description } should be a symlink to \\`${ displayableRDBinPath }\\`, but points to \\`${ replaceHome(link) }\\`.`,\n          passed:      false,\n          fixes:       [], // TODO: [{ description: `ln -sf ${ displayableRDBinPath } ${ displayableStartingPath }` }],\n        };\n      }\n    } catch (ex: any) {\n      const code = ex.code ?? '';\n\n      if (code === 'ENOENT') {\n        state = 'does not exist';\n      } else if (code === 'EINVAL') {\n        state = 'is not a symlink';\n      } else {\n        state = 'cannot be read';\n      }\n\n      return {\n        description: `${ description } ${ state }. It should be a symlink to \\`${ displayableRDBinPath }\\`.`,\n        passed:      false,\n        fixes:       [],\n      };\n    }\n\n    description = `The file \\`${ displayableRDBinPath }\\``;\n    try {\n      const link = await this.readlink(rdBinPath);\n\n      if (link !== finalTarget) {\n        return {\n          description: `${ description } should be a symlink to \\`${ displayableFinalTarget }\\`, but points to \\`${ replaceHome(link) }\\`.`,\n          passed:      false,\n          fixes:       [],\n        };\n      }\n      await this.access(link, fs.constants.X_OK);\n\n      return {\n        description: `\\`${ displayableStartingPath }\\` is a symlink to \\`${ displayableFinalTarget }\\` through \\`${ displayableRDBinPath }\\`.`,\n        passed:      true,\n        fixes:       [],\n      };\n    } catch (ex: any) {\n      const code = ex.code ?? '';\n\n      if (code === 'ENOENT') {\n        finalDescription = `${ description } is a symlink to \\`${ displayableFinalTarget }\\`, which does not exist.`;\n      } else if (code === 'EINVAL') {\n        state = `is not a symlink`;\n      } else if (code === 'ELOOP') {\n        state = `is a symlink with a loop`;\n      } else if (code === 'EACCES') {\n        finalDescription = `${ description } is a symlink to \\`${ displayableFinalTarget }\\`, which is not executable.`;\n      } else {\n        finalDescription = `${ description } is a symlink to \\`${ displayableFinalTarget }\\`, but cannot be read (${ code || 'unknown error' }).`;\n      }\n\n      return {\n        description: finalDescription || `${ description } ${ state }. It should be a symlink to \\`${ displayableFinalTarget }\\`.`,\n        passed:      false,\n        fixes:       [],\n      };\n    }\n  }\n}\n\nconst dockerCliSymlinkCheckers: Promise<DiagnosticsChecker[]> = (async() => {\n  const resourcesDir = path.join(paths.resources, os.platform(), 'docker-cli-plugins');\n  const names = await fs.promises.readdir(resourcesDir, 'utf-8');\n\n  return names.map((name) => {\n    return new CheckerDockerCLISymlink(name);\n  });\n})();\n\nexport default dockerCliSymlinkCheckers;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/dockerContext.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types';\n\nimport { ContainerEngine } from '@pkg/config/settings';\nimport mainEvents from '@pkg/main/mainEvents';\nimport dockerDirManager from '@pkg/utils/dockerDirManager';\n\nconst dockerContextChecker: DiagnosticsChecker = {\n  id:       'DOCKER_CONTEXT',\n  category: DiagnosticsCategory.ContainerEngine,\n  async applicable(): Promise<boolean> {\n    const settings = await mainEvents.invoke('settings-fetch');\n\n    return settings.containerEngine.name === ContainerEngine.MOBY;\n  },\n  async check(): Promise<DiagnosticsCheckerSingleResult[]> {\n    const results: DiagnosticsCheckerSingleResult[] = [];\n    for (const variable of ['DOCKER_HOST', 'DOCKER_CONTEXT', 'DOCKER_CONFIG']) {\n      if (variable in process.env) {\n        results.push({\n          id:          `DOCKER_CONTEXT_ENV_${ variable }`,\n          description: `\\`${ variable }\\` environment variable is set.`,\n          passed:      false,\n          fixes:       [{ description: `Unset \\`${ variable }\\`.` }],\n        });\n      }\n    }\n\n    const DEFAULT_CONTEXT = 'default';\n    const settings = await mainEvents.invoke('settings-fetch');\n    const useDefaultContext = process.platform === 'win32' || settings.application.adminAccess;\n    const currentContext = await dockerDirManager.currentDockerContext ?? DEFAULT_CONTEXT;\n    const desiredContext = await dockerDirManager.getDesiredDockerContext(useDefaultContext, undefined) ?? DEFAULT_CONTEXT;\n\n    if (currentContext !== desiredContext) {\n      results.push({\n        id:          'DOCKER_CONTEXT',\n        description: `Docker context is currently \\`${ currentContext }\\` instead of \\`${ desiredContext }\\`.`,\n        passed:      false,\n        fixes:       [{\n          description: `Run \\`docker context use ${ desiredContext }\\``,\n        }],\n      });\n    } else {\n      results.push({\n        id:          'DOCKER_CONTEXT',\n        description: `Correctly using context \\`${ currentContext }\\``,\n        passed:      true,\n        fixes:       [],\n      });\n    }\n\n    return results;\n  },\n};\n\nexport default dockerContextChecker;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/integrationsWindows.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types';\n\nimport mainEvents from '@pkg/main/mainEvents';\n\nconst cachedResults: Record<string, DiagnosticsCheckerResult> = {};\n\nconst CheckWindowsIntegrations: DiagnosticsChecker = {\n  id:       'WINDOWS_INTEGRATIONS',\n  category: DiagnosticsCategory.ContainerEngine,\n  applicable() {\n    return Promise.resolve(process.platform === 'win32');\n  },\n  check(): Promise<DiagnosticsCheckerSingleResult[]> {\n    const resultMapper = ([id, result]: [string, DiagnosticsCheckerResult]) => {\n      return ({ ...result, id });\n    };\n\n    return Promise.resolve(Object.entries(cachedResults).map(resultMapper));\n  },\n};\n\nmainEvents.on('diagnostics-event', (payload) => {\n  if (payload.id !== 'integrations-windows') {\n    return;\n  }\n  const { distro, key, error } = payload;\n  const message = error?.message ?? error?.toString();\n\n  cachedResults[`${ distro || '<main>' }-${ key }`] = {\n    passed: false,\n    fixes:  [],\n    ...(() => {\n      if (!error) {\n        return { passed: true, description: `${ distro }/${ key } passed` };\n      }\n      if (distro) {\n        return { description: `Error managing distribution ${ distro }: ${ key }: ${ message }` };\n      }\n\n      return { description: `Error managing ${ key }: ${ message }` };\n    })(),\n  };\n});\n\nexport default CheckWindowsIntegrations;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/kubeConfigSymlink.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport WindowsIntegrationManager from '@pkg/integrations/windowsIntegrationManager';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.diagnostics;\n\nasync function verifyKubeConfigSymlink(): Promise<boolean> {\n  const integrationManager = WindowsIntegrationManager.getInstance();\n\n  try {\n    await integrationManager.verifyAllDistrosKubeConfig();\n\n    return true;\n  } catch (error: any) {\n    console.error(`Error verifying kubeconfig symlinks: ${ error.message }`);\n\n    return false;\n  }\n}\n\n/**\n * CheckKubeConfigSymlink checks the symlinked kubeConfig in WSL integration\n * enabled distro for non-rancher desktop configuration.\n */\nconst CheckKubeConfigSymlink: DiagnosticsChecker = {\n  id:       'VERIFY_WSL_INTEGRATION_KUBECONFIG',\n  category: DiagnosticsCategory.Kubernetes,\n  applicable() {\n    return Promise.resolve(process.platform === 'win32');\n  },\n  async check() {\n    return Promise.resolve({\n      description: 'Rancher Desktop cannot automatically convert the provided kubeconfig file to a symlink' +\n        ' due to existing configurations within that file. To resolve this issue, you will need to ' +\n        'manually create the symlink to ensure existing configurations are preserved and to prevent ' +\n        'any loss of configuration.',\n      passed: await verifyKubeConfigSymlink(),\n      fixes:  [],\n    });\n  },\n};\n\nexport default CheckKubeConfigSymlink;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/kubeContext.ts",
    "content": "import path from 'path';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nimport type { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult } from './types';\n\nconst console = Logging.diagnostics;\n\nconst KubeContextDefaultChecker: DiagnosticsChecker = {\n  id:       'KUBE_CONTEXT',\n  category: 'Kubernetes' as DiagnosticsCategory,\n  async applicable(): Promise<boolean> {\n    const settings = await mainEvents.invoke('settings-fetch');\n\n    console.debug(`${ this.id }: Kubernetes enabled? ${ settings.kubernetes.enabled }`);\n\n    return settings.kubernetes.enabled;\n  },\n  async check(): Promise<DiagnosticsCheckerResult> {\n    const kubectl = path.join(paths.resources, process.platform, 'bin', 'kubectl');\n    const { stdout } = await spawnFile(kubectl, ['config', 'view', '--minify', '--output=json'], {\n      // While we only need stdout here, capture stderr so if we encounter errors\n      // the message shows up in the logs.\n      stdio:    ['ignore', 'pipe', 'pipe'],\n      encoding: 'utf-8',\n    });\n    const config = JSON.parse(stdout);\n    const contexts = config['contexts'] as any[] ?? [];\n    const passed = contexts.some(context => context.name === 'rancher-desktop');\n    let description: string;\n\n    console.debug(`${ this.id }: using ${ kubectl }`);\n    console.debug(`${ this.id }: defaults to RD context? ${ passed }`);\n    if (passed) {\n      description = 'Kubernetes is using the \\`rancher-desktop\\` context.';\n    } else {\n      const context = contexts.map(context => context.name).filter(c => c).shift();\n\n      console.debug(`${ this.id }: current default context: ${ context }`);\n      if (context) {\n        description = `Kubernetes is using context \\`${ context }\\` instead of \\`rancher-desktop\\`.`;\n      } else {\n        description = 'No active Kubernetes context found; should be \\`rancher-desktop\\`.';\n      }\n    }\n\n    return {\n      description,\n      fixes: [],\n      passed,\n    };\n  },\n};\n\nexport default KubeContextDefaultChecker;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/kubeVersionsAvailable.ts",
    "content": "import mainEvents from '../mainEvents';\nimport { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult } from './types';\n\nlet kubeVersionsAvailable = true;\n\nmainEvents.on('diagnostics-event', (payload) => {\n  if (payload.id !== 'kube-versions-available') {\n    return;\n  }\n  kubeVersionsAvailable = payload.available;\n  mainEvents.invoke('diagnostics-trigger', instance.id);\n});\n\n/**\n * KubeVersionsAvailable is a diagnostic that will be emitted when all of the\n * following are met:\n * - Kubernetes was configured to be enabled\n * - The selected Kubernetes version is unavailable (e.g. user is offline)\n * Once the diagnostic is triggered, it stays on until the backend is restarted.\n */\nclass KubeVersionsAvailable implements DiagnosticsChecker {\n  readonly id = 'KUBE_VERSIONS_AVAILABLE';\n  readonly category = DiagnosticsCategory.Kubernetes;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(true);\n  }\n\n  check(): Promise<DiagnosticsCheckerResult> {\n    const description = [\n      'There are no issues with Kubernetes versions',\n      'Kubernetes has been disabled due to issues with fetching Kubernetes versions',\n    ][kubeVersionsAvailable ? 0 : 1];\n\n    return Promise.resolve({\n      passed: kubeVersionsAvailable,\n      description,\n      fixes:  [{ description: 'Check your network connection to update.k3s.io' }],\n    });\n  }\n}\n\nconst instance = new KubeVersionsAvailable();\n\nexport default instance;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/limaDarwin.ts",
    "content": "import semver from 'semver';\n\nimport { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport { getMacOsVersion } from '@pkg/utils/osVersion';\n\nconst console = Logging.diagnostics;\n\nlet virtualMachineMemory = Number.POSITIVE_INFINITY;\n\nmainEvents.on('settings-update', (cfg) => {\n  virtualMachineMemory = cfg.virtualMachine.memoryInGB;\n});\n\n/**\n * CheckLimaDarwin version checks for an issue where lima/qemu isn't able to\n * allocate more than 3GiB of memory when running on macOS 12.3 (darwin 21.4.0).\n *\n * See also: https://github.com/lima-vm/lima/issues/795\n */\nconst CheckLimaDarwin: DiagnosticsChecker = {\n  id:       'LIMA_DARWIN_VERSION',\n  category: DiagnosticsCategory.ContainerEngine,\n  applicable() {\n    const isDarwin = process.platform === 'darwin';\n    const isArm = process.arch === 'arm64';\n\n    return Promise.resolve(isDarwin && isArm);\n  },\n  check() {\n    const result = {\n      description: '',\n      passed:      false,\n      fixes:       [] as { description: string }[],\n    };\n    const currentVersion = getMacOsVersion();\n\n    result.passed = !!currentVersion && semver.gte(currentVersion, '12.4.0', { loose: true });\n    result.description = `This machine is running macOS ${ currentVersion }.`;\n    if (!result.passed) {\n      if (currentVersion) {\n        result.description = `This machine is running macOS ${ currentVersion }, which is too old; virtual machine memory is limited to 3GiB.`;\n        result.fixes.push({ description: 'Update your macOS installation to at least macOS 12.4 (Monterey).' });\n      } else {\n        result.description = `There was an error determining your macOS version.  Virtual memory may be limited to 3GiB.`;\n      }\n    }\n    if (Math.ceil(virtualMachineMemory) <= 3) {\n      // If we're not using more than 3GB of memory, consider this a pass.\n      result.passed = true;\n    }\n    console.debug(`${ this.id }: version=${ currentVersion } result=${ JSON.stringify(result) }`);\n\n    return Promise.resolve(result);\n  },\n};\n\nexport default CheckLimaDarwin;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/limaOverrides.ts",
    "content": "import fs from 'node:fs';\nimport path from 'node:path';\n\nimport yaml from 'yaml';\n\nimport { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerSingleResult } from './types';\n\nimport paths from '@pkg/utils/paths';\n\n/**\n * Check for things in the user's Lima overrides file.  We never create the file\n * ourselves, but the user may manually create it to adjust how Lima runs; it\n * may end up conflicting with what we attempt to do.\n */\nconst CheckLimaOverrides: DiagnosticsChecker = {\n  id:       'LIMA_OVERRIDES',\n  category: DiagnosticsCategory.ContainerEngine,\n  applicable() {\n    return Promise.resolve(process.platform !== 'win32');\n  },\n  async check() {\n    const overridePath = path.join(paths.lima, '_config', 'override.yaml');\n    const checkers = {\n      /**\n       * Check if the user has an override for the lima disk size.  We have built-in\n       * support for the feature now, and overrides would cause our settings to be\n       * ignored.\n       */\n      DISK_SIZE: (override) => {\n        if ('disk' in override) {\n          return {\n            description: `Disk overrides are set in Lima override file \\`${ overridePath }\\``,\n            passed:      false,\n            fixes:       [{\n              description: `Remove Lima override file \\`${ overridePath }\\``,\n            }],\n          };\n        }\n        return {\n          description: `Disk size override not specified in Lima override file \\`${ overridePath }\\``,\n          passed:      true,\n          fixes:       [],\n        };\n      },\n    } satisfies Record<string, (override: any) => Omit<DiagnosticsCheckerSingleResult, 'id'>>;\n    const override = await (async function() {\n      try {\n        return yaml.parse(await fs.promises.readFile(overridePath, 'utf-8'));\n      } catch {\n        return undefined;\n      }\n    })();\n\n    if (!override || typeof override !== 'object') {\n      // Override file does not exist, or is not valid YAML\n      return Object.keys(checkers).map(id => ({\n        id,\n        description: `Override file \\`${ overridePath }\\` not loaded`,\n        passed:      true,\n        fixes:       [],\n      }));\n    }\n\n    return Object.entries(checkers).map(([id, checker]) => ({\n      id,\n      ...checker(override),\n    }));\n  },\n};\n\nexport default CheckLimaOverrides;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/mobyImageStore.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport { ContainerEngine } from '@pkg/config/settings';\nimport mainEvents from '@pkg/main/mainEvents';\n\nconst id = 'MOBY_IMAGE_STORE';\nconst documentation = 'https://docs.rancherdesktop.io/how-to-guides/migrating-images/';\n\nlet containerEngine: ContainerEngine = ContainerEngine.NONE;\nlet state = {\n  hasClassicData:     false,\n  hasSnapshotterData: false,\n  useSnapshotter:     false,\n};\n\nmainEvents.on('settings-update', (settings) => {\n  containerEngine = settings.containerEngine.name;\n});\n\nmainEvents.on('diagnostics-event', (payload) => {\n  if (payload.id === 'moby-storage') {\n    state = payload;\n    mainEvents.invoke('diagnostics-trigger', id);\n  }\n});\n\n/**\n * We use moby's containerd image store for new VMs, as well as when WASM is\n * enabled; however, the migration in Rancher Desktop 1.21 had a bug that caused\n * some users to end up using the containerd snapshotter when they still had\n * data in the old moby image store.  Detect when we have data in both and warn\n * the user.\n */\nclass CheckMobyImageStore implements DiagnosticsChecker {\n  readonly id = id;\n\n  category = DiagnosticsCategory.ContainerEngine;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(containerEngine === ContainerEngine.MOBY);\n  }\n\n  async check() {\n    if (!await this.applicable()) {\n      return {\n        passed:      true,\n        description: 'Moby container engine is not in use',\n        fixes:       [],\n      };\n    }\n\n    if (state.hasClassicData && state.hasSnapshotterData) {\n      let description = 'There are images in both the moby classic storage driver and the containerd image store.';\n      if (state.useSnapshotter) {\n        description += '  Currently using the containerd snapshotter.';\n      } else {\n        description += '  Currently using the moby classic storage driver.';\n      }\n      return {\n        passed:        false,\n        description,\n        fixes:         [],\n        documentation,\n      };\n    }\n    if (state.hasClassicData && state.useSnapshotter) {\n      return {\n        passed:        false,\n        description:   `There are images in the moby classic storage driver, but the containerd snapshotter is being used.`,\n        fixes:         [],\n        documentation,\n      };\n    }\n    if (state.hasSnapshotterData && !state.useSnapshotter) {\n      return {\n        passed:        false,\n        description:   `There are images in the containerd image store, but the moby classic storage driver is being used.`,\n        fixes:         [],\n        documentation,\n      };\n    }\n\n    return {\n      passed:      true,\n      description: `There are no issues with the moby image store: classic:${ state.hasClassicData } snapshotter:${ state.hasSnapshotterData } using snapshotter:${ state.useSnapshotter }`,\n      fixes:       [],\n    };\n  }\n}\n\nexport default new CheckMobyImageStore();\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/mockForScreenshots.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\n/**\n * Sample tests for testing\n */\nclass MockChecker implements DiagnosticsChecker {\n  get id() {\n    return 'MOCK_CHECKER';\n  }\n\n  category = DiagnosticsCategory.Utilities;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(!!process.env.RD_MOCK_FOR_SCREENSHOTS);\n  }\n\n  check() {\n    return Promise.resolve({\n      passed:        false,\n      documentation: 'https://www.example.com/not-a-valid-link',\n      description:   `The \\`~/.rd/bin\\` directory has not been added to the \\`PATH\\`, so command-line utilities are not configured in your shell.`,\n      fixes:         [],\n    });\n  }\n}\n\nexport default [new MockChecker()];\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/pathManagement.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult, DiagnosticsCheckerSingleResult } from './types';\n\nimport { ErrorDeterminingExtendedAttributes, ErrorCopyingExtendedAttributes, ErrorNotRegularFile, ErrorWritingFile } from '@pkg/integrations/manageLinesInFile';\nimport mainEvents from '@pkg/main/mainEvents';\n\nconst cachedResults: Record<string, DiagnosticsCheckerResult> = {};\n\n/**\n * Check for any errors raised from handling path management (i.e. handling of\n * ~/.bashrc and related files) and report them to the user.\n */\nconst CheckPathManagement: DiagnosticsChecker = {\n  id:       'PATH_MANAGEMENT',\n  category: DiagnosticsCategory.Utilities,\n  applicable() {\n    return Promise.resolve(['darwin', 'linux'].includes(process.platform));\n  },\n  check(): Promise<DiagnosticsCheckerSingleResult[]> {\n    return Promise.resolve(Object.entries(cachedResults).map(([id, result]) => {\n      return ({\n        ...result,\n        id,\n      });\n    }));\n  },\n};\n\nmainEvents.on('diagnostics-event', (payload) => {\n  if (payload.id !== 'path-management') {\n    return;\n  }\n  const { fileName, error } = payload;\n\n  cachedResults[fileName] = {\n    description: error?.message ?? error?.toString() ?? `Unknown error managing ${ fileName }`,\n    passed:      false,\n    fixes:       [],\n    ...(() => {\n      if (!error) {\n        return { passed: true, description: `\\`${ fileName }\\` is managed` };\n      }\n\n      if (error instanceof ErrorCopyingExtendedAttributes) {\n        return { fixes: [{ description: `Remove extended attributes from \\`${ fileName }\\`` }] };\n      }\n\n      if (error instanceof ErrorNotRegularFile) {\n        return { fixes: [{ description: `Replace \\`${ fileName }\\` with a regular file` }] };\n      }\n\n      if (error instanceof ErrorWritingFile) {\n        return { fixes: [{ description: `Restore \\`${ fileName }\\` from backup file \\`${ error.backupPath }\\`` }] };\n      }\n\n      if (error instanceof ErrorDeterminingExtendedAttributes && error.cause) {\n        return { description: `${ error }: ${ error.cause }` };\n      }\n\n      return {};\n    })(),\n  };\n});\n\nexport default CheckPathManagement;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/rdBinInShell.ts",
    "content": "import path from 'path';\n\nimport which from 'which';\n\nimport { DiagnosticsCategory, DiagnosticsChecker, DiagnosticsCheckerResult } from './types';\n\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst console = Logging.diagnostics;\nconst pathOutputDelimiter = 'Rancher Desktop Diagnostics PATH:';\nlet pathStrategy = PathManagementStrategy.RcFiles;\n\nmainEvents.on('settings-update', (cfg) => {\n  pathStrategy = cfg.application.pathManagementStrategy;\n});\n\nexport class RDBinInShellPath implements DiagnosticsChecker {\n  constructor(id: string, executable: string, ...args: string[]) {\n    this.id = id;\n    this.executable = executable;\n    this.args = args.concat(`printf \"\\n${ pathOutputDelimiter }%s\\n\" \"$PATH\"`);\n  }\n\n  id:         string;\n  executable: string;\n  args:       string[];\n  category = DiagnosticsCategory.Utilities;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(['darwin', 'linux'].includes(process.platform));\n  }\n\n  async check(): Promise<DiagnosticsCheckerResult> {\n    const fixes: { description: string }[] = [];\n    let passed: boolean;\n    let description: string;\n\n    try {\n      const executable = await which(this.executable, { nothrow: true });\n\n      if (!executable) {\n        return {\n          passed:      true, // No need to throw a diagnostic in this case.\n          description: `Failed to find ${ this.executable } executable`,\n          fixes:       [{ description: `Install ${ this.executable }` }],\n        };\n      }\n\n      const integrationPath = RDBinInShellPath.removeTrailingSlash(paths.integration);\n      const currentPaths = process.env.PATH?.split(path.delimiter) ?? ['/usr/local/bin', '/usr/bin', '/bin'];\n      const fixedPath = currentPaths.map(RDBinInShellPath.removeTrailingSlash).filter(p => p !== integrationPath).join(path.delimiter);\n      const { stdout } = await spawnFile(\n        this.executable,\n        this.args,\n        { stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PATH: fixedPath } });\n      const dirs = stdout.split('\\n')\n        .filter(line => line.startsWith(pathOutputDelimiter))\n        .pop()?.split(path.delimiter)\n        .map(RDBinInShellPath.removeTrailingSlash) ?? [];\n      const desiredDirs = dirs.filter(p => p === integrationPath);\n\n      passed = desiredDirs.length > 0;\n      description = `The \\`~/.rd/bin\\` directory has not been added to the \\`PATH\\`, so command-line utilities are not configured in your **${ this.executable }** shell.`;\n      if (passed) {\n        description = `The \\`~/.rd/bin\\` directory is found in your \\`PATH\\` as seen from **${ this.executable }**.`;\n      } else if (pathStrategy !== PathManagementStrategy.RcFiles) {\n        const description = `You have selected manual \\`PATH\\` configuration;\n            consider letting Rancher Desktop automatically configure it.`;\n\n        fixes.push({ description: description.replace(/\\s+/gm, ' ') });\n      }\n    } catch (ex: any) {\n      console.error(`path diagnostics for ${ this.executable }: error: `, ex);\n      description = ex.message ?? ex.toString();\n      passed = false;\n    }\n\n    return {\n      description,\n      passed,\n      fixes,\n    };\n  }\n\n  static removeTrailingSlash(s: string): string {\n    return s.replace(/(.)\\/*$/, '$1');\n  }\n}\n\n// Use `bash -l` because `bash -i` causes RD to suspend\nconst RDBinInBash = new RDBinInShellPath('RD_BIN_IN_BASH_PATH', 'bash', '-l', '-c');\n// Use `zsh -i -l` because we can't know if the user manually added the PATH in .zshrc or in .zprofile\nconst RDBinInZsh = new RDBinInShellPath('RD_BIN_IN_ZSH_PATH', 'zsh', '-i', '-l', '-c');\n\nexport default [RDBinInBash, RDBinInZsh] as DiagnosticsChecker[];\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/testCheckers.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport { isDevEnv } from '@pkg/utils/environment';\n\n/**\n * Sample tests for testing\n */\nclass CheckTesting implements DiagnosticsChecker {\n  pass: boolean;\n  constructor(pass: boolean) {\n    this.pass = pass;\n  }\n\n  get id() {\n    return `STATIC_${ this.pass.toString().toUpperCase() }`;\n  }\n\n  category = DiagnosticsCategory.Testing;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(isDevEnv && !process.env.RD_MOCK_FOR_SCREENSHOTS);\n  }\n\n  check() {\n    return Promise.resolve({\n      passed:        this.pass,\n      documentation: 'https://www.example.com/not-a-valid-link',\n      description:   `This is a \\`sample\\` test that will **${ this.pass ? 'always' : 'never' }** pass.`,\n      fixes:         [],\n    });\n  }\n}\n\nexport default [new CheckTesting(true), new CheckTesting(false)];\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/types.ts",
    "content": "export enum DiagnosticsCategory {\n  ContainerEngine = 'Container Engine',\n  Kubernetes = 'Kubernetes',\n  Networking = 'Networking',\n  Utilities = 'Utilities',\n  Testing = 'Testing',\n}\n\ninterface DiagnosticsFix {\n  /** A textual description of the fix to be displayed to the user. */\n  description: string;\n}\n\n/**\n * DiagnosticsCheckerResult is the result for running a given diagnostics\n * checker.\n */\nexport interface DiagnosticsCheckerResult {\n  /** Link to documentation about this check. */\n  documentation?: string,\n  /** User-visible markdown description about this check. */\n  description:    string,\n  /** If true, the check succeeded (no fixes need to be applied). */\n  passed:         boolean,\n  /** Potential fixes when this check fails. */\n  fixes:          DiagnosticsFix[],\n}\n\nexport type DiagnosticsCheckerSingleResult = DiagnosticsCheckerResult & {\n  /**\n   * For checkers returning multiple results, each result must have its own\n   * identifier that is consistent over checker runs.\n   */\n  id: string;\n};\n\n/**\n * DiagnosticsChecker describes an implementation of a diagnostics checker.\n * The checker may return one or more results.\n */\nexport interface DiagnosticsChecker {\n  /** Unique identifier for this check. */\n  id:       string;\n  category: DiagnosticsCategory,\n  /**\n   * Whether any of the checks this checker supports should be used on this\n   * system.\n   */\n  applicable(): Promise<boolean>,\n  /**\n   * Perform the check.  If this checker does multiple checks, any checks that\n   * are not applicable on this system should be skipped (rather than returning\n   * a failing result).\n   */\n  check(): Promise<DiagnosticsCheckerResult | DiagnosticsCheckerSingleResult[]>;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/wslDistros.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\n\n/**\n * Check for known-incompatible WSL distributions\n */\nclass CheckWSLDistros implements DiagnosticsChecker {\n  readonly id = 'WSL_DISTROS';\n\n  category = DiagnosticsCategory.Testing;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(process.platform === 'win32');\n  }\n\n  async check() {\n    const banned = new Set(['wsl-vpnkit']);\n    try {\n      const { stdout } = await spawnFile(\n        'wsl.exe',\n        ['--list', '--quiet'],\n        { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf-8', env: { WSL_UTF8: '1' } },\n      );\n      const distros = new Set(stdout.split(/\\s+/m));\n      const issues = banned.intersection(distros);\n\n      if (issues.size === 0) {\n        return {\n          passed:      true,\n          description: 'No unsupported WSL distributions detected',\n          fixes:       [],\n        };\n      }\n\n      return Array.from(issues).map(distro => ({\n        id:          distro,\n        passed:      false,\n        description: `WSL distribution \\`${ distro }\\` causes issues with Rancher Desktop`,\n        fixes:       [{ description: `Remove WSL distribution \\`${ distro }\\`` }],\n      }));\n    } catch (ex: any) {\n      return {\n        passed:      false,\n        description: `There was an error checking for unknown WSL distributions: \\`${ ex?.stderr || ex }\\``,\n        fixes:       [],\n      };\n    }\n  }\n}\n\nexport default new CheckWSLDistros();\n"
  },
  {
    "path": "pkg/rancher-desktop/main/diagnostics/wslInfo.ts",
    "content": "import { DiagnosticsCategory, DiagnosticsChecker } from './types';\n\nimport getWSLVersion, { compareVersion, makeVersion, versionString } from '@pkg/utils/wslVersion';\n\n/**\n * Check information about WSL.\n */\nclass CheckWSLFromStore implements DiagnosticsChecker {\n  readonly id = 'WSL_INFO';\n\n  category = DiagnosticsCategory.Testing;\n  applicable(): Promise<boolean> {\n    return Promise.resolve(process.platform === 'win32');\n  }\n\n  async check() {\n    // Microsoft Store URL for WSL; product ID is from searching the store.\n    const storeURL = 'ms-windows-store://pdp/?ProductId=9P9TQF7MRM4R&mode=mini';\n    const version = await getWSLVersion();\n\n    if (!version.installed) {\n      // Since all versions we care about can install from the store now, just\n      // say that.\n      return {\n        passed:      false,\n        description: 'Windows Subsystem for Linux is not installed.',\n        fixes:       [{\n          description: `Install Windows Subsystem for Linux from the [Microsoft Store](${ storeURL }).`,\n          url:         storeURL,\n        }],\n      };\n    }\n\n    if (!version.has_kernel) {\n      // The kernel is not installed; this covers virtualization not available.\n      return {\n        passed:      false,\n        description: `The WSL kernel does not appear to be installed.`,\n        fixes:       [{ description: 'Install the WSL kernel with `wsl.exe --update`' }],\n      };\n    }\n\n    if (compareVersion(version.version, makeVersion(2, 5, 7)) < 0) {\n      return {\n        passed:      false,\n        description: `WSL version ${ versionString(version.version) } is too old.`,\n        fixes:       [{ description: 'Update WSL with `wsl.exe --update`' }],\n      };\n    }\n\n    return {\n      passed: true, description: `WSL is installed (version ${ versionString(version.version) }).`, fixes: [],\n    };\n  }\n}\n\nexport default new CheckWSLFromStore();\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/__tests__/extensions.spec.ts",
    "content": "import { ExtensionImpl } from '@pkg/main/extensions/extensions';\n\ndescribe('ExtensionImpl', () => {\n  describe('checkInstallAllowed', () => {\n    const subject = ExtensionImpl['checkInstallAllowed'];\n\n    it('should reject invalid image references', () => {\n      expect(() => subject(undefined, '/')).toThrow();\n    });\n\n    it('should allow images if the allow list is not enabled', () => {\n      expect(() => subject(undefined, 'image')).not.toThrow();\n    });\n\n    it('should disallow any images given an empty list', () => {\n      expect(() => subject([], 'image')).toThrow();\n    });\n\n    it('should allow specified image', () => {\n      expect(() => subject(['other', 'image'], 'image')).not.toThrow();\n    });\n\n    it('should reject unknown image', () => {\n      expect(() => subject(['allowed'], 'image')).toThrow();\n    });\n\n    it('should support missing tags', () => {\n      expect(() => subject(['image'], 'image:1.0.0')).not.toThrow();\n    });\n\n    it('should reject images with the wrong tag', () => {\n      expect(() => subject(['image:0.1'], 'image:0.2')).toThrow();\n    });\n\n    it('should support image references with registries', () => {\n      const ref = 'r.example.test:1234/org/name:tag';\n\n      expect(() => subject([ref], ref)).not.toThrow();\n    });\n\n    it('should support org-level references', () => {\n      expect(() => subject(['test.invalid/org/'], 'test.invalid/org/image:tag')).not.toThrow();\n    });\n\n    it('should support registry-level references', () => {\n      expect(() => subject(['registry.test/'], 'registry.test/image:tag')).not.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/__tests__/manager.spec.ts",
    "content": "import { jest } from '@jest/globals';\n\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\nmockModules({ electron: undefined });\n\nconst { ExtensionManagerImpl } = await import('../manager');\n\ndescribe('ExtensionManagerImpl', () => {\n  describe('findBestVersion', () => {\n    let subject: InstanceType<typeof ExtensionManagerImpl>;\n\n    beforeEach(() => {\n      subject = new ExtensionManagerImpl({ getTags: jest.fn() } as any, false);\n    });\n\n    test.each<[string[], string | RegExp | undefined]>([\n      // Highest semver\n      [['0.0.1', '0.0.3', '0.0.2'], '0.0.3'],\n      // Use latest\n      [['foo', 'latest', 'bar', 'xyzzy'], 'latest'],\n      // No tags available\n      [[], undefined],\n      // Prefer proper semver over random numbers embedded in strings\n      [['foo', 'chore23', '0.0.1'], '0.0.1'],\n      // ... including with \"v\" prefix\n      [['foo', 'chore23', 'v0.0.1'], 'v0.0.1'],\n      // ... or \"v.\" prefix\n      [['foo', 'chore23', 'v.0.0.1'], 'v.0.0.1'],\n      // If no semver, grab anything with numbers\n      [['foo1', 'foo3', 'foo2'], 'foo3'],\n    ])('%s => %s', async(versions, expected) => {\n      jest.spyOn(subject.client, 'getTags').mockImplementation(() => {\n        return Promise.resolve(new Set(versions));\n      });\n      if (expected === undefined) {\n        await expect(subject['findBestVersion']('')).rejects.toThrow();\n      } else if (typeof expected === 'string') {\n        await expect(subject['findBestVersion']('')).resolves.toEqual(expected);\n      } else {\n        await expect(subject['findBestVersion']('')).resolves.toMatch(expected);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/extensions.ts",
    "content": "import { ChildProcessByStdio, spawn } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport { Readable } from 'stream';\n\nimport Electron from 'electron';\nimport _ from 'lodash';\nimport yaml from 'yaml';\n\nimport {\n  Extension, ExtensionError, ExtensionErrorCode, ExtensionErrorMarker, ExtensionMetadata, SpawnOptions,\n} from './types';\n\nimport type { ContainerEngineClient } from '@pkg/backend/containerClient';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { parseImageReference } from '@pkg/utils/dockerUtils';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { defined } from '@pkg/utils/typeUtils';\n\n/**\n * ComposeFile describes the contents of a compose file.\n * @note The typing here is incomplete.\n */\ninterface ComposeFile {\n  name?:    string;\n  services: Record<string, {\n    image?:       string;\n    environment?: string[];\n    command?:     string;\n    volumes?: (string | {\n      type:       string;\n      source?:    string;\n      target:     string;\n      read_only?: boolean;\n      bind?: {\n        propagation?:      string;\n        create_host_path?: boolean;\n        selinux?:          'z' | 'Z';\n      };\n      volume?:      { nocopy?: boolean };\n      tmpfs?:       { size?: number | string; mode?: number };\n      consistency?: string;\n    })[];\n  }>;\n  volumes?: Record<string, any>;\n}\n\n// ScriptType is any key in ExtensionMetadata.host that starts with `x-rd-`.\ntype ScriptType = keyof {\n  [K in keyof Required<ExtensionMetadata>['host'] as K extends `x-rd-${ infer _U }` ? K : never]: 1;\n};\n\nconst console = Logging.extensions;\n\nexport class ExtensionErrorImpl extends Error implements ExtensionError {\n  [ExtensionErrorMarker] = 0;\n  code: ExtensionErrorCode;\n\n  constructor(code: ExtensionErrorCode, message: string, cause?: Error) {\n    // XXX We're currently using a version of TypeScript that doesn't have the\n    // ES2022.Error lib, so it doesn't understand the \"cause\" option to the\n    // Error constructor.  Work around this by explicitly calling setting the\n    // cause.  It appears to still be printed in that case.\n    super(message);\n    if (cause) {\n      (this as any).cause = cause;\n    }\n    this.code = code;\n  }\n}\n\n/**\n * isVMTypeImage asserts that a ExtensionMetadata.vm is an image.\n */\nfunction isVMTypeImage(input: ExtensionMetadata['vm']): input is { image: string } {\n  return typeof (input as any)?.image === 'string';\n}\n\n/**\n * isVMTypeComposefile asserts that a ExtensionMetadata.vm is a composefile.\n */\nfunction isVMTypeComposefile(input: ExtensionMetadata['vm']): input is { composefile: string } {\n  return typeof (input as any)?.composefile === 'string';\n}\n\nexport class ExtensionImpl implements Extension {\n  constructor(id: string, tag: string, client: ContainerEngineClient) {\n    const encodedId = Buffer.from(id, 'utf-8').toString('base64url');\n\n    this.id = id;\n    this.version = tag;\n    this.client = client;\n    this.dir = path.join(paths.extensionRoot, encodedId);\n  }\n\n  /** The extension ID (the image ID), excluding the tag */\n  id:                        string;\n  /** The extension image tag */\n  version:                   string;\n  /** The directory this extension will be installed into */\n  readonly dir:              string;\n  protected readonly client: ContainerEngineClient;\n  protected _metadata:       Promise<ExtensionMetadata> | undefined;\n  protected _labels:         Promise<Record<string, string>> | undefined;\n  /** The (nerdctl) namespace to use; shared with ExtensionManagerImpl */\n  static readonly extensionNamespace = 'rancher-desktop-extensions';\n  protected readonly VERSION_FILE = 'version.txt';\n  protected get extensionNamespace() {\n    return ExtensionImpl.extensionNamespace;\n  }\n\n  get image() {\n    return `${ this.id }:${ this.version }`;\n  }\n\n  /** Extension metadata */\n  get metadata(): Promise<ExtensionMetadata> {\n    this._metadata ??= (async() => {\n      const fallback = { vm: {} };\n\n      try {\n        const raw = await this.readFile('metadata.json');\n        const result = _.merge({}, fallback, JSON.parse(raw));\n\n        if (result.icon) {\n          return result;\n        }\n      } catch (ex: any) {\n        console.error(`Failed to read metadata for ${ this.id }: ${ ex }`);\n        // Unset metadata so we can try again later\n        this._metadata = undefined;\n        throw new ExtensionErrorImpl(ExtensionErrorCode.INVALID_METADATA, 'Could not read extension metadata', ex);\n      }\n      // If we reach here, we got the metadata but there was no icon set.\n      // There's no point in retrying in that case.\n      throw new ExtensionErrorImpl(ExtensionErrorCode.INVALID_METADATA, 'Invalid extension: missing icon');\n    })();\n\n    return this._metadata;\n  }\n\n  /** Extension image labels */\n  get labels(): Promise<Record<string, string>> {\n    this._labels ??= (async() => {\n      try {\n        if (await this.isInstalled()) {\n          const labelPath = path.join(this.dir, 'labels.json');\n\n          return JSON.parse(await fs.promises.readFile(labelPath, 'utf-8'));\n        }\n\n        const info = await this.client.runClient(\n          ['image', 'inspect', '--format={{ json .Config.Labels }}', this.image],\n          'pipe',\n          { namespace: ExtensionImpl.extensionNamespace });\n\n        return JSON.parse(info.stdout);\n      } catch (ex: any) {\n        // Unset cached value so we can try again later\n        this._labels = undefined;\n        throw new ExtensionErrorImpl(ExtensionErrorCode.INVALID_METADATA, 'Could not read image labels', ex);\n      }\n    })();\n\n    return this._labels;\n  }\n\n  protected _iconName: Promise<string> | undefined;\n\n  /** iconName is the file name of the icon (e.g. icon.png, icon.svg) */\n  get iconName(): Promise<string> {\n    this._iconName ??= (async() => {\n      return `icon${ path.extname((await this.metadata).icon) }`;\n    })();\n\n    return this._iconName;\n  }\n\n  /**\n   * Check if the given image is allowed to be installed according to the\n   * extension allow list.\n   * @throws If the image is not allowed to be installed.\n   */\n  protected static checkInstallAllowed(allowedImages: readonly string[] | undefined, image: string) {\n    const desired = parseImageReference(image);\n    const code = ExtensionErrorCode.INSTALL_DENIED;\n    const prefix = `Disallowing install of ${ image }:`;\n\n    if (!desired) {\n      throw new ExtensionErrorImpl(code, `${ prefix } Invalid image reference`);\n    }\n    if (!allowedImages) {\n      return;\n    }\n    for (const pattern of allowedImages) {\n      const allowed = parseImageReference(pattern, true);\n\n      if (allowed?.tag && allowed.tag !== desired.tag) {\n        // This pattern doesn't match the tag, look for something else.\n        continue;\n      }\n\n      if (allowed?.registry.href !== desired.registry.href) {\n        // This pattern has a different registry\n        continue;\n      }\n\n      if (!allowed.name) {\n        // If there's no name given, the whole registry is allowed.\n        return '';\n      }\n\n      if (allowed.name.endsWith('/')) {\n        if (desired.name.startsWith(allowed.name)) {\n          // The allowed pattern ends with a slash, anything in the org is fine.\n          return '';\n        }\n      } else if (allowed.name === desired.name) {\n        return '';\n      }\n    }\n\n    throw new ExtensionErrorImpl(code, `${ prefix } Image is not allowed`);\n  }\n\n  /**\n   * Determine the post-install or pre-uninstall script to run, if any.\n   * Returns the script executable plus arguments; the executable path is always\n   * absolute.\n   */\n  protected getScriptArgs(metadata: ExtensionMetadata, key: ScriptType): string[] | undefined {\n    const scriptData = metadata.host?.[key]?.[this.platform];\n\n    if (!scriptData) {\n      return;\n    }\n\n    const [scriptName, ...scriptArgs] = Array.isArray(scriptData) ? scriptData : [scriptData];\n    const description = {\n      'x-rd-install':   'Post-install',\n      'x-rd-uninstall': 'Pre-uninstall',\n      'x-rd-shutdown':  'Shutdown',\n    }[key];\n    const binDir = path.join(this.dir, 'bin');\n    const scriptPath = path.normalize(path.resolve(binDir, scriptName));\n\n    if (/^\\.+[/\\\\]/.test(path.relative(binDir, scriptPath))) {\n      throw new Error(`${ description } script for ${ this.id } (${ scriptName }) not inside binaries directory`);\n    }\n\n    return [scriptPath, ...scriptArgs];\n  }\n\n  async install(allowedImages: readonly string[] | undefined): Promise<boolean> {\n    const metadata = await this.metadata;\n\n    ExtensionImpl.checkInstallAllowed(allowedImages, this.image);\n    console.debug(`Image ${ this.image } is allowed to install: ${ allowedImages }`);\n\n    await fs.promises.mkdir(this.dir, { recursive: true });\n    try {\n      await this.installMetadata(this.dir, metadata);\n      await this.installIcon(this.dir, metadata);\n      await this.installUI(this.dir, metadata);\n      await this.installHostExecutables(this.dir, metadata);\n      await this.installContainers(this.dir, metadata);\n      await this.markInstalled(this.dir);\n    } catch (ex) {\n      console.error(`Failed to install extension ${ this.id }, cleaning up:`, ex);\n      await fs.promises.rm(this.dir, { recursive: true, maxRetries: 3 }).catch((e) => {\n        console.error(`Failed to cleanup extension directory ${ this.dir }`, e);\n      });\n      throw ex;\n    }\n\n    mainEvents.emit('settings-write', { application: { extensions: { installed: { [this.id]: this.version } } } });\n\n    try {\n      const [scriptPath, ...scriptArgs] = this.getScriptArgs(metadata, 'x-rd-install') ?? [];\n\n      if (scriptPath) {\n        console.log(`Running ${ this.id } post-install script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);\n        await spawnFile(scriptPath, scriptArgs, { stdio: console, cwd: path.dirname(scriptPath) });\n      }\n    } catch (ex) {\n      console.error(`Ignoring error running ${ this.id } post-install script: ${ ex }`);\n    }\n\n    // Since we now run extensions in a separate session, register the protocol handler there.\n    const encodedId = Buffer.from(this.id).toString('hex');\n\n    await mainEvents.invoke('extensions/register-protocol', `persist:rdx-${ encodedId }`);\n\n    // Clear the default session cache so updated icons are loaded fresh\n    Electron.session.defaultSession.clearCache();\n\n    console.debug(`Install ${ this.id }: install complete.`);\n\n    return true;\n  }\n\n  protected async installMetadata(workDir: string, metadata: ExtensionMetadata): Promise<void> {\n    await Promise.all([\n      fs.promises.writeFile(\n        path.join(workDir, 'metadata.json'),\n        JSON.stringify(metadata, undefined, 2)),\n      fs.promises.writeFile(\n        path.join(workDir, 'labels.json'),\n        JSON.stringify(await this.labels, undefined, 2)),\n    ]);\n  }\n\n  protected async installIcon(workDir: string, metadata: ExtensionMetadata): Promise<void> {\n    try {\n      const origIconName = path.basename(metadata.icon);\n\n      try {\n        await this.client.copyFile(this.image, metadata.icon, workDir, { namespace: this.extensionNamespace });\n      } catch (ex) {\n        throw new ExtensionErrorImpl(ExtensionErrorCode.FILE_NOT_FOUND, `Could not copy icon file ${ metadata.icon }`, ex as Error);\n      }\n      if (origIconName !== await this.iconName) {\n        await fs.promises.rename(path.join(workDir, origIconName), path.join(workDir, await this.iconName));\n      }\n    } catch (ex) {\n      console.error(`Could not copy icon for extension ${ this.id }: ${ ex }`);\n      throw ex;\n    }\n  }\n\n  protected async installUI(workDir: string, metadata: ExtensionMetadata): Promise<void> {\n    if (!metadata.ui) {\n      return;\n    }\n\n    const uiDir = path.join(workDir, 'ui');\n\n    await fs.promises.mkdir(uiDir, { recursive: true });\n    await Promise.all(Object.entries(metadata.ui).map(async([name, data]) => {\n      try {\n        await fs.promises.mkdir(path.join(uiDir, name), { recursive: true });\n\n        if (!data?.root) {\n          throw new Error('Error: installUI - data.root is undefined');\n        }\n\n        await this.client.copyFile(\n          this.image,\n          data.root,\n          path.join(uiDir, name),\n          { namespace: this.extensionNamespace });\n      } catch (ex: any) {\n        throw new ExtensionErrorImpl(ExtensionErrorCode.FILE_NOT_FOUND, `Could not copy UI ${ name }`, ex);\n      }\n    }));\n  }\n\n  protected get platform() {\n    switch (process.platform) {\n    case 'win32':\n      return 'windows';\n    case 'linux':\n    case 'darwin':\n      return process.platform;\n    default:\n      throw new Error(`Platform ${ process.platform } is not supported`);\n    }\n  }\n\n  protected async installHostExecutables(workDir: string, metadata: ExtensionMetadata): Promise<void> {\n    const binDir = path.join(workDir, 'bin');\n\n    await fs.promises.mkdir(binDir, { recursive: true });\n    const binaries = metadata.host?.binaries ?? [];\n    const paths = binaries.flatMap(p => p[this.platform]).map(b => b?.path).filter(defined);\n\n    await Promise.all(paths.map(async(p) => {\n      try {\n        await this.client.copyFile(this.image, p, binDir, { namespace: this.extensionNamespace });\n      } catch (ex: any) {\n        throw new ExtensionErrorImpl(ExtensionErrorCode.FILE_NOT_FOUND, `Could not copy host binary ${ p }`, ex);\n      }\n    }));\n  }\n\n  protected async getComposeName() {\n    if (this._composeName) {\n      return this._composeName;\n    }\n\n    const normalizedId = this.id.toLowerCase().replaceAll(/[^a-z0-9_-]/g, '_');\n    const contents = await this.getComposeFileContents();\n    let composeName = `rd-extension-${ normalizedId }`;\n\n    const maxServiceLength = Math.max(...Object.keys(contents.services).map(n => n.length));\n\n    // On nerdctl, container names are something like:\n    // <this.composeName>_<compose .services.*>_<counter>\n    // If this string is longer than 76 characters, installation will fail.\n    if (composeName.length + maxServiceLength + 4 > 76) {\n      composeName = normalizedId.slice(0, 76 - maxServiceLength - 4);\n    }\n\n    this._composeName = composeName;\n\n    return composeName;\n  }\n\n  /** memoized result of getComposeName() */\n  protected _composeName = '';\n\n  /**\n   * Return the contents of the compose file.\n   */\n  protected async getComposeFileContents(): Promise<ComposeFile> {\n    if (await this.isInstalled()) {\n      const composePath = path.join(this.dir, 'compose', 'compose.yaml');\n\n      return yaml.parse(await fs.promises.readFile(composePath, 'utf-8'));\n    }\n\n    const metadata = await this.metadata;\n\n    if (isVMTypeImage(metadata.vm)) {\n      // Only an image was specified, make up a compose file.\n      return {\n        name:     this.id.replace(/[^a-z0-9_-]/g, '_'),\n        // Disable lint because it's a literal ${DESKTOP_PLUGIN_IMAGE} string.\n        // eslint-disable-next-line no-template-curly-in-string\n        services: { web: { image: '${DESKTOP_PLUGIN_IMAGE}' } },\n      };\n    }\n    if (isVMTypeComposefile(metadata.vm)) {\n      const composePath = path.posix.normalize(metadata.vm.composefile);\n      const opts = { namespace: this.extensionNamespace };\n\n      return yaml.parse(await this.client.readFile(this.image, composePath, opts));\n    }\n    throw new Error(`Invalid vm type`);\n  }\n\n  protected async installContainers(workDir: string, metadata: ExtensionMetadata): Promise<void> {\n    const composeDir = path.join(workDir, 'compose');\n    let contents: ComposeFile;\n\n    // Extract compose file and place it in composeDir\n    if (isVMTypeImage(metadata.vm)) {\n      contents = await this.getComposeFileContents();\n      await fs.promises.mkdir(composeDir, { recursive: true });\n    } else if (isVMTypeComposefile(metadata.vm)) {\n      const imageComposeDir = path.posix.dirname(path.posix.normalize(metadata.vm.composefile));\n\n      await fs.promises.mkdir(composeDir, { recursive: true });\n      await this.client.copyFile(\n        this.image,\n        imageComposeDir === '.' ? '/' : `${ imageComposeDir }/`,\n        composeDir,\n        { namespace: this.extensionNamespace });\n\n      contents = await this.getComposeFileContents();\n      // Always clobber the compose project name to avoid issues with length.\n      contents.name = await this.getComposeName();\n    } else {\n      console.debug(`Extension ${ this.id } does not have containers to run.`);\n\n      return;\n    }\n\n    if (metadata.vm.exposes?.socket) {\n      _.merge(contents, {\n        services: {\n          'r-d-x-port-forwarding': {\n            image:       'ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy:latest',\n            environment: { SOCKET: `/run/guest-services/${ metadata.vm.exposes.socket }` },\n            ports:       ['80'],\n          },\n        },\n        volumes: { 'r-d-x-guest-services': { labels: { 'io.rancherdesktop.type': 'guest-services' } } },\n      });\n\n      // Fix up the compose file to always have a volume at /run/guest-services/\n      // so that it can be used for sockets to be exposed.\n      for (const service of Object.values(contents.services)) {\n        service.volumes ??= [];\n        if (!service.volumes.find(v => typeof v !== 'string' && v?.target === '/run/guest-services')) {\n          service.volumes.push({\n            type:   'volume',\n            source: 'r-d-x-guest-services',\n            target: '/run/guest-services',\n            volume: { nocopy: true },\n          });\n        }\n      }\n    }\n\n    // Write out the modified compose file, either clobbering the original or\n    // using the preferred name and shadowing the original.\n    await fs.promises.writeFile(path.join(composeDir, 'compose.yaml'), JSON.stringify(contents));\n\n    // Run `ctrctl compose up`\n    console.debug(`Running ${ this.id } compose up for ${ contents.name }`);\n    await this.client.composeUp(\n      {\n        composeDir,\n        name:      await this.getComposeName(),\n        namespace: this.extensionNamespace,\n        env:       { DESKTOP_PLUGIN_IMAGE: this.image },\n      },\n    );\n  }\n\n  protected async markInstalled(workDir: string) {\n    await fs.promises.writeFile(path.join(workDir, this.VERSION_FILE), this.version, 'utf-8');\n  }\n\n  async uninstall(): Promise<boolean> {\n    const installedVersion = await this.getInstalledVersion();\n\n    if (installedVersion !== undefined && installedVersion !== this.version) {\n      // A _different_ version is installed; nothing to do here.\n      // Note that we continue if no version is installed, in case there was a\n      // partial install (so we can clean up leftover files).\n      console.debug(`Extension ${ this.id }:${ installedVersion } is installed, skipping uninstall of ${ this.image }.`);\n\n      return false;\n    }\n\n    try {\n      const [scriptPath, ...scriptArgs] = this.getScriptArgs(await this.metadata, 'x-rd-uninstall') ?? [];\n\n      if (scriptPath) {\n        console.log(`Running ${ this.id } pre-uninstall script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);\n        await spawnFile(scriptPath, scriptArgs, { stdio: console, cwd: path.dirname(scriptPath) });\n      }\n    } catch (ex) {\n      console.error(`Ignoring error running ${ this.id } pre-uninstall script: ${ ex }`);\n    }\n\n    try {\n      await this.uninstallContainers();\n    } catch (ex) {\n      console.error(`Ignoring error stopping ${ this.id } containers on uninstall: ${ ex }`);\n    }\n\n    try {\n      await fs.promises.rm(this.dir, { recursive: true, maxRetries: 3 });\n    } catch (ex: any) {\n      if ((ex as NodeJS.ErrnoException).code !== 'ENOENT') {\n        throw ex;\n      }\n    }\n\n    // Clear memoized caches so that if the extension is reinstalled, fresh\n    // metadata will be extracted from the Docker image.\n    this._metadata = undefined;\n    this._labels = undefined;\n    this._iconName = undefined;\n    this._composeFile = undefined;\n    this._composeName = '';\n\n    mainEvents.emit('settings-write', { application: { extensions: { installed: { [this.id]: undefined } } } });\n\n    return true;\n  }\n\n  protected async uninstallContainers() {\n    const metadata = await this.metadata;\n\n    if (!isVMTypeImage(metadata.vm) && !isVMTypeComposefile(metadata.vm)) {\n      console.debug(`Extension ${ this.id } does not have containers to stop.`);\n\n      return;\n    }\n\n    console.debug(`Running ${ this.id } compose down`);\n    await this.client.composeDown({\n      composeDir: path.join(this.dir, 'compose'),\n      name:       await this.getComposeName(),\n      namespace:  this.extensionNamespace,\n      env:        { DESKTOP_PLUGIN_IMAGE: this.image },\n    });\n  }\n\n  protected async getInstalledVersion(): Promise<string | undefined> {\n    try {\n      const filePath = path.join(this.dir, this.VERSION_FILE);\n      const installed = await fs.promises.readFile(filePath, 'utf-8');\n\n      return installed.trim();\n    } catch (ex) {\n      return undefined;\n    }\n  }\n\n  async isInstalled(): Promise<boolean> {\n    return this.version === await this.getInstalledVersion();\n  }\n\n  _composeFile: Promise<any> | undefined;\n  get composeFile(): Promise<any> {\n    this._composeFile ??= (async() => {\n      // Because we wrote out `compose.yaml` in installContainers(), we\n      // can assume that name.\n\n      const filePath = path.join(this.dir, 'compose', 'compose.yaml');\n\n      return yaml.parse(await fs.promises.readFile(filePath, 'utf-8'));\n    })();\n\n    return this._composeFile;\n  }\n\n  async getBackendPort() {\n    const portInfo = await this.client.composePort({\n      composeDir: path.join(this.dir, 'compose'),\n      name:       await this.getComposeName(),\n      namespace:  this.extensionNamespace,\n      env:        { DESKTOP_PLUGIN_IMAGE: this.image },\n      service:    'r-d-x-port-forwarding',\n      port:       80,\n      protocol:   'tcp',\n    });\n\n    // The port info looks like \"0.0.0.0:1234\", return only the port number.\n    return /:(\\d+)$/.exec(portInfo)?.[1];\n  }\n\n  async composeExec(options: SpawnOptions): Promise<ChildProcessByStdio<null, Readable, Readable>> {\n    const metadata = await this.metadata;\n\n    if (!isVMTypeImage(metadata.vm) && !isVMTypeComposefile(metadata.vm)) {\n      throw new Error(`Could not run exec, extension ${ this.id } does not have containers`);\n    }\n\n    const composeData = await this.composeFile;\n    const service = Object.keys(composeData?.services ?? {}).shift();\n\n    if (!service) {\n      throw new Error('No services found, cannot run exec');\n    }\n\n    return this.client.composeExec({\n      composeDir: path.join(this.dir, 'compose'),\n      name:       await this.getComposeName(),\n      namespace:  this.extensionNamespace,\n      env:        { ...options.env, DESKTOP_PLUGIN_IMAGE: this.image },\n      service,\n      command:    options.command,\n      ...options.cwd ? { workdir: options.cwd } : {},\n    });\n  }\n\n  async extractFile(sourcePath: string, destinationPath: string): Promise<void> {\n    await this.client.copyFile(\n      this.image,\n      sourcePath,\n      destinationPath,\n      { namespace: this.extensionNamespace });\n  }\n\n  async readFile(sourcePath: string): Promise<string> {\n    return await this.client.readFile(this.image, sourcePath, { namespace: this.extensionNamespace });\n  }\n\n  async shutdown() {\n    // Don't trigger downloading the extension if it hasn't been installed.\n    const metadata = await this._metadata;\n\n    if (!metadata) {\n      return;\n    }\n    try {\n      const [scriptPath, ...scriptArgs] = this.getScriptArgs(metadata, 'x-rd-shutdown') ?? [];\n\n      if (scriptPath) {\n        console.log(`Running ${ this.id } shutdown script: ${ scriptPath } ${ scriptArgs.join(' ') }...`);\n        // No need to wait for the script to finish here.\n        const stream = await console.fdStream;\n        const process = spawn(scriptPath, scriptArgs, {\n          detached: true, stdio: ['ignore', stream, stream], cwd: path.dirname(scriptPath), windowsHide: true,\n        });\n\n        process.unref();\n      }\n    } catch (ex) {\n      console.error(`Ignoring error running ${ this.id } post-install script: ${ ex }`);\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/index.ts",
    "content": "export * from './types';\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/manager.ts",
    "content": "import { ChildProcessByStdio, spawn, SpawnOptionsWithStdioTuple } from 'child_process';\nimport http from 'http';\nimport path from 'path';\nimport { Readable } from 'stream';\n\nimport Electron, { IpcMainEvent, IpcMainInvokeEvent, net } from 'electron';\nimport _ from 'lodash';\nimport semver from 'semver';\n\nimport { ExtensionErrorImpl, ExtensionImpl } from './extensions';\nimport {\n  Extension, ExtensionErrorCode, ExtensionManager, SpawnOptions, SpawnResult,\n} from './types';\n\nimport MARKETPLACE_DATA from '@pkg/assets/extension-data.yaml';\nimport type { ContainerEngineClient } from '@pkg/backend/containerClient';\nimport { ContainerEngine, Settings } from '@pkg/config/settings';\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport mainEvents from '@pkg/main/mainEvents';\nimport type { IpcMainEvents, IpcMainInvokeEvents, IpcRendererEvents } from '@pkg/typings/electron-ipc';\nimport { parseImageReference } from '@pkg/utils/dockerUtils';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\nimport { RecursiveReadonly } from '@pkg/utils/typeUtils';\n\nconst console = Logging.extensions;\nconst ipcMain = getIpcMainProxy(console);\nlet manager: ExtensionManager | undefined;\n\ntype IpcMainEventListener<K extends keyof IpcMainEvents> =\n  (event: IpcMainEvent, ...args: Parameters<IpcMainEvents[K]>) => void;\n\ntype IpcMainEventHandler<K extends keyof IpcMainInvokeEvents> =\n  (event: IpcMainInvokeEvent, ...args: Parameters<IpcMainInvokeEvents[K]>) =>\n    Promise<ReturnType<IpcMainInvokeEvents[K]>> | ReturnType<IpcMainInvokeEvents[K]>;\n\ntype ReadableChildProcess = ChildProcessByStdio<null, Readable, Readable>;\n\n/**\n * EXTENSION_APP is a fake extension ID that signifies the other end is not a\n * real extension, but instead is our main application.\n */\nconst EXTENSION_APP = '<app>';\n\n/**\n * Flag to indicate that we've already spawned the process to wait for the main\n * process to shut down; this is global because we may need to restart the\n * extension manager on settings change, but we should not spawn a new copy of\n * the process watcher in that case.\n */\nlet mainProcessWatcherInitialized = false;\n\nexport class ExtensionManagerImpl implements ExtensionManager {\n  /**\n   * Known extensions.  Keyed by the image (excluding tag), then the tag.\n   * @note Items here are not necessarily installed, but all installed\n   * extensions are listed.\n   */\n  protected extensions: Record<string, Record<string, ExtensionImpl>> = {};\n\n  constructor(client: ContainerEngineClient, containerd: boolean) {\n    this.client = client;\n    this.containerd = containerd;\n  }\n\n  readonly client: ContainerEngineClient;\n\n  /**\n   * Flag indicating whether we're using containerd.\n   * @note avoid if possible.\n   */\n  readonly containerd: boolean;\n\n  /**\n   * Mapping of event listeners we used with ipcMain.on(), which will be used to\n   * ensure we unregister them correctly.\n   */\n  protected eventListeners: {\n    [channel in keyof IpcMainEvents]?: IpcMainEventListener<channel>;\n  } = {};\n\n  /**\n   * Mapping of event handlers we used with ipcMain.handle(), which will be used\n   * to ensure we unregister them correctly.\n   */\n  protected eventHandlers: {\n    [channel in keyof IpcMainInvokeEvents]?: IpcMainEventHandler<channel>;\n  } = {};\n\n  /**\n   * Attach a listener to ipcMainEvents that will be torn down when this\n   * extension manager shuts down.\n   * @note Only one listener per topic is supported.\n   */\n  protected setMainListener<K extends keyof IpcMainEvents>(channel: K, listener: IpcMainEventListener<K>) {\n    const oldListener = this.eventListeners[channel];\n\n    if (oldListener) {\n      console.error(`Removing duplicate event listener for ${ channel }`);\n      ipcMain.removeListener(channel, oldListener);\n    }\n    this.eventListeners[channel] = listener as any;\n    ipcMain.on(channel, listener);\n  }\n\n  /**\n   * Attach a handler to ipcMainInvokeEvents that will be torn down when this\n   * extension manager shuts down.\n   * @note Only one handler per topic is supported.\n   */\n  protected setMainHandler<K extends keyof IpcMainInvokeEvents>(channel: K, handler: IpcMainEventHandler<K>) {\n    const oldHandler = this.eventHandlers[channel];\n\n    if (oldHandler) {\n      console.error(`Removing duplicate event handler for ${ channel }`);\n      ipcMain.removeHandler(channel);\n    }\n    this.eventHandlers[channel] = handler as any;\n    ipcMain.handle(channel, handler);\n  }\n\n  /**\n   * Processes from extensions created in spawnStreaming() that may still be\n   * running.\n   */\n  protected processes: Record<string, WeakRef<ReadableChildProcess>> = {};\n\n  async init(config: RecursiveReadonly<Settings>) {\n    if (process.platform !== 'win32' && !mainProcessWatcherInitialized) {\n      // If we're not running on Windows, spawn a process that waits for this\n      // process to exit and then kill all processes in this process group.\n      // We don't do this on Windows as we have a different (job-based)\n      // implementation there that will trigger automatically when we exit,\n      // based on `wsl-helper process spawn`.\n      const proc = spawn(\n        executable('rdctl'),\n        ['internal', 'process', 'wait-kill', `--pid=${ process.pid }`],\n        { stdio: ['ignore', await console.fdStream, await console.fdStream], detached: true });\n\n      proc.unref();\n      mainProcessWatcherInitialized = true;\n    }\n\n    // Handle events from the renderer process.\n    this.setMainListener('extensions/open-external', (...[, url]) => {\n      Electron.shell.openExternal(url);\n    });\n\n    this.setMainListener('extensions/spawn/kill', (event, execId) => {\n      const extensionId = this.getExtensionIdFromEvent(event);\n      const fullExecId = `${ extensionId }:${ execId }`;\n      const process = this.processes[fullExecId]?.deref();\n\n      process?.kill('SIGTERM');\n      console.debug(`Killed ${ fullExecId }: ${ process }`);\n    });\n\n    this.setMainListener('extensions/spawn/streaming', async(event, options) => {\n      switch (options.scope) {\n      case 'host':\n        return this.execStreaming(event, options, await this.spawnHost(event, options));\n      case 'docker-cli':\n        return this.execStreaming(event, options, await this.spawnDockerCli(event, options));\n      case 'container':\n        return this.execStreaming(event, options, await this.spawnContainer(event, options));\n      default:\n        console.error(`Unexpected scope ${ options.scope }`);\n        throw new Error(`Unexpected scope ${ options.scope }`);\n      }\n    });\n\n    this.setMainHandler('extensions/spawn/blocking', async(event, options) => {\n      switch (options.scope) {\n      case 'host':\n        return this.execBlocking(await this.spawnHost(event, options));\n      case 'docker-cli':\n        return this.execBlocking(await this.spawnDockerCli(event, options));\n      case 'container':\n        return this.execBlocking(await this.spawnContainer(event, options));\n      default:\n        console.error(`Unexpected scope ${ options.scope }`);\n        throw new Error(`Unexpected scope ${ options.scope }`);\n      }\n    });\n\n    this.setMainHandler('extensions/ui/show-open', (event, options) => {\n      const window = Electron.BrowserWindow.fromWebContents(event.sender);\n\n      if (window) {\n        return Electron.dialog.showOpenDialog(window, options);\n      }\n\n      return Electron.dialog.showOpenDialog(options);\n    });\n\n    this.setMainListener('extensions/ui/toast', (event, level, message) => {\n      const title = {\n        success: 'Success',\n        warning: 'Warning',\n        error:   'Error',\n      }[level];\n      const urgency = ({\n        success: 'low',\n        warning: 'normal',\n        error:   'critical',\n      } as const)[level];\n\n      const notification = new Electron.Notification({\n        title,\n        body: message,\n        urgency,\n      });\n\n      notification.show();\n    });\n\n    this.setMainHandler('extensions/vm/http-fetch', async(event, config) => {\n      const extensionId = this.getExtensionIdFromEvent(event);\n\n      if (!extensionId) {\n        // Sender frame has gone away, no need to fetch anymore.\n        return {\n          statusCode: -1, name: 'Request aborted', message: 'Request aborted',\n        };\n      }\n      if (extensionId === EXTENSION_APP) {\n        throw new Error('HTTP fetch from main app not implemented yet');\n      }\n\n      const extension = await this.getExtension(extensionId) as ExtensionImpl;\n      let url: URL;\n\n      if (/^[^:/]*:/.test(config.url)) {\n        // the URL is absolute, use as-is.\n        url = new URL(config.url);\n      } else {\n        // given a relative URL, we need to figure out how to connect to the backend.\n        const port = await extension.getBackendPort();\n\n        if (!port) {\n          return Promise.reject(new Error('Could not find backend port'));\n        }\n        url = new URL(config.url, `http://127.0.0.1:${ port }`);\n      }\n\n      if (!url.hostname) {\n        console.error(`Fetching from extension backend service not implemented yet (${ extensionId } fetching ${ url })`);\n\n        return Promise.reject(new Error('Could not fetch from backend'));\n      }\n\n      const options: RequestInit = {\n        method:  config.method,\n        headers: config.headers ?? {},\n        body:    config.data,\n      };\n      const response = await net.fetch(url.toString(), options);\n\n      return {\n        statusCode: response.status, name: http.STATUS_CODES[response.status] ?? 'Unknown', message: await response.text(),\n      };\n    });\n\n    // Import image for port forwarding\n    await this.client.runClient(\n      ['image', 'load', '--input', path.join(paths.resources, 'rdx-proxy.tar')],\n      console, { namespace: ExtensionImpl.extensionNamespace });\n\n    // Install / uninstall extensions as needed.\n    const tasks: Promise<any>[] = [];\n    const { enabled: allowEnabled, list: allowListRaw } = config.application.extensions.allowed;\n    const allowList = allowEnabled ? allowListRaw : undefined;\n\n    for (const [repo, tag] of Object.entries(config.application.extensions.installed)) {\n      if (!tag) {\n        // If the tag is unset / falsy, we wanted to uninstall the extension.\n        // There is no need to re-initialize it.\n        continue;\n      }\n\n      if (!this.isSupported(repo)) {\n        // If this extension is explicitly not supported, don't re-install it.\n        console.log(`Uninstalling unsupported extension ${ repo }:${ tag }`);\n        mainEvents.emit('settings-write', { application: { extensions: { installed: { [repo]: undefined } } } });\n        mainEvents.emit('extensions/ui/uninstall', repo);\n        continue;\n      }\n\n      tasks.push((async(repo: string, tag: string) => {\n        const id = `${ repo }:${ tag }`;\n\n        try {\n          return await (await this.getExtension(id)).install(allowList);\n        } catch (ex) {\n          console.error(`Failed to install extension ${ id }`, ex);\n          mainEvents.emit('settings-write', { application: { extensions: { installed: { [repo]: undefined } } } });\n        }\n      })(repo, tag));\n    }\n    await Promise.all(tasks);\n\n    // Register a listener to shut down extensions on quit\n    mainEvents.handle('extensions/shutdown', this.triggerExtensionShutdown);\n  }\n\n  /**\n   * Check if the given extension is supported.\n   * @note This is a temporary hack while we have a hard-coded list of\n   * extensions.\n   */\n  protected isSupported(repo: string): boolean {\n    if (!this.containerd) {\n      return true;\n    }\n\n    const desired = parseImageReference(repo);\n\n    if (!desired) {\n      return false;\n    }\n\n    if (!this.#supportedExtensions) {\n      const supported: Record<string, boolean> = {};\n\n      for (const item of MARKETPLACE_DATA) {\n        const slug = parseImageReference(item.slug);\n\n        if (!slug) {\n          continue;\n        }\n\n        supported[new URL(slug.name, slug.registry).toString()] = item.containerd_compatible;\n      }\n\n      this.#supportedExtensions = supported;\n    }\n\n    const ref = new URL(desired.name, desired.registry).toString();\n\n    return this.#supportedExtensions[ref] ?? true;\n  }\n\n  #supportedExtensions: Record<string, boolean> | undefined;\n\n  async getExtension(image: string, options: { preferInstalled?: boolean } = {}): Promise<Extension> {\n    let [, imageName, tag] = /^(.*):(.*?)$/.exec(image) ?? ['', image, undefined];\n\n    // The build process uses an older TypeScript that can't infer imageName correctly.\n    imageName ??= image;\n\n    this.extensions[imageName] ??= {};\n    const extGroup = this.extensions[imageName];\n    const preferInstalled = options?.preferInstalled ?? true;\n\n    if (tag) {\n      // Requested a specific tag; create it if we don't have it.\n      extGroup[tag] ||= new ExtensionImpl(imageName, tag, this.client);\n\n      return extGroup[tag];\n    }\n\n    // No tag specified; grab the installed version, if available\n    if (preferInstalled) {\n      for (const ext of Object.values(extGroup)) {\n        if (await ext.isInstalled()) {\n          return ext;\n        }\n      }\n    }\n\n    // If we get here, no tag is specified and nothing is installed.\n    tag = await this.findBestVersion(imageName);\n    extGroup[tag] ||= new ExtensionImpl(imageName, tag, this.client);\n\n    return extGroup[tag];\n  }\n\n  /**\n   * Given an image name (without tag), calculate the best tag to use as an\n   * extension image.\n   */\n  protected async findBestVersion(imageName: string): Promise<string> {\n    const tags = await this.client.getTags(\n      imageName, { namespace: ExtensionImpl.extensionNamespace });\n    const tagArray = Array.from(tags);\n\n    console.debug(`Got tags: ${ JSON.stringify(tagArray) }`);\n\n    // Select the highest semver tag, if available.\n    // We try a couple ways to determine semver in the tag.\n    const vers: [semver.SemVer, string][] = [];\n\n    for (const converter of [\n      // semver.parse, possibly stripping \"v\" or \"v.\" prefix.\n      (tag: string) => semver.parse(tag.replace(/^v\\.?/i, '')),\n      // semver.coerce (grab the first digits in the string)\n      semver.coerce,\n    ]) {\n      vers.push(...tagArray.map(tag => [converter(tag), tag] as const)\n        .filter(([v]) => v) as [semver.SemVer, string][]);\n      if (vers.length > 0) {\n        break;\n      }\n    }\n\n    const newest = vers.sort(([l], [r]) => semver.compare(l, r)).pop()?.[1];\n\n    if (newest) {\n      return newest;\n    }\n\n    // Use the \"latest\" tag, if available.\n    if (tags.has('latest')) {\n      return 'latest';\n    }\n\n    // No relevant tags are available.\n    throw new ExtensionErrorImpl(\n      ExtensionErrorCode.FILE_NOT_FOUND,\n      `Could not detect relevant version for image \"${ imageName }\"`);\n  }\n\n  async getInstalledExtensions() {\n    // Get a list of all extensions, installed or not.\n    const exts = Object.values(this.extensions).flatMap(group => Object.values(group));\n    // Calculate if each is installed (in parallel).\n    const states = await Promise.all(exts.map(async ext => [ext, await ext.isInstalled()] as const));\n\n    // Return the extensions that are marked as installed.\n    return states.filter(([, state]) => state).map(([ext]) => ext);\n  }\n\n  /**\n   * Given an IpcMainEvent, return the extension ID associated with it.\n   */\n  protected getExtensionIdFromEvent(event: IpcMainEvent | IpcMainInvokeEvent): string | undefined {\n    const { senderFrame } = event;\n\n    if (!senderFrame) {\n      return;\n    }\n\n    const origin = new URL(senderFrame.origin);\n\n    return origin.protocol === 'app:' ? EXTENSION_APP : Buffer.from(origin.hostname, 'hex').toString();\n  }\n\n  /** Spawn a process in the host context. */\n  protected async spawnHost(event: IpcMainEvent | IpcMainInvokeEvent, options: SpawnOptions): Promise<ReadableChildProcess> {\n    const extensionId = this.getExtensionIdFromEvent(event);\n\n    if (!extensionId) {\n      throw new Error(`spawning process from a closed window`);\n    }\n    if (extensionId === EXTENSION_APP) {\n      throw new Error(`spawning a process from the main application is not implemented yet: ${ options.command.join(' ') }`);\n    }\n\n    const extension = await this.getExtension(extensionId) as ExtensionImpl;\n\n    if (!extension) {\n      throw new Error(`Could not find calling extension ${ extensionId }`);\n    }\n\n    const command = [...options.command];\n    const finalOptions: SpawnOptionsWithStdioTuple<'ignore', 'pipe', 'pipe'> = {\n      stdio: ['ignore', 'pipe', 'pipe'],\n      env:   {},\n      ..._.pick(options, ['cwd', 'env']),\n    };\n    const binDir = path.join(paths.resources, process.platform, 'bin');\n\n    finalOptions.env = _.merge({}, process.env, finalOptions.env);\n    finalOptions.env.PATH = finalOptions.env.PATH + path.delimiter + binDir;\n\n    command[0] = path.join(extension.dir, 'bin', command[0]);\n    if (process.platform === 'win32') {\n      // Use wsl-helper to launch the executable\n      command.unshift(executable('wsl-helper'), 'process', 'spawn', `--parent=${ process.pid }`, '--');\n    }\n\n    return spawn(command[0], command.slice(1), finalOptions);\n  }\n\n  /** Spawn a process in the docker-cli context. */\n  protected async spawnDockerCli(event: IpcMainEvent | IpcMainInvokeEvent, options: SpawnOptions): Promise<ReadableChildProcess> {\n    const extensionId = this.getExtensionIdFromEvent(event);\n\n    if (!extensionId) {\n      throw new Error(`Spawning docker client from closed sender frame`);\n    }\n    if (extensionId !== EXTENSION_APP) {\n      const extension = await this.getExtension(extensionId) as ExtensionImpl;\n\n      if (!extension) {\n        throw new Error(`Could not find calling extension ${ extensionId }`);\n      }\n    }\n\n    return this.client.runClient(\n      // For docker compatibility, strip quotes for any arguments.\n      options.command.map(arg => (/^([\"'])(.*)\\1$/.exec(arg) ?? ['', '', arg])[2]),\n      'stream',\n      _.pick(options, ['cwd', 'env', 'namespace']));\n  }\n\n  /** Spawn a process in the container context. */\n  protected async spawnContainer(event: IpcMainEvent | IpcMainInvokeEvent, options: SpawnOptions): Promise<ReadableChildProcess> {\n    const extensionId = this.getExtensionIdFromEvent(event);\n\n    if (!extensionId) {\n      throw new Error(`Spawning docker client from closed sender frame`);\n    }\n    if (extensionId === EXTENSION_APP) {\n      throw new Error(`Spawning a container command is not implemented for the main app: ${ options.command.join(' ') }`);\n    }\n    const extension = await this.getExtension(extensionId) as ExtensionImpl;\n\n    if (!extension) {\n      return Promise.reject(new Error(`Could not find calling extension ${ extensionId }`));\n    }\n\n    return extension.composeExec(options);\n  }\n\n  /**\n   * Execute a process on behalf of an extension, returning a promise that will\n   * be resolved when the process completes.\n   */\n  protected execBlocking(process: ReadableChildProcess): Promise<SpawnResult> {\n    const stdout: Buffer[] = [];\n    const stderr: Buffer[] = [];\n    let errored = false;\n\n    process.stdout.on('data', (data: string | Buffer) => {\n      stdout.push(typeof data === 'string' ? Buffer.from(data) : data);\n    });\n    process.stderr.on('data', (data: string | Buffer) => {\n      stderr.push(typeof data === 'string' ? Buffer.from(data) : data);\n    });\n\n    return new Promise((resolve, reject) => {\n      process.on('error', (error) => {\n        errored = true;\n        reject(error);\n      });\n\n      process.on('exit', (code, signal) => {\n        if (errored) {\n          return;\n        }\n        resolve({\n          cmd:    process.spawnargs.join(' '),\n          result: signal ?? code ?? 0,\n          stdout: Buffer.concat(stdout).toString('utf-8'),\n          stderr: Buffer.concat(stderr).toString('utf-8'),\n        });\n      });\n    });\n  }\n\n  /**\n   * Execute a process on behalf of an extension, with the output fed back to\n   * the extension via callbacks.\n   */\n  protected execStreaming(event: IpcMainEvent, options: SpawnOptions, process: ReadableChildProcess) {\n    const extensionId = this.getExtensionIdFromEvent(event);\n    const fullId = `${ extensionId }:${ options.execId }`;\n\n    let errored = false;\n\n    /***\n     * Helper for event.senderFrame.send() to add checking of channel names and\n     * process liveness.\n     */\n    const sendToFrame = <K extends keyof IpcRendererEvents>(channel: K, ...args: Parameters<IpcRendererEvents[K]>) => {\n      if (this.processes[fullId]?.deref()) {\n        event.senderFrame?.send?.(channel, ...args as any);\n      } else {\n        // If we get here, the process is only alive due to the closure, but the\n        // weak ref has been removed; this happens if the client has killed the\n        // process already.  In that case, just clean up and do not send anything\n        // to the client frame.\n        console.debug(`Sending ${ channel } to dead process ${ fullId }, force killing.`);\n        process.kill('SIGKILL');\n        // Close outputs to avoid buffered lines being sent after the process has been killed.\n        process.stdout.destroy();\n        process.stderr.destroy();\n      }\n    };\n\n    process.stdout.on('data', (stdout: string | Buffer) => {\n      sendToFrame('extensions/spawn/output', options.execId, { stdout: stdout.toString('utf-8') });\n    });\n    process.stderr.on('data', (stderr: string | Buffer) => {\n      sendToFrame('extensions/spawn/output', options.execId, { stderr: stderr.toString('utf-8') });\n    });\n    process.on('error', (error) => {\n      errored = true;\n      sendToFrame('extensions/spawn/error', options.execId, error);\n    });\n    process.on('exit', (code, signal) => {\n      if (errored) {\n        return;\n      }\n      if (code !== null ) {\n        sendToFrame('extensions/spawn/close', options.execId, code);\n      } else if (signal !== null) {\n        errored = true;\n        sendToFrame('extensions/spawn/error', options.execId, signal);\n      } else {\n        errored = true;\n        sendToFrame('extensions/spawn/error', options.execId, new Error('exited with neither code nor signal'));\n      }\n    });\n\n    this.processes[fullId] = new WeakRef(process);\n  }\n\n  shutdown() {\n    // Remove our event listeners (to avoid issues when we switch backends).\n    for (const untypedChannel in this.eventListeners) {\n      const channel = untypedChannel as keyof IpcMainEvents;\n      const listener = this.eventListeners[channel] as IpcMainEventListener<typeof channel>;\n\n      ipcMain.removeListener(channel, listener);\n    }\n\n    for (const untypedChannel in this.eventHandlers) {\n      ipcMain.removeHandler(untypedChannel as keyof IpcMainInvokeEvents);\n    }\n\n    for (const proc of Object.values(this.processes)) {\n      proc.deref()?.kill();\n    }\n\n    mainEvents.handle('extensions/shutdown', undefined);\n\n    return Promise.resolve();\n  }\n\n  triggerExtensionShutdown = async() => {\n    await Promise.all((await this.getInstalledExtensions()).map((extension) => {\n      return extension.shutdown();\n    }));\n  };\n}\n\nfunction getExtensionManager(): Promise<ExtensionManager | undefined> {\n  return Promise.resolve(manager);\n}\n\nexport async function initializeExtensionManager(client: ContainerEngineClient, cfg: RecursiveReadonly<Settings>): Promise<void> {\n  if (manager?.client === client) {\n    // The manager is already the correct one; do nothing.\n    return;\n  }\n\n  await manager?.shutdown();\n\n  console.debug(`Creating new extension manager...`);\n  manager = new ExtensionManagerImpl(client, cfg.containerEngine.name === ContainerEngine.CONTAINERD);\n\n  await manager.init(cfg);\n}\n\nexport default getExtensionManager;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/extensions/types.ts",
    "content": "/**\n * This file contains the main process side code to install extensions.\n * @see @pkg/extensions for the renderer process code.\n */\nimport type { ContainerEngineClient } from '@pkg/backend/containerClient';\nimport type { Settings } from '@pkg/config/settings';\nimport type { RecursiveReadonly } from '@pkg/utils/typeUtils';\n\ntype PlatformSpecific<T> = Record<'darwin' | 'windows' | 'linux', T>;\n\nexport interface ExtensionMetadata {\n  /** Icon for the extension, as a path in the image. */\n  icon: string;\n  /** UI endpoints. Currently only \"dashboard-tab\" is supported. */\n  ui?: {\n    'dashboard-tab'?: {\n      /** The title of the UI, as shown in the side bar. */\n      title:    string;\n      /** Root of the directory inside the image holding the UI files. */\n      root:     string;\n      /** The initial HTML page to load, relative to root. */\n      src:      string;\n      /** Information on the backend to expose. */\n      backend?: {\n        /** The name of the socket, as found in vm.exposes.socket */\n        socket: string;\n      }\n    }\n  }\n  /** Containers to run. */\n  vm?: ({ image: string } | { composefile: string }) & {\n    /** Things to expose to the UI */\n    exposes?: {\n      /** Path to a Unix socket to expose; this is in `/run/guest-services/`. */\n      socket: string;\n    }\n  };\n  host?: {\n    /** Files to copy to the host. */\n    binaries:          PlatformSpecific<{ path: string }[]>[],\n    /**\n     * Rancher Desktop extension: this will be run after the extension is\n     * installed (possibly as an upgrade).  This file should be listed in\n     * `binaries`.  Errors will be ignored.\n     */\n    'x-rd-install'?:   PlatformSpecific<string | string[]>,\n    /**\n     * Rancher Desktop extension: this will be run before the extension is\n     * uninstalled (possibly as an upgrade).  This file should be listed in\n     * `binaries`.  Errors will be ignored.\n     */\n    'x-rd-uninstall'?: PlatformSpecific<string | string[]>,\n    /**\n     * Rancher Desktop extension: this will be executed when the application\n     * quits.  The application may exit before the process completes.  It is not\n     * defined what the container engine / Kubernetes cluster may be doing at\n     * the time this is called.\n     */\n    'x-rd-shutdown'?:  PlatformSpecific<string | string[]>,\n  };\n}\n\n/**\n * A singular extension (identified by an image ID).\n * @note A reference of an extension does not imply that it is installed;\n * therefore, some operations may not be valid for uninstall extensions.\n */\nexport interface Extension {\n  /**\n   * The image ID for this extension, excluding the tag.\n   */\n  readonly id: string;\n\n  /**\n   * The image tag for this extension.\n   */\n  readonly version: string;\n\n  /**\n   * The full image tag for this image (a combination of id and version).\n   */\n  readonly image: string;\n\n  /**\n   * Metadata for this extension.\n   */\n  readonly metadata: Promise<ExtensionMetadata>;\n\n  /**\n   * Image labels associated with this extension.\n   */\n  readonly labels: Promise<Record<string, string>>;\n\n  /**\n   * Install this extension.\n   * @param allowedImages The list of extension images that are allowed to be\n   *        used; if all images are allowed, pass in undefined.\n   * @note If the extension is already installed, this is a no-op.\n   * @throws If the settings specify an allow list and this is not in it.\n   * @return Whether the extension was installed.\n   */\n  install(allowedImages: readonly string[] | undefined): Promise<boolean>;\n  /**\n   * Uninstall this extension.\n   * @note If the extension was not installed, this is a no-op.\n   * @returns Whether the extension was uninstalled.\n   */\n  uninstall(): Promise<boolean>;\n\n  /**\n   * Check whether this extension is installed (at this version).\n   */\n  isInstalled(): Promise<boolean>;\n\n  /**\n   * Extract the given file from the image.\n   * @param sourcePath The name of the file (or directory) to extract, relative\n   * to the root of the image; for example, `metadata.json`.\n   * @param destinationPath The directory to extract into.  If this does not\n   * exist and `sourcePath` is a file (rather than a directory), the contents\n   * are written directly to the named file (rather than treating it as a\n   * directory name).\n   */\n  extractFile(sourcePath: string, destinationPath: string): Promise<void>;\n}\n\nexport interface ExtensionManager {\n  readonly client: ContainerEngineClient;\n\n  init(config: RecursiveReadonly<Settings>): Promise<void>;\n\n  /**\n   * Get the given extension.\n   * @param image The image reference of the extension, possibly including the\n   *        tag.  If the tag is not supplied, the currently-installed version is\n   *        used (see options.preferInstalled); if no version is installed,\n   *        \"latest\" is assumed.\n   * @param [options.preferInstalled=true] If the given image reference does not\n   *        include tags and the extension is already installed, return the\n   *        currently installed version.\n   * @note This may cause the given image to be downloaded.\n   * @note The extension will not be automatically installed.\n   */\n  getExtension(image: string, options?: { preferInstalled?: boolean }): Promise<Extension>;\n\n  /**\n   * Get a collection of all installed extensions.\n   */\n  getInstalledExtensions(): Promise<Extension[]>;\n\n  /**\n   * Shut down the extension manager, doing any clean up necessary.\n   */\n  shutdown(): Promise<void>;\n}\n\nexport interface SpawnOptions {\n  /**\n   * The command to invoke, including arguments.  For some scopes, the\n   * executable may be fixed (and therefore this only contains arguments).\n   */\n  command: string[];\n  /**\n   * Identifier for the spawn event, scoped to the webContents frame.\n   */\n  execId:  string;\n  /**\n   * The scope where the execution will take place; this is determined by which\n   * API is being called.\n   */\n  scope:   'host' | 'docker-cli' | 'container';\n  /**\n   * Current working directory for the command.\n   */\n  cwd?:    string;\n  /**\n   * Override the process environment variables when running this command.\n   */\n  env?:    Record<string, string | undefined>;\n}\n\n/**\n * SpawnResult is the result of extension/spawn/blocking\n */\nexport interface SpawnResult {\n  /** The command executed. */\n  cmd:     string;\n  /** Whether the process was forcefully killed via the API. */\n  killed?: boolean;\n  /** The command exit code / signal. */\n  result:  NodeJS.Signals | number;\n  stdout:  string;\n  stderr:  string;\n}\n\nexport const ExtensionErrorMarker = Symbol('extension-error');\n\nexport enum ExtensionErrorCode {\n  INVALID_METADATA,\n  FILE_NOT_FOUND,\n  INSTALL_DENIED,\n}\n\nexport interface ExtensionError extends Error {\n  code:   ExtensionErrorCode;\n  cause?: unknown;\n}\n\nexport function isExtensionError(error: Error): error is ExtensionError {\n  return ExtensionErrorMarker in error;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/imageEvents.ts",
    "content": "/**\n * This module contains code for handling image-processor events (containerd/nerdctl, moby/docker).\n */\n\nimport path from 'path';\n\nimport Electron from 'electron';\n\nimport { ImageProcessor, ImageType } from '@pkg/backend/images/imageProcessor';\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport { isUnixError } from '@pkg/typings/unix.interface';\nimport Logging from '@pkg/utils/logging';\nimport * as window from '@pkg/window';\n\nconst console = Logging.images;\nconst ipcMainProxy = getIpcMainProxy(console);\n\n// Map image-related events to the associated image processor's methods\n// TODO: export the factory function to make this a singleton\n/**\n * The ImageEventHandler is a singleton.\n * It points to an active ImageProcessor, and relays relevant events to that processor.\n * Having image processors handle their own events is messy (see the notion of activating\n * an image processor), and shouldn't handle any of them.\n */\n\nexport class ImageEventHandler {\n  imageProcessor: ImageProcessor;\n  #lastBuildDirectory = '';\n  #mountCount = 0;\n\n  constructor(imageProcessor: ImageProcessor) {\n    this.imageProcessor = imageProcessor;\n    this.initEventHandlers();\n  }\n\n  protected onImagesChanged(images: ImageType[]) {\n    window.send('images-changed', images);\n  }\n\n  protected initEventHandlers() {\n    ipcMainProxy.handle('images-mounted', (_, mounted) => {\n      this.#mountCount += mounted ? 1 : -1;\n      if (this.#mountCount < 1) {\n        this.imageProcessor.removeListener('images-changed', this.onImagesChanged);\n      } else if (this.#mountCount === 1) {\n        this.imageProcessor.on('images-changed', this.onImagesChanged);\n      }\n\n      return this.imageProcessor.listImages();\n    });\n\n    ipcMainProxy.on('do-image-deletion', async(event, imageName, imageID) => {\n      try {\n        await this.imageProcessor.deleteImage(imageID);\n        await this.imageProcessor.refreshImages();\n        event.reply('images-process-ended', 0);\n      } catch (err) {\n        await Electron.dialog.showMessageBox({\n          message: `Error trying to delete image ${ imageName } (${ imageID }):\\n\\n ${ isUnixError(err) ? err.stderr : '' } `,\n          type:    'error',\n        });\n        event.reply('images-process-ended', 1);\n      }\n    });\n\n    ipcMainProxy.on('do-image-deletion-batch', async(event, imageIDs) => {\n      try {\n        const uniqueImageIDs = new Set<string>(imageIDs);\n\n        await this.imageProcessor.deleteImages([...uniqueImageIDs]);\n        await this.imageProcessor.refreshImages();\n        event.reply('images-process-ended', 0);\n      } catch (err) {\n        await Electron.dialog.showMessageBox({\n          message: `Error trying to delete images ${ imageIDs }`,\n          type:    'error',\n        });\n        event.reply('images-process-ended', 1);\n      }\n    });\n\n    ipcMainProxy.on('do-image-build', async(event, taggedImageName) => {\n      const options: any = {\n        title:      'Pick the build directory',\n        properties: ['openFile'],\n        message:    'Please select the Dockerfile to use (could have a different name)',\n      };\n\n      if (this.#lastBuildDirectory) {\n        options.defaultPath = this.#lastBuildDirectory;\n      }\n      const results = Electron.dialog.showOpenDialogSync(options);\n\n      if (results === undefined) {\n        event.reply('images-process-cancelled');\n\n        return;\n      }\n      if (results.length !== 1) {\n        console.log(`Expecting exactly one result, got ${ results.join(', ') }`);\n        event.reply('images-process-cancelled');\n\n        return;\n      }\n      const pathParts = path.parse(results[0]);\n      let code;\n\n      this.#lastBuildDirectory = pathParts.dir;\n      try {\n        code = (await this.imageProcessor.buildImage(this.#lastBuildDirectory, pathParts.base, taggedImageName)).code;\n        await this.imageProcessor.refreshImages();\n      } catch (err) {\n        if (isUnixError(err)) {\n          code = err.code;\n        }\n      }\n      event.reply('images-process-ended', code);\n    });\n\n    ipcMainProxy.on('do-image-pull', async(event, imageName) => {\n      let taggedImageName = imageName;\n      let code;\n\n      if (!imageName.includes(':')) {\n        taggedImageName += ':latest';\n      }\n      try {\n        code = (await this.imageProcessor.pullImage(taggedImageName)).code;\n        await this.imageProcessor.refreshImages();\n      } catch (err) {\n        if (isUnixError(err)) {\n          code = err.code;\n        }\n      }\n      event.reply('images-process-ended', code);\n    });\n\n    ipcMainProxy.on('do-image-scan', async(event, imageName, namespace) => {\n      let taggedImageName = imageName;\n      let code;\n\n      // The containerd scanner only supports image names that include the registry name\n      if (!taggedImageName.includes('/')) {\n        taggedImageName = `library/${ imageName }`;\n      }\n      if (!taggedImageName.split('/')[0].includes('.')) {\n        taggedImageName = `docker.io/${ taggedImageName }`;\n      }\n      if (!taggedImageName.includes(':')) {\n        taggedImageName += ':latest';\n      }\n\n      try {\n        code = (await this.imageProcessor.scanImage(taggedImageName, namespace)).code;\n        await this.imageProcessor.refreshImages();\n      } catch (err) {\n        console.error(`Failed to scan image ${ imageName }: `, err);\n        if (isUnixError(err)) {\n          code = err.code;\n        }\n        Electron.dialog.showMessageBox({\n          message: `Error trying to scan ${ taggedImageName }:\\n\\n ${ isUnixError(err) ? err.stderr : '' } `,\n          type:    'error',\n        }).catch((err) => {\n          console.log('messageBox failure: ', err);\n        });\n      }\n      event.reply('images-process-ended', code);\n    });\n\n    ipcMainProxy.on('do-image-push', async(event, imageName, imageID, tag) => {\n      const taggedImageName = `${ imageName }:${ tag }`;\n      let code;\n\n      try {\n        code = (await this.imageProcessor.pushImage(taggedImageName)).code;\n      } catch (err) {\n        if (isUnixError(err)) {\n          code = err.code;\n        }\n        Electron.dialog.showMessageBox({\n          message: `Error trying to push ${ taggedImageName }:\\n\\n ${ isUnixError(err) ? err.stderr : '' } `,\n          type:    'error',\n        }).catch((err) => {\n          console.log('messageBox failure: ', err);\n        });\n      }\n      event.reply('images-process-ended', code);\n    });\n\n    ipcMainProxy.handle('images-check-state', () => {\n      return this.imageProcessor.isReady;\n    });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/ipcMain.ts",
    "content": "import Electron from 'electron';\n\nimport type { IpcMainEvents, IpcMainInvokeEvents } from '@pkg/typings/electron-ipc';\nimport { Log } from '@pkg/utils/logging';\n\n// Intended to be passed to the replacer parameter in a JSON.stringify\n// call. Should rectify any circular references that the object you are\n// stringifying may have.\nfunction removeCircularReferences(property: string | symbol, value: any): any {\n  if (property === '_idlePrev') {\n    return undefined;\n  }\n\n  return value;\n}\n\nexport function makeArgsPrintable(args: any[]): string[] {\n  const maxPrintableArgLength = 500;\n  const printableArgs = args.map((arg) => {\n    let printableArg = JSON.stringify(arg, removeCircularReferences) ?? 'undefined';\n\n    if (printableArg.length > maxPrintableArgLength) {\n      printableArg = printableArg.slice(0, maxPrintableArgLength);\n      printableArg += '...';\n    }\n\n    return printableArg;\n  });\n\n  return printableArgs;\n}\n\ninterface IpcMainProxy {\n  on<eventName extends keyof IpcMainEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcMainEvent, ...args: globalThis.Parameters<IpcMainEvents[eventName]>) => void\n  ): this;\n  once<eventName extends keyof IpcMainEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcMainEvent, ...args: globalThis.Parameters<IpcMainEvents[eventName]>) => void\n  ): this;\n  removeListener<eventName extends keyof IpcMainEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcMainEvent, ...args: globalThis.Parameters<IpcMainEvents[eventName]>) => void\n  ): this;\n  removeAllListeners<eventName extends keyof IpcMainEvents>(channel?: eventName): this;\n\n  handle<eventName extends keyof IpcMainInvokeEvents>(\n    channel: eventName,\n    listener: (\n      event: Electron.IpcMainInvokeEvent,\n      ...args: globalThis.Parameters<IpcMainInvokeEvents[eventName]>\n    ) => Promise<ReturnType<IpcMainInvokeEvents[eventName]>> | ReturnType<IpcMainInvokeEvents[eventName]>\n  ): void;\n  handleOnce<eventName extends keyof IpcMainInvokeEvents>(\n    channel: eventName,\n    listener: (\n      event: Electron.IpcMainInvokeEvent,\n      ...args: globalThis.Parameters<IpcMainInvokeEvents[eventName]>\n    ) => Promise<ReturnType<IpcMainInvokeEvents[eventName]>> | ReturnType<IpcMainInvokeEvents[eventName]>\n  ): void;\n  removeHandler<eventName extends keyof IpcMainInvokeEvents>(channel: eventName): void;\n}\n\ntype Listener = (event: Electron.IpcMainEvent, ...args: any) => void;\ntype Handler = (event: Electron.IpcMainInvokeEvent, ...args: any) => Promise<unknown>;\n\nclass IpcMainProxyImpl implements IpcMainProxy {\n  constructor(logger: Log, ipcMain?: Electron.IpcMain) {\n    this.logger = logger;\n    this.ipcMain = ipcMain ?? Electron.ipcMain;\n  }\n\n  protected logger:  Log;\n  protected ipcMain: Electron.IpcMain;\n\n  // Bijective weak maps between the user-provided listener and the wrapper that\n  // introduces logging.  We do not keep strong references to either; the user-\n  // provided listener is only kept alive by the wrapper, which the underlying\n  // IpcMain has a strong reference to.\n  protected listenerWrapperToRaw = new WeakMap<Listener, WeakRef<Listener>>();\n  protected listenerRawToWrapper = new WeakMap<Listener, WeakRef<Listener>>();\n\n  on(channel: string, listener: Listener): this {\n    const wrapper: Listener = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" triggered with arguments: ${ printableArgs.join(', ') }`);\n      listener(event, ...args);\n    };\n\n    this.listenerWrapperToRaw.set(wrapper, new WeakRef(listener));\n    this.listenerRawToWrapper.set(listener, new WeakRef(wrapper));\n    this.ipcMain.on(channel, wrapper);\n\n    return this;\n  }\n\n  addListener(channel: string, listener: Listener): this {\n    return this.on(channel, listener);\n  }\n\n  prependListener(channel: string, listener: Listener): this {\n    const wrapper: Listener = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" triggered with arguments: ${ printableArgs.join(', ') }`);\n      listener(event, ...args);\n    };\n\n    this.listenerWrapperToRaw.set(wrapper, new WeakRef(listener));\n    this.listenerRawToWrapper.set(listener, new WeakRef(wrapper));\n    this.ipcMain.prependListener(channel, wrapper);\n\n    return this;\n  }\n\n  once(channel: string, listener: Listener): this {\n    const wrapper: Listener = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" triggered with arguments: ${ printableArgs.join(', ') }`);\n      listener(event, ...args);\n    };\n\n    this.listenerWrapperToRaw.set(wrapper, new WeakRef(listener));\n    this.listenerRawToWrapper.set(listener, new WeakRef(wrapper));\n    this.ipcMain.once(channel, wrapper);\n\n    return this;\n  }\n\n  prependOnceListener(channel: string, listener: Listener): this {\n    const wrapper: Listener = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" triggered with arguments: ${ printableArgs.join(', ') }`);\n      listener(event, ...args);\n    };\n\n    this.listenerWrapperToRaw.set(wrapper, new WeakRef(listener));\n    this.listenerRawToWrapper.set(listener, new WeakRef(wrapper));\n    this.ipcMain.prependOnceListener(channel, wrapper);\n\n    return this;\n  }\n\n  removeListener(channel: string, listener: Listener): this {\n    const wrapper = this.listenerRawToWrapper.get(listener)?.deref();\n\n    if (wrapper) {\n      this.ipcMain.removeListener(channel, wrapper);\n      this.listenerWrapperToRaw.delete(wrapper);\n    }\n    this.listenerRawToWrapper.delete(listener);\n\n    return this;\n  }\n\n  off(channel: string, listener: Listener): this {\n    return this.removeListener(channel, listener);\n  }\n\n  removeAllListeners(channel?: string): this {\n    this.ipcMain.removeAllListeners(channel);\n\n    return this;\n  }\n\n  setMaxListeners(n: number): this {\n    this.ipcMain.setMaxListeners(n);\n\n    return this;\n  }\n\n  getMaxListeners(): number {\n    return this.ipcMain.getMaxListeners();\n  }\n\n  listeners(eventName: string | symbol) {\n    return this.ipcMain.listeners(eventName);\n  }\n\n  rawListeners(eventName: string | symbol) {\n    return this.ipcMain.rawListeners(eventName);\n  }\n\n  listenerCount(eventName: string | symbol, listener?: Listener): number {\n    return this.ipcMain.listenerCount(eventName, listener);\n  }\n\n  emit(eventName: string | symbol, ...args: any[]): boolean {\n    return this.ipcMain.emit(eventName, ...args);\n  }\n\n  eventNames(): (string | symbol)[] {\n    return this.ipcMain.eventNames();\n  }\n\n  // For dealing with handlers, we don't need to keep track of the wrappers\n  // (because removeHandler() doesn't actually take the handler to remove).\n\n  handle(channel: string, handler: Handler): void {\n    const wrapper: Handler = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" handle called with: ${ printableArgs.join(', ') }`);\n\n      return handler(event, ...args);\n    };\n\n    this.ipcMain.handle(channel, wrapper);\n  }\n\n  handleOnce(channel: string, handler: Handler) {\n    const wrapper: Handler = (event, ...args) => {\n      const printableArgs = makeArgsPrintable(args);\n\n      this.logger.debug(`ipcMain: \"${ channel }\" handle called with: ${ printableArgs.join(', ') }`);\n\n      return handler(event, ...args);\n    };\n\n    this.ipcMain.handleOnce(channel, wrapper);\n  }\n\n  removeHandler(channel: string): void {\n    this.ipcMain.removeHandler(channel);\n  }\n}\n\nexport function getIpcMainProxy(logger: Log, ipcMain?: Electron.IpcMain): IpcMainProxy {\n  return new IpcMainProxyImpl(logger, ipcMain);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/mainEvents.ts",
    "content": "/**\n * This module is an EventEmitter for communication between various parts of the\n * main process.\n */\n\nimport { EventEmitter } from 'events';\n\nimport type { VMBackend } from '@pkg/backend/backend';\nimport type { Settings } from '@pkg/config/settings';\nimport type { TransientSettings } from '@pkg/config/transientSettings';\nimport { DiagnosticsCheckerResult } from '@pkg/main/diagnostics/types';\nimport { RecursivePartial, RecursiveReadonly } from '@pkg/utils/typeUtils';\n\nexport class NoMainEventsHandlerError extends Error {\n  constructor(eventName: string) {\n    super(`No handlers registered for mainEvents::${ eventName }`);\n  }\n}\n\n/**\n * MainEventNames describes the events available over the MainEvents event\n * emitter.  All normal events are described as methods returning void, with\n * the parameters of the event being the data that is send (and received).\n * For asynchronous RPC, we use a non-void return type; they can be used via\n * mainEvents.handle() and mainEvents.invoke(); see the description of those\n * methods for details.\n */\ninterface MainEventNames {\n  /**\n   * Emitted when the Kubernetes backend state has changed.\n   */\n  'k8s-check-state'(mgr: VMBackend): void;\n\n  /**\n   * Fetch the currently stored settings.\n   * @note This may not match the currently active settings.\n   */\n  'settings-fetch'(): Settings;\n\n  /**\n   * Emitted when the settings have been changed.\n   *\n   * @param settings The new settings.\n   */\n  'settings-update'(settings: Settings): void;\n\n  /**\n   * Emitted to request that the settings be changed.\n   *\n   * @param settings The settings to change.\n   */\n  'settings-write'(settings: RecursivePartial<RecursiveReadonly<Settings>>): void;\n\n  /**\n   * Read the current transient settings.\n   */\n  'transient-settings-fetch'(): TransientSettings;\n\n  /**\n    * Emitted to update current transient settings.\n    *\n    * @param transientSettings The new transient settings.\n    */\n  'transient-settings-update'(transientSettings: RecursivePartial<TransientSettings>): void;\n\n  /**\n   * Emitted as a request to get the CA certificates.\n   */\n  'cert-get-ca-certificates'(): void;\n\n  /**\n   * Emitted as a reply to 'cert-get-ca-certificates'.\n   *\n   * @param certs The certificates found.\n   */\n  'cert-ca-certificates'(certs: (string | Buffer)[]): void;\n\n  /**\n   * Emitted after the network setup is complete.\n   */\n  'network-ready'(): void;\n\n  /**\n   * Emitted when the network comes online or goes offline.\n   * @param connected The new network state.\n   */\n  'update-network-status'(connected: boolean): void;\n\n  /**\n   * Emitted when the integration state has changed.\n   *\n   * @param state A mapping of WSL distributions to the current state, or a\n   * string if there is an error.\n   */\n  'integration-update'(state: Record<string, boolean | string>): void;\n\n  /**\n   * Fetch the API credentials that can be used for HTTP basic auth on localhost\n   * to talk to the backend.\n   *\n   * @note These credentials are meant for the UI; using them may require user\n   * interaction.\n   */\n  'api-get-credentials'(): { user: string, password: string, port: number };\n\n  /**\n   * Force trigger diagnostics with the given id.\n   * This is used when something has changed that might affect whether the given\n   * diagnostic needs to be re-run.\n   * @note This does not update the last run time (since it only runs a single\n   * checker).\n   */\n  'diagnostics-trigger'(id: string): DiagnosticsCheckerResult | DiagnosticsCheckerResult[] | undefined;\n\n  /**\n   * Generically signify that a diagnostic should be updated.\n   * @param id The diagnostic identifier.\n   * @param state The new state for the diagnostic.\n   */\n  'diagnostics-event'(payload: DiagnosticsEventPayload): void;\n\n  /**\n   * Emitted when an extension is uninstalled via the extension manager.\n   * @param id The ID of the extension that was uninstalled.\n   */\n  'extensions/ui/uninstall'(id: string): void;\n\n  /**\n   * Emitted on application quit; this is used to shut down extensions.\n   */\n  'extensions/shutdown'(): Promise<void>;\n\n  /**\n   * Register the extension protocol handler in the given webContents partition.\n   * @param partition The partition name; likely \"persist:rdx-...\"\n   */\n  'extensions/register-protocol'(partition: string): Promise<void>;\n\n  /**\n   * Emitted on application quit, used to shut down any integrations.  This\n   * requires feedback from the handler to know when all tasks are complete.\n   */\n  'shutdown-integrations'(): Promise<void>;\n\n  /**\n   * Emitted on application quit.  Note that at this point we're committed to\n   * quitting.\n   */\n  'quit'(): void;\n\n  /**\n   * Emitted when the state of the backend lock changes. An empty string indicates\n   * a locked state, and a nonempty string indicates a locked state and serves as\n   * an explanation as to why Rancher Desktop is in this state. It disables the UI,\n   * prevents the user from making changes to settings, and possibly prevents other\n   * actions that could cause problems with snapshot operations (as of the time of\n   * writing snapshots is the sole use for this).\n   */\n  'backend-locked-update'(backendIsLocked: string, action?: string): void;\n\n  /**\n   * Emitted when a component wants to check the state of the backend lock.\n   * Responds by emitting a backend-locked-update.\n   */\n  'backend-locked-check'(): void;\n\n  'dialog-info'(args: Record<string, string>): void;\n}\n\n/**\n * DiagnosticsEventPayload defines the data that will be passed on a\n * 'diagnostics-event' event.\n */\ntype DiagnosticsEventPayload =\n  { id: 'integrations-windows', distro?: string, key: string, error?: Error } |\n  { id: 'kube-versions-available', available: boolean } |\n  { id: 'moby-storage', hasClassicData: boolean, hasSnapshotterData: boolean, useSnapshotter: boolean } |\n  { id: 'network-connectivity', connected: boolean } |\n  { id: 'path-management', fileName: string; error: Error | undefined };\n\n/**\n * Helper type definition to check if the given event name is a handler (i.e.\n * has a return value) instead of an event (i.e. returns void).\n */\ntype IsHandler<eventName extends keyof MainEventNames> =\n  // We check if void extends the return type; if the return type is also void,\n  // then this check succeeds (they're equal); otherwise, it fails.\n  void extends ReturnType<MainEventNames[eventName]> ? false : true;\n\n/**\n * Parameter types for mainEvents.invoke(eventName, ...params)\n * Given the definition above, these only apply to methods on MainEventNames\n * that do not return void.\n */\ntype HandlerParams<eventName extends keyof MainEventNames> =\n  IsHandler<eventName> extends true\n    ? Parameters<MainEventNames[eventName]>\n    : never;\n\n/**\n * The return type for mainEvents.invoke(eventName, ...), without the Promise<>\n * wrapper.  Given the definition above, these only apply to methods on\n * MainEventNames that do not return void.\n */\ntype HandlerReturn<eventName extends keyof MainEventNames> =\n  IsHandler<eventName> extends true\n    ? Awaited<ReturnType<MainEventNames[eventName]>>\n    : never;\n\n/**\n * The complete type for a handler, combining both the parameters and the\n * return type.\n */\ntype HandlerType<eventName extends keyof MainEventNames> =\n  IsHandler<eventName> extends true\n    ? (...args: HandlerParams<eventName>) => Promise<HandlerReturn<eventName>>\n    : never;\n\nexport interface MainEvents extends EventEmitter {\n  emit<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    ...args: Parameters<MainEventNames[eventName]>\n  ): boolean;\n\n  on<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n  once<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n  off<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n\n  /**\n   * Invoke a handler that returns a promise of a result.\n   */\n  invoke<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    ...args: HandlerParams<eventName>): Promise<HandlerReturn<eventName>>;\n\n  /**\n   * Invoke a handler that returns a promise of a result.  Unlike `invoke`, this\n   * does not raise an exception if the event handler is not registered.\n   */\n  tryInvoke<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    ...args: HandlerParams<eventName>): Promise<HandlerReturn<eventName> | undefined>;\n\n  /**\n   * Register a handler that will handle invoke() callers.  If the given handler\n   * is `undefined`, unregister it instead.\n   */\n  handle<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    handler: HandlerType<eventName> | undefined,\n  ): void;\n}\n\nclass MainEventsImpl extends EventEmitter implements MainEvents {\n  handlers: {\n    [eventName in keyof MainEventNames]?: IsHandler<eventName> extends true ? HandlerType<eventName> : never;\n  } = {};\n\n  emit<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    ...args: Parameters<MainEventNames[eventName]>\n  ): boolean;\n\n  emit(eventName: string, ...args: any[]): boolean {\n    return super.emit(eventName, ...args);\n  }\n\n  on<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n\n  on(eventName: string, listener: (...args: any[]) => void) {\n    return super.on(eventName, listener);\n  }\n\n  once<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n\n  once(eventName: string, listener: (...args: any[]) => void) {\n    return super.once(eventName, listener);\n  }\n\n  off<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends false ? eventName : never,\n    listener: (...args: Parameters<MainEventNames[eventName]>) => void\n  ): this;\n\n  off(eventName: string, listener: (...args: any[]) => void) {\n    return super.off(eventName, listener);\n  }\n\n  async invoke<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    ...args: HandlerParams<eventName>\n  ): Promise<HandlerReturn<eventName>> {\n    const handler: HandlerType<eventName> | undefined = this.handlers[event] as any;\n\n    if (handler) {\n      return await handler(...args);\n    }\n    throw new NoMainEventsHandlerError(event);\n  }\n\n  async tryInvoke<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    ...args: HandlerParams<eventName>\n  ): Promise<HandlerReturn<eventName> | undefined> {\n    const handler: HandlerType<eventName> | undefined = this.handlers[event];\n\n    return await handler?.(...args);\n  }\n\n  handle<eventName extends keyof MainEventNames>(\n    event: IsHandler<eventName> extends true ? eventName : never,\n    handler: HandlerType<eventName> | undefined,\n  ): void {\n    if (handler) {\n      this.handlers[event] = handler as any;\n    } else {\n      delete this.handlers[event];\n    }\n  }\n}\nconst mainEvents: MainEvents = new MainEventsImpl();\n\nexport default mainEvents;\n"
  },
  {
    "path": "pkg/rancher-desktop/main/mainmenu.ts",
    "content": "import Electron, { Menu, MenuItem, MenuItemConstructorOptions, shell } from 'electron';\n\nimport { getVersion, parseDocsVersion } from '@pkg/utils/version';\nimport { openPreferences } from '@pkg/window/preferences';\n\nconst baseUrl = `https://docs.rancherdesktop.io`;\n\nasync function versionedDocsUrl() {\n  const version = await getVersion();\n  const parsed = parseDocsVersion(version);\n\n  return `${ baseUrl }/${ parsed }`;\n}\n\nexport default function buildApplicationMenu(): void {\n  const menuItems: MenuItem[] = getApplicationMenu();\n  const menu = Menu.buildFromTemplate(menuItems);\n\n  Menu.setApplicationMenu(menu);\n}\n\nfunction getApplicationMenu(): MenuItem[] {\n  switch (process.platform) {\n  case 'darwin':\n    return getMacApplicationMenu();\n  case 'linux':\n    return getWindowsApplicationMenu();\n  case 'win32':\n    return getWindowsApplicationMenu();\n  default:\n    throw new Error(`Unsupported platform: ${ process.platform }`);\n  }\n}\n\nfunction getEditMenu(isMac: boolean): MenuItem {\n  return new MenuItem({\n    label:   '&Edit',\n    submenu: [\n      { role: 'undo', label: '&Undo' },\n      { role: 'redo', label: '&Redo' },\n      { type: 'separator' },\n      { role: 'cut', label: 'Cu&t' },\n      { role: 'copy', label: '&Copy' },\n      { role: 'paste', label: '&Paste' },\n      { role: 'delete', label: 'De&lete' },\n      ...(!isMac ? [{ type: 'separator' } as MenuItemConstructorOptions] : []),\n      { role: 'selectAll', label: 'Select &All' },\n    ],\n  });\n}\n\nfunction getViewMenu(): MenuItem {\n  return new MenuItem({\n    label:   '&View',\n    submenu: [\n      ...(Electron.app.isPackaged\n        ? []\n        : [\n          { role: 'reload', label: '&Reload' },\n          { role: 'forceReload', label: '&Force Reload' },\n          { role: 'toggleDevTools', label: 'Toggle &Developer Tools' },\n          { type: 'separator' },\n        ] as const),\n      {\n        label:       '&Actual Size',\n        accelerator: 'CmdOrCtrl+0',\n        click(_item, focusedWindow) {\n          adjustZoomLevel(focusedWindow, 0);\n        },\n      },\n      {\n        label:       'Zoom &In',\n        accelerator: 'CmdOrCtrl+Plus',\n        click(_item, focusedWindow) {\n          adjustZoomLevel(focusedWindow, 0.5);\n        },\n      },\n      {\n        label:       'Zoom &Out',\n        accelerator: 'CmdOrCtrl+-',\n        click(_item, focusedWindow) {\n          adjustZoomLevel(focusedWindow, -0.5);\n        },\n      },\n      { type: 'separator' },\n      { role: 'togglefullscreen', label: 'Toggle Full &Screen' },\n    ],\n  });\n}\n\nfunction getHelpMenu(isMac: boolean): MenuItem {\n  const helpMenuItems: MenuItemConstructorOptions[] = [\n    ...(!isMac\n      ? [\n        {\n          role:  'about',\n          label: `&About ${ Electron.app.name }`,\n          click() {\n            Electron.app.showAboutPanel();\n          },\n        } as MenuItemConstructorOptions,\n        { type: 'separator' } as MenuItemConstructorOptions,\n      ]\n      : []),\n    {\n      label: isMac ? 'Rancher Desktop &Help' : 'Get &Help',\n      click: async() => {\n        shell.openExternal(await versionedDocsUrl());\n      },\n    },\n    {\n      label: 'File a &Bug',\n      click() {\n        shell.openExternal('https://github.com/rancher-sandbox/rancher-desktop/issues');\n      },\n    },\n    {\n      label: '&Project Page',\n      click() {\n        shell.openExternal('https://rancherdesktop.io/');\n      },\n    },\n    {\n      label: '&Discuss',\n      click() {\n        shell.openExternal('https://slack.rancher.io/');\n      },\n    },\n  ];\n\n  return new MenuItem({\n    role:    'help',\n    label:   '&Help',\n    submenu: helpMenuItems,\n  });\n}\n\nfunction getMacApplicationMenu(): MenuItem[] {\n  return [\n    new MenuItem({\n      label:   Electron.app.name,\n      submenu: [\n        { role: 'about' },\n        { type: 'separator' },\n        ...getPreferencesMenuItem(),\n        { role: 'services' },\n        { type: 'separator' },\n        { role: 'hide' },\n        { role: 'hideOthers' },\n        { role: 'unhide' },\n        { type: 'separator' },\n        { role: 'quit' },\n      ],\n    }),\n    new MenuItem({\n      label: 'File',\n      role:  'fileMenu',\n    }),\n    getEditMenu(true),\n    getViewMenu(),\n    new MenuItem({\n      label: '&Window',\n      role:  'windowMenu',\n    }),\n    getHelpMenu(true),\n  ];\n}\n\nfunction getWindowsApplicationMenu(): MenuItem[] {\n  return [\n    new MenuItem({\n      label:   '&File',\n      role:    'fileMenu',\n      submenu: [\n        ...getPreferencesMenuItem(),\n        {\n          role:  'quit',\n          label: 'E&xit',\n        },\n      ],\n    }),\n    getEditMenu(false),\n    getViewMenu(),\n    getHelpMenu(false),\n  ];\n}\n\n/**\n * Gets the preferences menu item for all supported platforms\n * @returns MenuItemConstructorOptions: The preferences menu item object\n */\nfunction getPreferencesMenuItem(): MenuItemConstructorOptions[] {\n  return [\n    {\n      label:               'Preferences',\n      visible:             true,\n      registerAccelerator: false,\n      accelerator:         'CmdOrCtrl+,',\n      click:               openPreferences,\n    },\n    { type: 'separator' },\n  ];\n}\n\n/**\n * Adjusts the zoom level for the focused window by the desired increment.\n * Also emits an IPC request to the webContents to trigger a resize of the\n * extensions view.\n * @param focusedWindow The window that has focus\n * @param zoomLevelAdjustment The desired increment to adjust the zoom level by\n */\nfunction adjustZoomLevel(focusedWindow: Electron.BaseWindow | undefined, zoomLevelAdjustment: number) {\n  if (!focusedWindow || !(focusedWindow instanceof Electron.BrowserWindow)) {\n    return;\n  }\n\n  const { webContents } = focusedWindow;\n  const currentZoomLevel = webContents.getZoomLevel();\n  const desiredZoomLevel = zoomLevelAdjustment === 0 ? zoomLevelAdjustment : currentZoomLevel + zoomLevelAdjustment;\n\n  webContents.setZoomLevel(desiredZoomLevel);\n\n  // Also sync the zoom level of any child views (e.g. the extensions view in\n  // the main window).\n  for (const child of focusedWindow.contentView.children) {\n    if (child instanceof Electron.WebContentsView) {\n      child.webContents.setZoomLevel(desiredZoomLevel);\n    }\n  }\n  // For the main window, this triggers resizing the extensions view.\n  setImmediate(() => webContents.send('extensions/getContentArea'));\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/__tests__/mac-ca.spec.ts",
    "content": "import crypto from 'crypto';\nimport fs from 'fs';\nimport os from 'os';\n\nimport { jest } from '@jest/globals';\n\nimport type { spawnFile } from '@pkg/utils/childProcess';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\n\n// mock child process execution to return our own results.\njest.mock('@pkg/utils/childProcess');\n\nconst modules = mockModules({\n  crypto:                    { X509Certificate: jest.fn<(blob: crypto.BinaryLike) => crypto.X509Certificate>() },\n  '@pkg/utils/childProcess': { spawnFile: jest.fn<typeof spawnFile>() },\n});\n\n/**\n * testCertMock is a subset of crypto.X509Certificate with an additional bit to\n * indicate whether we expect this certificate to be accepted.\n */\ninterface testCertMock {\n  ca:         boolean;\n  issuer:     string;\n  subject:    string;\n  acceptable: boolean;\n}\n\nconst testDarwin = os.platform() === 'darwin' ? test : test.skip;\n\ntestDarwin('getMacCertificates', async() => {\n  const { default: getMacCertificates } = await import('../mac-ca');\n  const endCertMarker = '\\n-----END CERTIFICATE-----';\n  // test certificates; keyed by keychain, then cert PEM\n  const testCerts: Record<string, Record<string, testCertMock>> = {\n    '/System/Library/Keychains/SystemRootCertificates.keychain': {\n      [`system ca root good issuer${ endCertMarker }`]: {\n        ca:         true,\n        issuer:     'some issuer',\n        subject:    'some issuer',\n        acceptable: true,\n      },\n      [`system root not ca${ endCertMarker }`]: {\n        ca:         false,\n        issuer:     'some issuer',\n        subject:    'some issuer',\n        acceptable: false,\n      },\n    },\n    '/Library/Keychains/System.keychain': {\n      [`system keychain${ endCertMarker }`]: {\n        ca:         true,\n        issuer:     'some issuer',\n        subject:    'some some issuer',\n        acceptable: true,\n      },\n      [`system keychain different issuer${ endCertMarker }`]: {\n        ca:         true,\n        issuer:     'some issuer',\n        subject:    'some subject',\n        acceptable: true,\n      },\n    },\n  };\n  const expected: string[] = [];\n  const actual: string[] = [];\n  const pemToKeychain: Record<string, string> = {};\n\n  for (const [keychain, store] of Object.entries(testCerts)) {\n    for (const [pem, cert] of Object.entries(store)) {\n      pemToKeychain[pem] = keychain;\n      if (cert.acceptable) {\n        expected.push(pem);\n      }\n    }\n  }\n\n  async function mockSpawnFile(command: string, args: string[], opts: { stdio?: any[] }): Promise<{ stdout: string }> {\n    let stdout = '';\n    const handlers: Record<string, () => Promise<void>> = {\n      'list-keychains': () => {\n        expect(args).toHaveLength(1);\n        stdout = Object\n          .keys(testCerts)\n          .filter(x => !x.endsWith('SystemRootCertificates.keychain'))\n          .map(p => `    \"${ p }\"    `).join('\\n');\n\n        return Promise.resolve();\n      },\n      'find-certificate': () => {\n        expect(args).toContain('-a'); // find all certs, not just the first\n        expect(args).toContain('-p'); // print certs as PEM\n        if (args.length > 3) {\n          const keychain = args[3];\n\n          expect(args).toHaveLength(4);\n          expect(Object.keys(testCerts)).toContain(keychain);\n          stdout = Object.keys(testCerts[keychain]).join('\\n');\n        } else {\n          expect(args).toHaveLength(3);\n          for (const keychain in testCerts) {\n            if (keychain.endsWith('SystemRootCertificates.keychain')) {\n            // emulate /usr/bin/security: don't list system roots implicitly.\n              continue;\n            }\n            stdout += `${ Object.keys(testCerts[keychain]).join('\\n') }\\n`;\n          }\n        }\n\n        return Promise.resolve();\n      },\n      'verify-cert': async() => {\n        const pathFlag = args.find(arg => arg.startsWith('-c')) ?? '';\n        const certPath = pathFlag.substring(2);\n\n        expect(certPath).not.toHaveLength(0);\n        expect(args).toContain('-L'); // local verification only; no network.\n        expect(args).toContain('-l'); // certificate should be a CA\n        expect(args).toContain('-Roffline'); // revocation checking: offline only\n\n        const actualPEM = await fs.promises.readFile(certPath, 'utf-8');\n        const keychain = pemToKeychain[actualPEM];\n\n        expect(pemToKeychain).toHaveProperty(actualPEM);\n        expect(testCerts[keychain]).toHaveProperty(actualPEM);\n        if (!testCerts[keychain][actualPEM].acceptable) {\n          throw new Error('certificate is not trusted, this should be caught');\n        }\n      },\n    };\n\n    expect(command).toEqual('/usr/bin/security');\n    expect(args).not.toHaveLength(0);\n    expect(handlers).toHaveProperty(args[0]);\n    await handlers[args[0]]();\n\n    const outStream = opts?.stdio?.[1];\n\n    if (outStream instanceof fs.WriteStream) {\n      outStream.write(stdout);\n    }\n\n    return { stdout };\n  }\n\n  modules['@pkg/utils/childProcess'].spawnFile.mockImplementation(mockSpawnFile as any);\n  modules.crypto.X509Certificate.mockImplementation((buffer) => {\n    const pem = buffer.toString();\n    const keychain = pemToKeychain[pem];\n\n    expect(pemToKeychain).toHaveProperty(pem);\n    expect(testCerts[keychain]).toHaveProperty(pem);\n\n    return testCerts[keychain][pem] as unknown as crypto.X509Certificate;\n  });\n  for await (const certPEM of getMacCertificates()) {\n    actual.push(certPEM);\n  }\n\n  expect(actual.sort()).toEqual(expected.sort());\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/cert-parse.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n\nSome code is derived from node-forge:\n\nNew BSD License (3-clause)\nCopyright (c) 2010, Digital Bazaar, Inc.\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n    * Redistributions of source code must retain the above copyright\n      notice, this list of conditions and the following disclaimer.\n    * Redistributions in binary form must reproduce the above copyright\n      notice, this list of conditions and the following disclaimer in the\n      documentation and/or other materials provided with the distribution.\n    * Neither the name of Digital Bazaar, Inc. nor the\n      names of its contributors may be used to endorse or promote products\n      derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY\nDIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND\nON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\n*/\n\nimport forge from 'node-forge';\n\nimport Logging from '@pkg/utils/logging';\nimport { defined } from '@pkg/utils/typeUtils';\n\nconst console = Logging.background;\nconst { asn1 } = forge;\n\nconst x509CertificateValidityValidator = {\n  name:        'Certificate',\n  tagClass:    asn1.Class.UNIVERSAL,\n  type:        asn1.Type.SEQUENCE,\n  constructed: true,\n  value:       [{\n    name:        'Certificate.TBSCertificate',\n    tagClass:    asn1.Class.UNIVERSAL,\n    type:        asn1.Type.SEQUENCE,\n    constructed: true,\n    value:       [{\n      name:        'Certificate.TBSCertificate.version',\n      tagClass:    asn1.Class.CONTEXT_SPECIFIC,\n      type:        0,\n      constructed: true,\n      optional:    true,\n      value:       [{\n        name:        'Certificate.TBSCertificate.version.integer',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.INTEGER,\n        constructed: false,\n      }],\n    }, {\n      name:        'Certificate.TBSCertificate.serialNumber',\n      tagClass:    asn1.Class.UNIVERSAL,\n      type:        asn1.Type.INTEGER,\n      constructed: false,\n    }, {\n      name:        'Certificate.TBSCertificate.signature',\n      tagClass:    asn1.Class.UNIVERSAL,\n      type:        asn1.Type.SEQUENCE,\n      constructed: true,\n      value:       [{\n        name:        'Certificate.TBSCertificate.signature.algorithm',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.OID,\n        constructed: false,\n      }, {\n        name:     'Certificate.TBSCertificate.signature.parameters',\n        tagClass: asn1.Class.UNIVERSAL,\n        optional: true,\n      }],\n    }, {\n      name:        'Certificate.TBSCertificate.issuer',\n      tagClass:    asn1.Class.UNIVERSAL,\n      type:        asn1.Type.SEQUENCE,\n      constructed: true,\n      captureAsn1: 'issuerEncoded',\n    }, {\n      name:        'Certificate.TBSCertificate.validity',\n      tagClass:    asn1.Class.UNIVERSAL,\n      type:        asn1.Type.SEQUENCE,\n      constructed: true,\n      // The spec only specifies that there will be two times, each of which may\n      // be either UTC or generalized.  We can't guarantee that both times are\n      // even in the same format; so we alternate reading UTC and generalized,\n      // twice, and only determine the actual value based on what we managed to\n      // read out.\n      value:       [{\n        name:        'Certificate.TBSCertificate.validity.notBefore (utc)',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.UTCTIME,\n        constructed: false,\n        optional:    true,\n        capture:     'validityUTC1',\n      }, {\n        name:        'Certificate.TBSCertificate.validity.notBefore (generalized)',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.GENERALIZEDTIME,\n        constructed: false,\n        optional:    true,\n        capture:     'validityGeneralized1',\n      }, {\n        name:        'Certificate.TBSCertificate.validity.notAfter (utc)',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.UTCTIME,\n        constructed: false,\n        optional:    true,\n        capture:     'validityUTC2',\n      }, {\n        name:        'Certificate.TBSCertificate.validity.notAfter (generalized)',\n        tagClass:    asn1.Class.UNIVERSAL,\n        type:        asn1.Type.GENERALIZEDTIME,\n        constructed: false,\n        optional:    true,\n        capture:     'validityGeneralized2',\n      }],\n    }],\n  }],\n};\n\n/**\n * parseIssuer decodes the ASN.1 encoded issuer and returns a map describing it.\n * @param encoded ASN.1 encoded issuer data.\n */\nfunction parseIssuer(encoded: any): Record<string, string> {\n  const decoded: {\n    type:       string;\n    value:      string;\n    name?:      string;\n    shortName?: string;\n  }[] = (forge.pki as any).RDNAttributesAsArray(encoded);\n  const result: Record<string, string> = Object.create({}, {\n    toString: {\n      enumerable: false,\n      value() {\n        return this.CN || this.OU || JSON.stringify(this);\n      },\n    },\n  });\n\n  for (const item of decoded) {\n    result[item.shortName ?? item.name ?? item.type] = item.value;\n  }\n\n  return result;\n}\n\n/**\n * Convert the given value to a Date using the given forge.asn1 method.\n */\nfunction convertDate(val: string | undefined, fn: 'utcTimeToDate' | 'generalizedTimeToDate') {\n  return val ? (forge.asn1 as any)[fn](val) as Date : undefined;\n}\n\n/**\n * Attempts to decode PEM certificate and handle exceptions\n * @param pem PEM file\n * @returns Decoded PEM certificate or null on error\n */\nfunction tryPemDecode(pem: string) {\n  try {\n    return forge.pem.decode(pem)[0];\n  } catch (e) {\n    console.error(`Rejecting invalid certificate: encountered errors:`, e);\n\n    return null;\n  }\n}\n\n/**\n * Check a given PEM certificate to ensure it is within the valid date range.\n * This does _not_ do any other checking of the certificate.\n */\nexport default function checkCertValidity(pem: string): boolean {\n  // Node-forge chokes on non-RSA certificates; so we will need to parse it\n  // manually.  Code is based on BSD-3 licensed node-forge (lib/x509.js).\n\n  console.debug('Checking certificate for expiry...');\n  const msg = tryPemDecode(pem);\n\n  if (!msg) {\n    console.warn('Skipping certificate, cannot decode');\n\n    return false;\n  }\n\n  if (!msg.type.endsWith('CERTIFICATE')) {\n    console.warn(`Skipping certificate with unknown type ${ msg.type }`);\n\n    return false;\n  }\n  if (msg.procType?.type === 'ENCRYPTED') {\n    console.warn('Skipping encrypted certificate');\n\n    return false;\n  }\n  const obj = forge.asn1.fromDer(msg.body);\n  const capture: {\n    validityUTC1?:         string;\n    validityGeneralized1?: string;\n    validityUTC2?:         string;\n    validityGeneralized2?: string;\n    issuerEncoded?:        any;\n  } = {};\n  const errors: string[] = [];\n  // @types/node-forge is missing many methods, so we need to cast as any.\n  const valid = (forge.asn1 as any).validate(obj, x509CertificateValidityValidator, capture, errors);\n  const now = new Date();\n\n  if (!valid) {\n    console.warn(`Rejecting invalid certificate: encountered errors:`, errors);\n\n    return false;\n  }\n\n  const certInfo = {\n    issuer:   parseIssuer(capture.issuerEncoded),\n    validity: [\n      convertDate(capture.validityUTC1, 'utcTimeToDate'),\n      convertDate(capture.validityGeneralized1, 'generalizedTimeToDate'),\n      convertDate(capture.validityUTC2, 'utcTimeToDate'),\n      convertDate(capture.validityGeneralized2, 'generalizedTimeToDate'),\n    ].filter(defined),\n  };\n\n  console.debug('Inspecting certificate', certInfo);\n\n  if (certInfo.validity.length !== 2) {\n    console.warn(`Certificate has unexpected validity dates:`, certInfo);\n\n    return false;\n  }\n  // We don't care about notBefore; just check that notAfter is valid.\n  if (certInfo.validity[1] < now) {\n    console.warn([\n      `Rejected cert expired on ${ certInfo.validity[1].toUTCString() }`,\n      `issued by ${ certInfo.issuer }`,\n    ].join(' '));\n\n    return false;\n  }\n\n  return true;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/index.ts",
    "content": "import dns from 'dns';\nimport http from 'http';\nimport https from 'https';\nimport os from 'os';\nimport util from 'util';\n\nimport Electron from 'electron';\n\nimport getLinuxCertificates from './linux-ca';\nimport getMacCertificates from './mac-ca';\nimport ElectronProxyAgent from './proxy';\nimport getWinCertificates from './win-ca';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport { windowMapping } from '@pkg/window';\n\nconst console = Logging.networking;\n\nexport default async function setupNetworking() {\n  const agentOptions = { ...https.globalAgent.options };\n\n  if (!Array.isArray(agentOptions.ca)) {\n    agentOptions.ca = agentOptions.ca ? [agentOptions.ca] : [];\n  }\n  try {\n    for await (const cert of getSystemCertificates()) {\n      agentOptions.ca.push(cert);\n    }\n  } catch (ex) {\n    console.error('Error getting system certificates:', ex);\n    throw ex;\n  }\n\n  const proxyAgent = new ElectronProxyAgent({\n    httpAgent:  new http.Agent(agentOptions),\n    httpsAgent: new https.Agent(agentOptions),\n  });\n\n  http.globalAgent = proxyAgent;\n  https.globalAgent = proxyAgent;\n\n  // Set up certificate handling for system certificates on Windows and macOS\n  Electron.app.on('certificate-error', async(event, webContents, url, error, certificate, callback) => {\n    const tlsPort = 9443;\n    const dashboardUrls = [\n      `https://127.0.0.1:${ tlsPort }`,\n      `wss://127.0.0.1:${ tlsPort }`,\n      'http://127.0.0.1:6120',\n      'ws://127.0.0.1:6120',\n    ];\n\n    const pluginDevUrls = [\n      `https://localhost:8888`,\n      `wss://localhost:8888`,\n    ];\n\n    if (\n      process.env.NODE_ENV === 'development' &&\n      process.env.RD_ENV_PLUGINS_DEV &&\n      pluginDevUrls.some(x => url.startsWith(x))\n    ) {\n      event.preventDefault();\n\n      callback(true);\n\n      return;\n    }\n\n    if (dashboardUrls.some(x => url.startsWith(x)) && 'dashboard' in windowMapping) {\n      event.preventDefault();\n\n      callback(true);\n\n      return;\n    }\n\n    if (error === 'net::ERR_CERT_INVALID') {\n      // If we're getting *this* particular error, it means it's an untrusted cert.\n      // Ask the system store.\n      console.log(`Attempting to check system certificates for ${ url } (${ certificate.subjectName }/${ certificate.fingerprint })`);\n      try {\n        for await (const cert of getSystemCertificates()) {\n          // For now, just check that the PEM data matches exactly; this is\n          // probably a little more strict than necessary, but avoids issues like\n          // an attacker generating a cert with the same serial.\n          if (cert === certificate.data.replace(/\\r/g, '')) {\n            console.log(`Accepting system certificate for ${ certificate.subjectName } (${ certificate.fingerprint })`);\n\n            callback(true);\n\n            return;\n          }\n        }\n      } catch (ex) {\n        console.error(ex);\n      }\n    }\n\n    console.log(`Not handling certificate error ${ error } for ${ url }`);\n\n    callback(false);\n  });\n\n  mainEvents.on('cert-get-ca-certificates', async() => {\n    const certs: string[] = [];\n\n    for await (const cert of getSystemCertificates()) {\n      certs.push(cert);\n    }\n\n    mainEvents.emit('cert-ca-certificates', certs);\n  });\n\n  mainEvents.emit('network-ready');\n}\n\n/**\n * Get the system certificates in PEM format.\n */\nexport async function * getSystemCertificates(): AsyncIterable<string> {\n  const platform = os.platform();\n\n  if (platform.startsWith('win')) {\n    yield * getWinCertificates();\n  } else if (platform === 'darwin') {\n    yield * getMacCertificates();\n  } else if (platform === 'linux') {\n    yield * getLinuxCertificates();\n  } else {\n    throw new Error(`Cannot get system certificates on ${ platform }`);\n  }\n}\n\nexport async function checkConnectivity(target: string): Promise<boolean> {\n  try {\n    await util.promisify(dns.lookup)(target);\n\n    return true;\n  } catch {\n    return false;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/linux-ca.ts",
    "content": "/**\n * This module fetches system CAs on Linux.  The command lines are based on the\n * `linux-ca` package on NPM, but none of the code is copied.\n */\n\nimport checkCertValidity from './cert-parse';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport { defined } from '@pkg/utils/typeUtils';\n\nconst console = Logging.networking;\n\n/**\n * Asynchronously enumerate the certificate authorities that should be used to\n * build the Rancher Desktop trust store, in PEM format in undefined order.\n */\nexport default async function * getLinuxCertificates(): AsyncIterable<string> {\n  const tokenURLs = await Array.fromAsync(listTokens());\n  const promises = tokenURLs.map(listCertificates).map(async function(certURLIterable) {\n    return (await Array.fromAsync(certURLIterable)).map(getCertificate);\n  });\n  const certs = await Promise.all((await Promise.all(promises)).flat());\n\n  yield * certs.filter(defined).filter(checkCertValidity);\n}\n\nasync function * listTokens(): AsyncIterable<string> {\n  try {\n    const { stdout } = await spawnFile('p11tool', ['--list-token-urls'],\n      { stdio: ['ignore', 'pipe', console] });\n\n    for (const line of stdout.split(/\\n/).filter(x => x)) {\n      yield line.trim();\n    }\n  } catch (ex) {\n    console.error(`Error listing system certificate tokens, ignoring: ${ ex }`);\n  }\n}\n\nasync function * listCertificates(tokenURL: string): AsyncIterable<string> {\n  try {\n    const { stdout } = await spawnFile(\n      'p11tool', ['--list-all-trusted', '--only-urls', '--batch', tokenURL],\n      { stdio: ['ignore', 'pipe', console] });\n\n    for (const line of stdout.split(/\\n/).filter(x => x)) {\n      yield line.trim();\n    }\n  } catch (ex) {\n    console.error(`Error listing system certificates, ignoring: ${ ex }`);\n  }\n}\n\nasync function getCertificate(certURL: string): Promise<string | undefined> {\n  try {\n    const { stdout } = await spawnFile(\n      'p11tool', ['--export', certURL],\n      { stdio: ['ignore', 'pipe', console] });\n\n    return stdout.trim();\n  } catch (ex) {\n    console.error(`Error getting system certificate, ignoring: ${ ex }`);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/mac-ca.ts",
    "content": "/**\n * This module fetches system certificates on macOS.\n */\n\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport util from 'util';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.networking;\n\n/**\n * Asynchronously enumerate the certificate authorities that should be used to\n * build the Rancher Desktop trust store, in PEM format in undefined order.\n */\nexport default async function * getMacCertificates(): AsyncIterable<string> {\n  const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rancher-desktop-certificates-'));\n\n  try {\n    const keychains = await Array.fromAsync(listKeychains());\n    const certLists = await Promise.all(keychains.map(async keychain => {\n      const keychainWorkDir = await fs.promises.mkdtemp(workdir + path.sep);\n\n      return await Array.fromAsync(getFilteredCertificates(keychainWorkDir, keychain));\n    }));\n    for (const certList of certLists) {\n      yield * certList;\n    }\n  } finally {\n    await fs.promises.rm(workdir, {\n      recursive: true, force: true, maxRetries: 3,\n    });\n  }\n}\n\n/**\n * Return all keychains that we should import from.\n */\nasync function * listKeychains(): AsyncIterable<string> {\n  const { stdout } = await spawnFile('/usr/bin/security', ['list-keychains'],\n    { stdio: ['ignore', 'pipe', console] });\n\n  for (const line of stdout.split(/\\n/).filter(x => x)) {\n    yield line.trim().replace(/^\"|\"$/g, '');\n  }\n  try {\n    // Add the system root certificates keychain; this is normally not listed\n    // as it wouldn't include _client_ certificates.\n    const rootCerts = '/System/Library/Keychains/SystemRootCertificates.keychain';\n\n    await fs.promises.access(rootCerts, fs.constants.R_OK);\n    yield rootCerts;\n  } catch (ex) { /* swallow the error */ }\n}\n\n/**\n  * Asynchronously enumerate PEM-encoded certificates from the given keychain in\n  * undefined order.\n  *\n  * @param workdir A temporary directory where files can be written.\n  * @param keychain The full path to the keychain database to enumerate.\n  */\nasync function * getFilteredCertificates(workdir: string, keychain: string): AsyncIterable<string> {\n  console.debug(`getting certificates from ${ keychain }...`);\n\n  const certIterator = getPEMCertificates(workdir, keychain);\n\n  for await (const certPEM of certIterator) {\n    try {\n      const cert = new crypto.X509Certificate(certPEM);\n      const certPath = path.join(workdir, 'cert.pem');\n      const subject = cert.subject.replace(/[\\r\\n]+/g, ' ');\n\n      if (!cert.ca) {\n        console.debug('Skipping non-CA certificate', subject);\n        continue;\n      }\n      await fs.promises.writeFile(certPath, certPEM, 'utf-8');\n      try {\n        await spawnFile('/usr/bin/security', ['verify-cert', `-c${ certPath }`, '-L', '-l', '-Roffline'], { stdio: ['ignore', 'ignore', 'pipe'] });\n      } catch (ex: any) {\n        console.debug(`Skipping untrusted certificate ${ subject }: ${ (ex.stderr?.toString() ?? ex.toString()).trim() }`);\n        continue;\n      }\n      console.debug('Including certificate', subject);\n    } catch (ex) {\n      console.debug('Skipping certificate that could not be parsed', ex, certPEM);\n      continue;\n    }\n    yield certPEM;\n  }\n\n  console.debug(`got certificates from ${ keychain }`);\n}\n\n/**\n * Enumerate all system certificates as PEM, in undefined order.  This does not\n * do the necessary processing to ensure they are valid for our use.\n *\n * @param workdir A temporary directory where files can be written.\n * @param keychain Optional absolute path to a specific Keychain database to use.\n */\nasync function * getPEMCertificates(workdir: string, keychain?: string): AsyncIterable<string> {\n  // In order to avoid issues on machine with a very large number of certificates,\n  // write all certificates (in PEM format) to a file, and then read that file out.\n  const pemMarker = '-----END CERTIFICATE-----';\n  const pemFilePath = path.join(workdir, 'all-certs.pem');\n  const pemFile = await fs.promises.open(pemFilePath, fs.constants.O_CREAT | fs.constants.O_TRUNC | fs.constants.O_WRONLY);\n  const pemFileStream = fs.createWriteStream(pemFilePath, { fd: pemFile });\n  const args = ['find-certificate', '-a', '-p'];\n\n  if (keychain) {\n    args.push(keychain);\n  }\n  await spawnFile('/usr/bin/security', args, { stdio: ['ignore', pemFileStream, console] });\n  await util.promisify((cb: (err?: Error | null ) => void) => pemFileStream.close(cb))();\n  await pemFile.close();\n\n  let pemLines: string[] = [];\n\n  for await (const line of readFileByLine(pemFilePath)) {\n    pemLines.push(line);\n    if (line === pemMarker) {\n      yield pemLines.join('\\n');\n      pemLines = [];\n    }\n  }\n\n  if (pemLines.length > 0 && pemLines[pemLines.length - 1] === pemMarker) {\n    yield pemLines.join('\\n');\n  }\n}\n\n/**\n * Read the given file, returning one line at a time.\n */\nasync function * readFileByLine(filePath: string, encoding: BufferEncoding = 'utf-8'): AsyncIterable<string> {\n  const file = await fs.promises.open(filePath, fs.constants.O_RDONLY);\n\n  try {\n    const buf = Buffer.alloc(256);\n    let lastLine = '';\n\n    while (true) {\n      const { bytesRead } = await file.read(buf, 0, buf.length);\n\n      if (bytesRead === 0) {\n        break;\n      }\n      let offset = 0;\n\n      while (true) {\n        const nextNewLine = buf.indexOf('\\n', offset, encoding);\n\n        if (nextNewLine < 0) {\n          lastLine += buf.toString(encoding, offset, bytesRead);\n          break;\n        }\n        yield lastLine + buf.toString(encoding, offset, nextNewLine);\n        lastLine = '';\n        offset = nextNewLine + 1;\n      }\n    }\n    if (lastLine) {\n      yield lastLine;\n    }\n  } finally {\n    await file.close();\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/proxy.ts",
    "content": "import Electron from 'electron';\nimport { ProxyAgent, ProxyAgentOptions } from 'proxy-agent';\n\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.networking;\n\nexport default class ElectronProxyAgent extends ProxyAgent {\n  constructor(options?: ProxyAgentOptions) {\n    super(options);\n    this.session = Electron.session.defaultSession;\n    this.getProxyForUrl = this.getProxyForUrlElectron.bind(this);\n  }\n\n  /**\n   * The Electron session to use.\n   */\n  session: Electron.Session;\n\n  async getProxyForUrlElectron(url: string): Promise<string> {\n    const result = await this.session.resolveProxy(url);\n\n    for (const line of result.split(';')) {\n      const [, type, proxy] = /^\\s*(\\S+)\\s+(.*?)\\s*$/.exec(line) ?? [];\n\n      switch (type) {\n      case undefined:\n        // Invalid line; skip.\n        continue;\n      case 'DIRECT':\n        // No proxy; return an empty string to mean no proxy.\n        return '';\n      case 'SOCKS':\n      case 'SOCKS5':\n        return `socks://${ proxy }`;\n      case 'SOCKS4':\n        return `socks4a://${ proxy }`;\n      case 'PROXY':\n      case 'HTTP':\n        return `http://${ proxy }`;\n      case 'HTTPS':\n        return `https://${ proxy }`;\n      default:\n        console.debug(`Unknown proxy specification ${ line.trim() } skipped.`);\n      }\n    }\n\n    // If we got no valid lines, just use a direct connection.  This is the case\n    // if no proxies are set at all.\n    return '';\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/networking/win-ca.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport tls from 'tls';\n\nimport * as childProcess from '@pkg/utils/childProcess';\nimport AsyncCallbackIterator from '@pkg/utils/iterator';\nimport Logging from '@pkg/utils/logging';\nimport { executable } from '@pkg/utils/resources';\n\n/**\n * Asynchronously enumerate the certificate authorities that should be used to\n * build the Rancher Desktop trust store, in PEM format in undefined order.\n */\nexport default async function * getWinCertificates(): AsyncIterable<string> {\n  // Windows will dynamically download CA certificates on demand by default;\n  // this means that if we just enumerate the Windows certificate store, we will\n  // be missing some standard certificates.  To approximate the desired\n  // behaviour, we will enumerate both the Windows store as well as the OpenSSL\n  // one built into NodeJS.\n\n  let buffer = '';\n  const proc = childProcess.spawn(executable('wsl-helper'), ['certificates'], {\n    stdio:       ['ignore', 'pipe', await Logging['networking-ca'].fdStream],\n    windowsHide: true,\n  });\n  const iterator = new AsyncCallbackIterator<string>();\n\n  proc.stdout.on('data', async(chunk: string | Buffer) => {\n    try {\n      if (Buffer.isBuffer(chunk)) {\n        buffer += chunk.toString('utf-8');\n      } else {\n        buffer += chunk;\n      }\n      while (true) {\n        const [match] = /^.*?-----END CERTIFICATE-----\\r?\\n?/s.exec(buffer) ?? [];\n\n        if (!match) {\n          break;\n        }\n        buffer = buffer.substring(match.length);\n        await iterator.emit(match);\n      }\n    } catch (ex) {\n      await iterator.error(ex);\n    }\n  });\n  proc.on('exit', async(code, signal) => {\n    if (!(code === 0 || signal === 'SIGTERM')) {\n      iterator.error(code || signal);\n    } else {\n      try {\n        await iterator.emit(buffer);\n        iterator.end();\n      } catch (ex) {\n        await iterator.error(ex);\n      }\n    }\n  });\n\n  yield * iterator;\n  yield * tls.rootCertificates;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/serverHelper.ts",
    "content": "import http from 'http';\n\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.server;\n\nexport function basicAuth(userdb: Record<string, string>, authString: string): string | false {\n  if (!authString) {\n    console.log('Auth failure: no username+password given');\n\n    return false;\n  }\n  const m = /^Basic\\s+(.*)/i.exec(authString);\n\n  if (!m) {\n    console.log('Auth failure: only Basic auth is supported');\n\n    return false;\n  }\n  const [user, ...passwordParts] = base64Decode(m[1]).split(':');\n  const password = passwordParts.join(':');\n\n  if (!(user in userdb)) {\n    console.log(`Auth failure: unknown user ${ user } specified.`);\n\n    return false;\n  }\n  if (userdb[user] === password) {\n    return user;\n  }\n  console.log(`Auth failure: user/password validation failure for attempted login of user ${ user }`);\n\n  return false;\n}\n\nfunction base64Decode(value: string): string {\n  return Buffer.from(value, 'base64').toString('utf-8');\n}\n\n/**\n * Reads in the input from the request body (which is done by calling `for await (const chunk of result)`),\n * verifies it hasn't exceeded the max-allowed size,\n * and returns it as a string.\n *\n * @param request\n * @param maxPayloadSize\n * @return [value: string, error: string]\n */\nexport async function getRequestBody(request: http.IncomingMessage, maxPayloadSize: number): Promise<[string, string, number]> {\n  const chunks: Buffer[] = [];\n  let error = '';\n  let errorCode = 200;\n  let dataSize = 0;\n\n  // Read in the request body\n  for await (const chunk of request) {\n    dataSize += chunk.length;\n    if (dataSize > maxPayloadSize) {\n      if (errorCode === 200) {\n        error = `request body is too long, request body size exceeds ${ maxPayloadSize }`;\n        errorCode = 413;\n      }\n      // Do not break out of the loop -- you need to stay to consume the rest of the input.\n    } else {\n      chunks.push(chunk);\n    }\n  }\n  const data = Buffer.concat(chunks).toString();\n\n  return [data, error, errorCode];\n}\n\n// There's a `randomStr` in utils/string.ts but it's only usable from the UI side\n// because it depends on access to the `window` object.\n// And trying to use `cryptoRandomString()` from crypto-random-string gives an error message\n// indicating that it pulls in some `require` statements where `import` is required.\n\nexport function randomStr(length = 16) {\n  const alpha = 'abcdefghijklmnopqrstuvwxyz';\n  const num = '0123456789';\n  const charSet = alpha + alpha.toUpperCase() + num;\n  const charSetLength = charSet.length;\n  const chars = [];\n\n  while (length-- > 0) {\n    chars.push(charSet[Math.floor(Math.random() * charSetLength)]);\n  }\n\n  return chars.join('');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/snapshots/snapshots.ts",
    "content": "import { exec } from 'child_process';\nimport util from 'util';\n\nimport { Snapshot, SpawnResult } from '@pkg/main/snapshots/types';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\nimport { getRdctlPath } from '@pkg/utils/paths';\nimport { executable } from '@pkg/utils/resources';\n\nconst console = Logging.snapshots;\n\nfunction parseLines(line: string): string[] {\n  return line?.split(/\\r?\\n/) || [];\n}\n\nclass SnapshotsError {\n  readonly isSnapshotError = true;\n  message: string;\n\n  constructor(args: string[], response: SpawnResult) {\n    console.error(`snapshot error: command rdctl ${ args.join(' ') } => error ${ response.stdout }`);\n    try {\n      const value = JSON.parse(response.stdout);\n\n      this.message = value?.error;\n      if (!this.message) {\n        console.error(`Empty or no error field found in the output`);\n        this.message = 'Something went wrong with the `rdctl snapshot` command; the details are in the snapshots log file';\n      }\n    } catch (error) {\n      const msg = 'Cannot parse error message from `rdctl snapshot` command';\n\n      console.error(`${ msg }: ${ error }`);\n      this.message = msg;\n    }\n  }\n}\n\nclass SnapshotsImpl {\n  private async rdctl(commandArgs: string[]): Promise<SpawnResult> {\n    try {\n      const rdctlPath = getRdctlPath();\n\n      return await spawnFile(rdctlPath || '', commandArgs, { stdio: ['ignore', 'pipe', 'pipe'] });\n    } catch (err: any) {\n      return {\n        stdout: err?.stdout ?? '', stderr: err?.stderr ?? '', error: err,\n      };\n    }\n  }\n\n  async list(): Promise<Snapshot[]> {\n    const response = await this.rdctl(['snapshot', 'list', '--json']);\n\n    if (response.error) {\n      return [];\n    }\n\n    const data = parseLines(response.stdout).filter(line => line);\n\n    return data.map(line => JSON.parse(line));\n  }\n\n  async create(snapshot: Snapshot) : Promise<void> {\n    const args = [\n      'snapshot',\n      'create',\n      snapshot.name,\n      '--json',\n    ];\n\n    if (snapshot.description) {\n      args.push('--description', snapshot.description);\n    }\n\n    const response = await this.rdctl(args);\n\n    if (response.error) {\n      throw new SnapshotsError(args, response);\n    }\n  }\n\n  async restore(name: string) : Promise<void> {\n    const args = ['snapshot', 'restore', name, '--json'];\n    const response = await this.rdctl(args);\n\n    if (response.error) {\n      throw new SnapshotsError(args, response);\n    }\n  }\n\n  async delete(name: string) : Promise<void> {\n    const args = ['snapshot', 'delete', name, '--json'];\n    const response = await this.rdctl(args);\n\n    if (response.error) {\n      throw new SnapshotsError(args, response);\n    }\n  }\n\n  async cancel() {\n    const name = 'rdctl';\n    const keyword = 'snapshot';\n    const command = `${ name } ${ keyword }`;\n    const asyncExec = util.promisify(exec);\n\n    try {\n      if (process.platform === 'win32') {\n        const { stdout } = await asyncExec(`tasklist /FI \"IMAGENAME eq ${ name }.exe\" /FO CSV /NH`);\n        const processes = stdout.split('\\r\\n');\n\n        for (const proc of processes) {\n          const [_imageName, rawPid, ..._rest] = proc.split(',');\n          const pid = Number(rawPid?.trim().replaceAll('\"', ''));\n          const exe = executable('wsl-helper');\n\n          if (pid) {\n            console.log(`Found process ${ command } with PID ${ pid }`);\n            await spawnFile(exe, ['process', 'kill', `--pid=${ pid }`], { stdio: console });\n          }\n        }\n      } else {\n        const { stdout } = await asyncExec(`ps aux | grep \"${ command }\" | grep -v grep`);\n        const processes = stdout.split('\\n');\n\n        processes.forEach((proc) => {\n          const [_user, rawPid, ..._rest] = proc.split(/\\s+/);\n          const pid = Number(rawPid?.trim());\n\n          if (pid) {\n            console.log(`Found process ${ command } with PID ${ pid }`);\n            process.kill(pid, 'SIGTERM');\n          }\n        });\n      }\n    } catch (error) {\n      console.error('Error:', error);\n    }\n  }\n}\n\nexport const Snapshots = new SnapshotsImpl();\n"
  },
  {
    "path": "pkg/rancher-desktop/main/snapshots/types.ts",
    "content": "export interface SnapshotEvent {\n  type?:         'restore' | 'delete' | 'create' | 'confirm' | 'backend-lock',\n  result?:       'success' | 'cancel' | 'error',\n  error?:        string,\n  snapshotName?: string,\n  eventTime?:    string,\n}\n\nexport interface SpawnResult {\n  stdout: string,\n  stderr: string,\n  error?: any,\n}\n\nexport interface SnapshotDialog {\n  header:             string,\n  snapshot?:          Snapshot,\n  message?:           string,\n  detail?:            string,\n  info?:              string | null,\n  showProgressBar?:   boolean,\n  type?:              string,\n  snapshotEventType?: SnapshotEvent['type'],\n}\n\nexport interface Snapshot {\n  name:         string,\n  created:      string,\n  description?: string,\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/tray.ts",
    "content": "// This import is for the tray found in the menu bar (upper right on macos or\n// lower right on Windows).\n\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { KubeConfig } from '@kubernetes/client-node';\nimport Electron from 'electron';\n\nimport { VMBackend } from '@pkg/backend/backend';\nimport { State } from '@pkg/backend/k8s';\nimport * as kubeconfig from '@pkg/backend/kubeconfig';\nimport { Settings } from '@pkg/config/settings';\nimport { getIpcMainProxy } from '@pkg/main/ipcMain';\nimport mainEvents from '@pkg/main/mainEvents';\nimport { checkConnectivity } from '@pkg/main/networking';\nimport Logging from '@pkg/utils/logging';\nimport { networkStatus } from '@pkg/utils/networks';\nimport paths from '@pkg/utils/paths';\nimport { openMain, send } from '@pkg/window';\nimport { openDashboard } from '@pkg/window/dashboard';\nimport { openPreferences } from '@pkg/window/preferences';\n\nconst console = Logging.background;\nconst ipcMainProxy = getIpcMainProxy(console);\n\n/**\n * Tray is a class to manage the tray icon for rancher-desktop.\n */\nexport class Tray {\n  protected trayMenu:              Electron.Tray;\n  protected backendIsLocked = '';\n  protected kubernetesState = State.STOPPED;\n  private settings:                Settings;\n  private currentNetworkStatus:    networkStatus = networkStatus.CHECKING;\n  private static instance:         Tray;\n  private networkState:            boolean | undefined;\n  private runBuildFromConfigTimer: NodeJS.Timeout | null = null;\n  private kubeConfigWatchers:      fs.FSWatcher[] = [];\n  private fsWatcherInterval:       NodeJS.Timeout;\n\n  protected contextMenuItems: Electron.MenuItemConstructorOptions[] = [\n    {\n      id:      'state',\n      enabled: false,\n      label:   'Kubernetes is starting',\n      type:    'normal',\n      icon:    path.join(paths.resources, 'icons', 'kubernetes-icon-black.png'),\n    },\n    {\n      id:      'network-status',\n      enabled: false,\n      label:   `Network status: ${ this.currentNetworkStatus }`,\n      type:    'normal',\n      icon:    '',\n    },\n    {\n      id:      'container-engine',\n      enabled: false,\n      label:   '?',\n      type:    'normal',\n      icon:    '',\n    },\n    { type: 'separator' },\n    {\n      id:    'main',\n      label: 'Open main window',\n      type:  'normal',\n      click() {\n        openMain();\n      },\n    },\n    {\n      id:    'preferences',\n      label: 'Open preferences dialog',\n      type:  'normal',\n      click: openPreferences,\n    },\n    {\n      id:      'dashboard',\n      enabled: false,\n      label:   'Open cluster dashboard',\n      type:    'normal',\n      click:   openDashboard,\n    },\n    { type: 'separator' },\n    {\n      id:      'contexts',\n      label:   'Kubernetes Contexts',\n      type:    'submenu',\n      submenu: [],\n    },\n    { type: 'separator' },\n    {\n      id:    'quit',\n      label: 'Quit Rancher Desktop',\n      role:  'quit',\n      type:  'normal',\n    },\n  ];\n\n  private isMacOs = () => {\n    return os.platform() === 'darwin';\n  };\n\n  private isLinux = () => {\n    return os.platform() === 'linux';\n  };\n\n  private readonly trayIconsMacOs = {\n    stopped:  path.join(paths.resources, 'icons', 'logo-tray-stopped-Template@2x.png'),\n    starting: path.join(paths.resources, 'icons', 'logo-tray-starting-Template@2x.png'),\n    started:  path.join(paths.resources, 'icons', 'logo-tray-Template@2x.png'),\n    stopping: path.join(paths.resources, 'icons', 'logo-tray-stopping-Template@2x.png'),\n    error:    path.join(paths.resources, 'icons', 'logo-tray-error-Template@2x.png'),\n  };\n\n  private readonly trayIcons = {\n    stopped:  '',\n    starting: path.join(paths.resources, 'icons', 'logo-square-bw.png'),\n    started:  path.join(paths.resources, 'icons', 'logo-square.png'),\n    stopping: '',\n    error:    path.join(paths.resources, 'icons', 'logo-square-red.png'),\n  };\n\n  private readonly trayIconSet = this.isMacOs() ? this.trayIconsMacOs : this.trayIcons;\n\n  /**\n   * Watch for changes to the kubeconfig files; when the change event is\n   * triggered, close the watcher and restart after a duration (one second).\n   */\n  private async watchForChanges() {\n    for (const watcher of this.kubeConfigWatchers) {\n      watcher.close();\n    }\n    this.kubeConfigWatchers = [];\n\n    const paths = await kubeconfig.getKubeConfigPaths();\n    const options: fs.WatchOptions = {\n      persistent: false,\n      recursive:  !this.isLinux(), // Recursive not implemented in Linux\n      encoding:   'utf-8',\n    };\n\n    this.kubeConfigWatchers = paths.map(filepath => fs.watch(filepath, options, async(eventType) => {\n      if (eventType === 'rename') {\n        try {\n          await fs.promises.access(filepath);\n        } catch (ex) {\n          // File doesn't exist; wait for it to be recreated.\n          return;\n        }\n      }\n\n      // This prevents calling buildFromConfig multiple times in quick succession\n      // while making sure that the last file change within the period is processed.\n      this.runBuildFromConfigTimer ||= setTimeout(() => {\n        this.runBuildFromConfigTimer = null;\n        this.buildFromConfig();\n      }, 1_000);\n    }));\n  }\n\n  private constructor(settings: Settings) {\n    this.settings = settings;\n    this.trayMenu = new Electron.Tray(this.trayIconSet.starting);\n    this.trayMenu.setToolTip('Rancher Desktop');\n    const menuItem = this.contextMenuItems.find(item => item.id === 'container-engine');\n\n    if (menuItem) {\n      menuItem.label = `Container engine: ${ this.settings.containerEngine.name }`;\n    }\n\n    // Discover k8s contexts\n    try {\n      this.updateContexts();\n    } catch (err) {\n      Electron.dialog.showErrorBox('Error starting the app:',\n        `Error message: ${ err instanceof Error ? err.message : err }`);\n    }\n\n    const contextMenu = Electron.Menu.buildFromTemplate(this.contextMenuItems);\n\n    this.trayMenu.setContextMenu(contextMenu);\n\n    this.buildFromConfig();\n    this.watchForChanges();\n\n    // We reset the watchers on an interval in the event that `fs.watch` silently\n    // fails to keep watching. This original issue is documented at\n    // https://github.com/rancher-sandbox/rancher-desktop/pull/2038 and further discussed at\n    // https://github.com/rancher-sandbox/rancher-desktop/pull/7238#discussion_r1690128729\n    this.fsWatcherInterval = setInterval(() => this.watchForChanges(), 5 * 60_000);\n\n    mainEvents.on('backend-locked-update', this.backendStateEvent);\n    mainEvents.emit('backend-locked-check');\n    mainEvents.on('k8s-check-state', this.k8sStateChangedEvent);\n    mainEvents.on('settings-update', this.settingsUpdateEvent);\n\n    // If the network connectivity diagnostic changes results, update it here.\n    mainEvents.on('diagnostics-event', payload => {\n      if (payload.id !== 'network-connectivity') {\n        return;\n      }\n\n      const { connected } = payload;\n\n      if (this.networkState === connected) {\n        return; // network state hasn't changed since last check\n      }\n\n      this.networkState = connected;\n\n      this.handleUpdateNetworkStatus(this.networkState).catch((err: any) => {\n        console.log('Error updating network status: ', err);\n      });\n    });\n  }\n\n  private backendStateEvent = (backendIsLocked: string) => {\n    this.backendStateChanged(backendIsLocked);\n  };\n\n  private k8sStateChangedEvent = (mgr: VMBackend) => {\n    this.k8sStateChanged(mgr.state);\n  };\n\n  private settingsUpdateEvent = (cfg: Settings) => {\n    this.settings = cfg;\n    this.settingsChanged();\n  };\n\n  private updateNetworkStatusEvent = (_: Electron.IpcMainEvent, status: boolean) => {\n    this.handleUpdateNetworkStatus(status).catch((err:any) => {\n      console.log('Error updating network status: ', err);\n    });\n  };\n\n  /**\n   * Checks for an existing instance of Tray. If one does not\n   * exist, instantiate a new one.\n   */\n  public static getInstance(settings: Settings): Tray {\n    Tray.instance ??= new Tray(settings);\n\n    return Tray.instance;\n  }\n\n  /**\n   * Hide the tray menu.\n   */\n  public hide() {\n    this.trayMenu.destroy();\n    mainEvents.off('k8s-check-state', this.k8sStateChangedEvent);\n    mainEvents.off('settings-update', this.settingsUpdateEvent);\n    ipcMainProxy.removeListener('update-network-status', this.updateNetworkStatusEvent);\n    clearInterval(this.fsWatcherInterval);\n    if (this.runBuildFromConfigTimer) {\n      clearTimeout(this.runBuildFromConfigTimer);\n      this.runBuildFromConfigTimer = null;\n    }\n    for (const watcher of this.kubeConfigWatchers) {\n      watcher.close();\n    }\n    this.kubeConfigWatchers = [];\n  }\n\n  /**\n   * Show the tray menu.\n   */\n  public show() {\n    if (this.trayMenu.isDestroyed()) {\n      Tray.instance = new Tray(this.settings);\n    }\n  }\n\n  protected async handleUpdateNetworkStatus(status: boolean) {\n    if (!status) {\n      this.currentNetworkStatus = networkStatus.OFFLINE;\n    } else {\n      this.currentNetworkStatus = await checkConnectivity('k3s.io') ? networkStatus.CONNECTED : networkStatus.OFFLINE;\n    }\n    mainEvents.emit('update-network-status', this.currentNetworkStatus === networkStatus.CONNECTED);\n    send('update-network-status', this.currentNetworkStatus === networkStatus.CONNECTED);\n    this.updateMenu();\n  }\n\n  protected buildFromConfig() {\n    try {\n      this.updateContexts();\n      const contextMenu = Electron.Menu.buildFromTemplate(this.contextMenuItems);\n\n      this.trayMenu.setContextMenu(contextMenu);\n    } catch (err) {\n      console.log(`Error trying to update context menu: ${ err }`);\n    }\n  }\n\n  protected backendStateChanged(backendIsLocked: string) {\n    this.backendIsLocked = backendIsLocked;\n    this.updateMenu();\n  }\n\n  /**\n   * Called when the Kubernetes cluster state has changed.\n   * @param state The new cluster state.\n   */\n  protected k8sStateChanged(state: State) {\n    this.kubernetesState = state;\n    this.updateMenu();\n  }\n\n  /**\n   * Called when the application settings have changed.\n   */\n  protected settingsChanged() {\n    this.updateMenu();\n  }\n\n  protected updateMenu() {\n    if (this.trayMenu.isDestroyed()) {\n      return;\n    }\n\n    const labels = {\n      [State.STOPPED]:  'Kubernetes is stopped',\n      [State.STARTING]: 'Kubernetes is starting',\n      [State.STARTED]:  'Kubernetes is running',\n      [State.STOPPING]: 'Kubernetes is shutting down',\n      [State.ERROR]:    'Kubernetes has encountered an error',\n      [State.DISABLED]: 'Kubernetes is disabled',\n    };\n\n    let icon = path.join(paths.resources, 'icons', 'kubernetes-icon-black.png');\n    let logo = this.trayIconSet.starting;\n\n    if (this.kubernetesState === State.STARTED || this.kubernetesState === State.DISABLED) {\n      icon = path.join(paths.resources, 'icons', 'kubernetes-icon-color.png');\n      logo = this.trayIconSet.started;\n      // Update the contexts as a new kubernetes context will be added\n      this.updateContexts();\n      this.contextMenuItems = this.updateDashboardState(\n        this.kubernetesState === State.STARTED &&\n        this.settings.kubernetes.enabled,\n      );\n    } else if (this.kubernetesState === State.ERROR) {\n      // For licensing reasons, we cannot just tint the Kubernetes logo.\n      // Here we're using an icon from GitHub's octicons set.\n      icon = path.join(paths.resources, 'icons', 'issue-opened-16.png');\n      logo = this.trayIconSet.error;\n    }\n\n    const stateMenu = this.contextMenuItems.find(item => item.id === 'state');\n\n    if (stateMenu) {\n      stateMenu.label = labels[this.kubernetesState] || labels[State.ERROR];\n      stateMenu.icon = icon;\n    }\n\n    const containerEngineMenu = this.contextMenuItems.find(item => item.id === 'container-engine');\n\n    if (containerEngineMenu) {\n      const containerEngine = this.settings.containerEngine.name;\n\n      containerEngineMenu.label = containerEngine === 'containerd' ? containerEngine : `dockerd (${ containerEngine })`;\n      containerEngineMenu.icon = containerEngine === 'containerd' ? path.join(paths.resources, 'icons', 'containerd-icon-color.png') : '';\n    }\n    const networkStatusItem = this.contextMenuItems.find(item => item.id === 'network-status');\n\n    if (networkStatusItem) {\n      networkStatusItem.label = `Network status: ${ this.currentNetworkStatus }`;\n    }\n\n    this.contextMenuItems\n      .filter(item => item.id && ['preferences', 'dashboard', 'contexts', 'quit'].includes(item.id))\n      .forEach((item) => {\n        item.enabled = !this.backendIsLocked;\n      });\n\n    const contextMenu = Electron.Menu.buildFromTemplate(this.contextMenuItems);\n\n    this.trayMenu.setContextMenu(contextMenu);\n    this.trayMenu.setImage(logo);\n  }\n\n  protected updateDashboardState = (enabled = true) => this.contextMenuItems\n    .map(item => item.id === 'dashboard' ? { ...item, enabled } : item);\n\n  /**\n   * Update the list of Kubernetes contexts in the tray menu.\n   * This does _not_ raise any exceptions if we fail to read the config.\n   */\n  protected updateContexts() {\n    const kc = new KubeConfig();\n\n    try {\n      kc.loadFromDefault();\n    } catch (ex) {\n      console.error('Failed to load kubeconfig, ignoring:', ex);\n      // Keep going, with no context set.\n    }\n\n    const contextsMenu = this.contextMenuItems.find(item => item.id === 'contexts');\n    const curr = kc.getCurrentContext();\n    const ctxs = kc.getContexts();\n\n    if (!contextsMenu) {\n      return;\n    }\n    if (ctxs.length === 0) {\n      contextsMenu.submenu = [{ label: 'None found' }];\n    } else {\n      contextsMenu.submenu = ctxs.map(val => ({\n        label:   val.name,\n        type:    'checkbox',\n        click:   menuItem => this.contextClick(menuItem),\n        checked: (val.name === curr),\n      }));\n    }\n  }\n\n  /**\n   * Call back when a menu item is clicked to change the active Kubernetes context.\n   * @param {Electron.MenuItem} menuItem The menu item that was clicked.\n   */\n  protected contextClick(menuItem: Electron.MenuItem) {\n    kubeconfig.setCurrentContext(menuItem.label, () => {\n      this.updateContexts();\n      const contextMenu = Electron.Menu.buildFromTemplate(this.contextMenuItems);\n\n      this.trayMenu.setContextMenu(contextMenu);\n    });\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/update/LonghornProvider.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport { URL } from 'url';\nimport util from 'util';\n\nimport { newError, CustomPublishOptions } from 'builder-util-runtime';\nimport Electron, { net } from 'electron';\nimport { AppUpdater, Provider, ResolvedUpdateFileInfo, UpdateInfo } from 'electron-updater';\nimport { ProviderRuntimeOptions, ProviderPlatform } from 'electron-updater/out/providers/Provider';\nimport semver from 'semver';\n\nimport Logging from '@pkg/utils/logging';\nimport { getMacOsVersion } from '@pkg/utils/osVersion';\nimport paths from '@pkg/utils/paths';\nimport getWSLVersion from '@pkg/utils/wslVersion';\n\nconst console = Logging.update;\nconst gCachePath = path.join(paths.cache, 'updater-longhorn.json');\n\n/**\n * LonghornProviderOptions specifies the options available for LonghornProvider.\n */\nexport interface LonghornProviderOptions extends CustomPublishOptions {\n  /**\n   * upgradeServer is the URL for the Upgrade Responder server\n   * @example \"https://responder.example.com:8314/v1/checkupgrade\"\n   */\n  readonly upgradeServer: string;\n\n  /**\n   * The GitHub owner / organization.  Should be detected during packaging.\n   */\n  readonly owner: string;\n\n  /**\n   * The GitHub repository name.  Should be detected during packaging.\n   */\n  readonly repo: string;\n\n  /**\n   * Whether to use `v`-prefixed tag name.\n   * @default true\n   */\n  readonly vPrefixedTagName?: boolean\n}\n\n/** LonghornUpdateInfo is an UpdateInfo with additional fields for custom use. */\nexport interface LonghornUpdateInfo extends UpdateInfo {\n  /**\n   * The minimum time (milliseconds since Unix epoch) we should next check for\n   * an update.\n   */\n  nextUpdateTime:             number;\n  /**\n   * Whether there is an unsupported version of Rancher Desktop that is\n   * newer than the latest supported version.\n   */\n  unsupportedUpdateAvailable: boolean;\n}\n\n/**\n * LonghornUpgraderResponse describes the response from the Longhorn Upgrade\n * Responder service.\n */\ninterface LonghornUpgraderResponse {\n  versions:                 UpgradeResponderVersion[];\n  /**\n   * The number of minutes before the next update check should be performed.\n   */\n  requestIntervalInMinutes: number;\n}\n\ninterface UpgradeResponderVersion {\n  Name:        string;\n  ReleaseDate: Date;\n  Supported?:  boolean;\n  Tags:        string[];\n}\n\ninterface UpgradeResponderQueryResult {\n  latest:                     UpgradeResponderVersion;\n  requestIntervalInMinutes:   number,\n  unsupportedUpdateAvailable: boolean,\n}\n\nexport interface UpgradeResponderRequestPayload {\n  appVersion: string;\n  extraInfo: {\n    platform:        string;\n    platformVersion: string;\n    wslVersion?:     string,\n  },\n}\n\nexport interface GitHubReleaseAsset {\n  url: string;\n\n  browser_download_url: string;\n  id:                   number;\n  name:                 string;\n  label:                string;\n  size:                 number;\n}\n\n/**\n * GitHubReleaseInfo describes the API response from GitHub for fetching one\n * release.\n */\ninterface GitHubReleaseInfo {\n  url: string;\n  id:  number;\n\n  tag_name:   string;\n  name:       string;\n  body:       string;\n  draft:      boolean;\n  prerelease: boolean;\n\n  published_at: string;\n  assets:       GitHubReleaseAsset[];\n}\n\n/**\n * LonghornCache contains the information we keep in the update cache file.\n * Note that this will only contain information relevant for the current\n * platform.\n */\ninterface LonghornCache {\n  /** The minimum time (in Unix epoch) we should next check for an update. */\n  nextUpdateTime:             number;\n  /**\n   * Whether there is an unsupported version of Rancher Desktop that is\n   * newer than the latest supported version.\n   */\n  unsupportedUpdateAvailable: boolean;\n  /** Whether the recorded release is an installable update */\n  isInstallable:              boolean;\n  release: {\n    /** Release tag, typically in the form \"v1.2.3\". */\n    tag:   string;\n    /** The name of the release, typically the same as the tag. */\n    name:  string;\n    /** Release notes, in GitHub-flavoured markdown. */\n    notes: string;\n    /** The release date of the next release. */\n    date:  string;\n  },\n  file: {\n    /** URL to download the release. */\n    url:      string;\n    /** File size of the release. */\n    size:     number;\n    /** Checksum of the release. */\n    checksum: string;\n  }\n}\n\nexport async function hasQueuedUpdate(): Promise<boolean> {\n  try {\n    const rawCache = await fs.promises.readFile(gCachePath, 'utf-8');\n    const cache: LonghornCache = JSON.parse(rawCache);\n\n    if (!cache.isInstallable) {\n      return false;\n    }\n\n    // The isInstallable flag isn't going to get clear _right_ after an update;\n    // in which case, we need to check that the release is newer than the\n    // current version.\n    const currentVersion = semver.parse(Electron.app.getVersion(), { loose: true });\n    const stagedVersion = semver.parse(cache.release.tag, { loose: true });\n\n    if (!currentVersion || !stagedVersion) {\n      console.log(`Error parsing staged versions: ${ currentVersion ?? '<none>' } -> ${ stagedVersion ?? '<none>' }`);\n\n      return false;\n    }\n    if (semver.gte(currentVersion, stagedVersion)) {\n      console.log(`Staged version ${ stagedVersion } not greater than current version ${ currentVersion }, skipping.`);\n\n      return false;\n    }\n    console.debug(`Performing update from ${ currentVersion } to ${ stagedVersion }...`);\n\n    return true;\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n      console.error('Could not check for queued update:', error);\n    }\n  }\n\n  return false;\n}\n\nexport async function setHasQueuedUpdate(isQueued: boolean): Promise<void> {\n  try {\n    const rawCache = await fs.promises.readFile(gCachePath, 'utf-8');\n    const cache: LonghornCache = JSON.parse(rawCache);\n\n    cache.isInstallable = isQueued;\n    await fs.promises.writeFile(gCachePath, JSON.stringify(cache),\n      { encoding: 'utf-8', mode: 0o600 });\n  } catch (error) {\n    if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n      console.error('Could not check for queued update:', error);\n    }\n  }\n}\n\n/**\n * Return the OS version of whatever platform we are running on.\n * Note that this is *not* the kernel version; it is the OS version,\n * i.e. the version of the entire package, including kernel,\n * userspace programs, base configuration, etc.\n */\nfunction getPlatformVersion(): string {\n  switch (process.platform) {\n  case 'win32':\n    return os.release();\n  case 'darwin': {\n    return getMacOsVersion().toString();\n  }\n  case 'linux':\n    // OS version is hard to get on Linux and could be in many different\n    // formats. We hard-code it to 0.0.0 so that Upgrade Responder can\n    // parse it into an InstanceInfo. Nevertheless, automatic updates\n    // are not supported on Linux as of the time of writing, so this is\n    // just in case we want to introduce rules for Linux that don't have\n    // to do with platform version in the future.\n    return '0.0.0';\n  }\n  throw new Error(`Platform \"${ process.platform }\" is not supported`);\n}\n\n/**\n * Get the installed WSL version as a string.  If the inbox version of WSL is\n * installed (rather than the store version), we just hard code \"1.0.0\" instead.\n * @note This function should never throw;\n */\nexport async function getWslVersionString(): Promise<string | undefined> {\n  try {\n    const { installed, inbox, version } = await getWSLVersion();\n\n    if (!installed) {\n      return;\n    }\n    if (inbox) {\n      return '1.0.0';\n    }\n\n    return `${ version.major }.${ version.minor }.${ version.revision }.${ version.build }`;\n  } catch (ex) {\n    console.error('Failed to get WSL version:', ex);\n  }\n}\n\n/**\n * Fetch info on available versions of Rancher Desktop, as well as other\n * things, from the Upgrade Responder server.\n */\nexport async function queryUpgradeResponder(url: string, currentVersion: semver.SemVer): Promise<UpgradeResponderQueryResult> {\n  const requestPayload: UpgradeResponderRequestPayload = {\n    appVersion: currentVersion.toString(),\n    extraInfo:  {\n      platform:        `${ process.platform }-${ os.arch() }`,\n      platformVersion: getPlatformVersion(),\n    },\n  };\n\n  if (process.platform === 'win32') {\n    const wslVersion = await getWslVersionString();\n\n    if (wslVersion) {\n      requestPayload.extraInfo.wslVersion = wslVersion;\n    }\n  }\n\n  // If we are using anything on `github.io` as the update server, we're\n  // trying to run a simplified test.  In that case, break the protocol and do\n  // a HTTP GET instead of the HTTP POST with data we should do for actual\n  // Longhorn Upgrade Responder servers.\n  const requestOptions = /^https?:\\/\\/[^/]+\\.github\\.io\\//.test(url)\n    ? { method: 'GET' }\n    : {\n      method: 'POST',\n      body:   JSON.stringify(requestPayload),\n    };\n\n  console.debug(`Checking ${ url } for updates`);\n  const responseRaw = await net.fetch(url, requestOptions);\n  const response = await responseRaw.json() as LonghornUpgraderResponse;\n\n  console.debug(`Upgrade Responder response:`, util.inspect(response, true, null));\n\n  const allVersions = response.versions;\n\n  // If Upgrade Responder does not send the Supported field,\n  // assume that the version is supported.\n  for (const version of allVersions) {\n    version.Supported ??= true;\n  }\n\n  allVersions.sort((version1, version2) => semver.rcompare(version1.Name, version2.Name));\n  const supportedVersions = allVersions.filter(version => version.Supported);\n\n  if (supportedVersions.length === 0) {\n    throw newError('Could not find latest version', 'ERR_UPDATER_LATEST_VERSION_NOT_FOUND');\n  }\n  const latest = supportedVersions[0];\n  const unsupportedUpdateAvailable = allVersions[0].Name !== latest.Name;\n\n  return {\n    latest,\n    requestIntervalInMinutes: response.requestIntervalInMinutes,\n    unsupportedUpdateAvailable,\n  };\n}\n\n/**\n * LonghornProvider is a Provider that interacts with Longhorn's\n * [Upgrade Responder](https://github.com/longhorn/upgrade-responder) server to\n * determine which versions are available. It assumes that the versions are\n * published as GitHub releases. It also assumes that all versions have assets\n * for all platforms (that is, it doesn't filter by platform on checking).\n *\n * Note that we do internal caching to avoid issues with being double-counted in\n * the stats.\n */\nexport default class LonghornProvider extends Provider<LonghornUpdateInfo> {\n  constructor(\n    private readonly configuration: CustomPublishOptions,\n    private readonly updater: AppUpdater,\n    runtimeOptions: ProviderRuntimeOptions,\n  ) {\n    super(runtimeOptions);\n    this.platform = runtimeOptions.platform;\n  }\n\n  private readonly platform: ProviderPlatform;\n\n  /**\n   * Fetch a checksum file and return the checksum; expects only one file per\n   * checksum file.\n   * @param checksumURL The URL to the file containing the checksum.\n   * @returns Base64-encoded checksum.\n   */\n  protected async getSha512Sum(checksumURL: string): Promise<string> {\n    const contents = await (await net.fetch(checksumURL)).text();\n    const buffer = Buffer.from(contents.split(/\\s+/)[0], 'hex');\n\n    return buffer.toString('base64');\n  }\n\n  /**\n   * Check for updates, possibly returning the cached information if it is still\n   * applicable.\n   */\n  protected async checkForUpdates(): Promise<LonghornCache> {\n    try {\n      const rawCache = await fs.promises.readFile(gCachePath, 'utf-8');\n      const cache: LonghornCache = JSON.parse(rawCache);\n\n      if (cache.nextUpdateTime > Date.now()) {\n        return cache;\n      }\n    } catch (error) {\n      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n        // Log the unexpected error, but keep going.\n        console.error('Error reading update cache:', error);\n      }\n    }\n\n    const queryResult = await queryUpgradeResponder(this.configuration.upgradeServer, this.updater.currentVersion);\n    const { latest, unsupportedUpdateAvailable } = queryResult;\n\n    // Trigger the next check no earlier than some early time in the next calendar day.\n    const nextRequestTime = new Date();\n    nextRequestTime.setDate(nextRequestTime.getDate() + 1);\n    nextRequestTime.setHours(\n      Math.floor(Math.random() * 2),\n      Math.floor(Math.random() * 60),\n      Math.floor(Math.random() * 60),\n      Math.floor(Math.random() * 1_000));\n\n    // Get release information from GitHub releases.\n    const { owner, repo, vPrefixedTagName } = this.configuration;\n    const tag = (vPrefixedTagName ? 'v' : '') + latest.Name.replace(/^v/, '');\n    const infoURL = `https://api.github.com/repos/${ owner }/${ repo }/releases/tags/${ tag }`;\n    const releaseInfoRaw = await net.fetch(infoURL,\n      { headers: { Accept: 'application/vnd.github.v3+json' } });\n    const releaseInfo = await releaseInfoRaw.json() as GitHubReleaseInfo;\n    const assetFilter: (asset: GitHubReleaseAsset) => boolean = (() => {\n      switch (this.platform) {\n      case 'darwin': {\n        const isArm64 = process.arch === 'arm64';\n        const suffix = isArm64 ? '-mac.aarch64.zip' : '-mac.x86_64.zip';\n\n        return (asset: GitHubReleaseAsset) => asset.name.endsWith(suffix);\n      }\n      case 'linux':\n        return (asset: GitHubReleaseAsset) => asset.name.endsWith('AppImage');\n      case 'win32': {\n        return (asset: GitHubReleaseAsset) => asset.name.endsWith('.msi');\n      }\n      }\n    })();\n    const wantedAsset = releaseInfo.assets.find(assetFilter);\n\n    if (!wantedAsset) {\n      console.log(`Rejecting release ${ releaseInfo.name } - could not find usable asset.`);\n      throw newError(\n        `Could not find suitable assets in release ${ releaseInfo.name }`,\n        'ERR_UPDATER_ASSET_NOT_FOUND',\n      );\n    }\n\n    const checksumAsset = releaseInfo.assets.find(asset => asset.name === `${ wantedAsset.name }.sha512sum`);\n\n    if (!checksumAsset) {\n      console.log(`Rejecting release ${ releaseInfo.name } - could not find checksum for ${ wantedAsset.name }`);\n      throw newError(\n        `Could not find checksum for asset ${ wantedAsset.name }`,\n        'ERR_UPDATER_ASSET_NOT_FOUND',\n      );\n    }\n\n    const cache: LonghornCache = {\n      nextUpdateTime: nextRequestTime.getTime(),\n      unsupportedUpdateAvailable,\n      isInstallable:  false, // Always false, we'll update this later.\n      release:        {\n        tag,\n        name:  releaseInfo.name,\n        notes: releaseInfo.body,\n        date:  releaseInfo.published_at,\n      },\n      file: {\n        url:      wantedAsset.browser_download_url,\n        size:     wantedAsset.size,\n        checksum: await this.getSha512Sum(checksumAsset.browser_download_url),\n      },\n    };\n\n    await fs.promises.writeFile(gCachePath, JSON.stringify(cache),\n      { encoding: 'utf-8', mode: 0o600 });\n\n    return cache;\n  }\n\n  async getLatestVersion(): Promise<LonghornUpdateInfo> {\n    const cache = await this.checkForUpdates();\n\n    return {\n      files: [{\n        url:                   cache.file.url,\n        size:                  cache.file.size,\n        sha512:                cache.file.checksum,\n        isAdminRightsRequired: false,\n      }],\n      version:                    cache.release.tag,\n      path:                       '',\n      sha512:                     '',\n      releaseName:                cache.release.name,\n      releaseNotes:               cache.release.notes,\n      releaseDate:                cache.release.date,\n      nextUpdateTime:             cache.nextUpdateTime,\n      unsupportedUpdateAvailable: cache.unsupportedUpdateAvailable,\n    };\n  }\n\n  resolveFiles(updateInfo: UpdateInfo): ResolvedUpdateFileInfo[] {\n    return updateInfo.files.map(file => ({\n      url:  new URL(file.url),\n      info: file,\n    }));\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/update/MSIUpdater.ts",
    "content": "import { spawn } from 'child_process';\nimport path from 'path';\n\nimport { AllPublishOptions, newError } from 'builder-util-runtime';\nimport { NsisUpdater } from 'electron-updater';\nimport { InstallOptions } from 'electron-updater/out/BaseUpdater';\nimport { ElectronHttpExecutor } from 'electron-updater/out/electronHttpExecutor';\nimport { findFile } from 'electron-updater/out/providers/Provider';\nimport { verifySignature } from 'electron-updater/out/windowsExecutableCodeSignatureVerifier';\nimport { Lazy } from 'lazy-val';\nimport * as reg from 'native-reg';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport paths from '@pkg/utils/paths';\n\nimport type { AppAdapter } from 'electron-updater/out/AppAdapter';\nimport type { DownloadUpdateOptions } from 'electron-updater/out/AppUpdater';\n\n/**\n * MsiUpdater implements updating for Rancher Desktop's MSI-based installer.\n */\n// We extend from NsisUpdater because extending BaseUpdater appears to cause\n// issues with AppImageUpdater (where it thinks BaseUpdater is undefined)?\nexport default class MsiUpdater extends NsisUpdater {\n  // eslint-disable-next-line no-useless-constructor -- This is used to change visibility\n  constructor(options?: AllPublishOptions | null, app?: AppAdapter) {\n    super(options, app);\n  }\n\n  // This implements an abstract method in BaseUpdater.\n  protected doDownloadUpdate(downloadUpdateOptions: DownloadUpdateOptions): Promise<string[]> {\n    const { info, provider } = downloadUpdateOptions.updateInfoAndProvider;\n    const fileInfo = findFile(provider.resolveFiles(info), 'msi');\n\n    if (!fileInfo) {\n      throw newError(`Could not find update information for MSI installer version ${ info.version }`,\n        'ERR_UPDATER_INVALID_UPDATE_INFO');\n    }\n\n    return this.executeDownload({\n      fileExtension: 'msi',\n      fileInfo,\n      downloadUpdateOptions,\n      task:          async(destinationFile, downloadOptions, packageFile, removeTempDirIfAny) => {\n        const httpExecutor: ElectronHttpExecutor = (this as any).httpExecutor;\n        const mergedDownloadOptions = {\n          ...downloadOptions,\n          sha512: fileInfo.packageInfo?.sha512 ?? downloadOptions.sha512,\n        };\n\n        this._logger.debug?.(`Downloading update for ${ info.version } from ${ fileInfo.url } (sha512: ${ mergedDownloadOptions.sha512 })`);\n        await httpExecutor.download(fileInfo.url, destinationFile, mergedDownloadOptions);\n        const signatureError = await this.verifySignature_(destinationFile);\n\n        if (signatureError) {\n          await removeTempDirIfAny();\n          throw newError(\n            `New version ${ info.version } (${ fileInfo.url }) is not signed correctly: ${ signatureError }`,\n            'ERR_UPDATER_INVALID_SIGNATURE');\n        }\n      },\n    });\n  }\n\n  // Verify that the given file is signed by the expected entity (as configured\n  // in electron-builder.yml).\n  private async verifySignature_(destinationFile: string): Promise<string | null> {\n    let publisherName: string | string[] | null;\n\n    try {\n      const configOnDisk: Lazy<any> = (this as any).configOnDisk;\n\n      publisherName = (await configOnDisk.value).publisherName;\n      if (!publisherName) {\n        return null;\n      }\n    } catch (e: any) {\n      if (e?.code === 'ENOENT') {\n        return null; // No updates configured\n      }\n      throw e;\n    }\n    const publisherNames = Array.isArray(publisherName) ? publisherName : [publisherName];\n\n    return await verifySignature(publisherNames, destinationFile, this._logger);\n  }\n\n  protected doInstall(options: InstallOptions): boolean {\n    const systemRoot = process.env.SystemRoot ?? 'C:\\\\Windows';\n    const msiexec = path.join(systemRoot, 'system32', 'msiexec.exe');\n    const installerPath = this.installerPath;\n\n    if (!installerPath) {\n      this._logger.error('doInstall() called without an installer path');\n      this.dispatchError(new Error(\"No valid update available, can't quit and install\"));\n\n      return false;\n    }\n\n    const args: string[] = [\n      '/norestart',\n      '/lv*', path.join(paths.logs, 'msiexec.log'),\n      '/i', installerPath,\n    ];\n    const elevate = options.isAdminRightsRequired || this.shouldElevate;\n\n    if (options.isSilent && !elevate) {\n      args.push('/quiet');\n    } else {\n      args.push('/passive');\n    }\n\n    args.push(`MSIINSTALLPERUSER=${ elevate ? '0' : '1' }`);\n\n    if (options.isForceRunAfter) {\n      args.push('RDRUNAFTERINSTALL=1');\n    }\n\n    this._logger.debug?.(`Will invoke installer on restart with: msiexec ${ args.join(' ') }`);\n    mainEvents.on('quit', () => {\n      this._logger.debug?.(`Running msiexec ${ args.join(' ') }`);\n      const proc = spawn(msiexec, args, {\n        detached: true, stdio: 'ignore', windowsHide: true,\n      });\n\n      proc.on('error', (err: NodeJS.ErrnoException) => {\n        this._logger.error(`Cannot run installer: error code: ${ err.code }`);\n        this.dispatchError(err);\n      });\n      proc.on('exit', (code, signal) => {\n        this._logger.debug?.(`msiexec exited with ${ code }/${ signal }`);\n      });\n      proc.unref();\n    });\n\n    return true;\n  }\n\n  /**\n   * shouldElevate indicates whether we need elevation to install the update.\n   */\n  protected get shouldElevate(): boolean {\n    let key: any = null;\n    let isAdmin = false;\n\n    try {\n      key = reg.openKey(reg.HKLM, 'SOFTWARE', reg.Access.READ);\n\n      if (key) {\n        const parsedValue = reg.getValue(key, 'SUSE\\\\RancherDesktop', 'AdminInstall');\n\n        isAdmin = parsedValue !== null;\n\n        return isAdmin;\n      } else {\n        this._logger.debug?.(`Failed to open registry key: HKEY_LOCAL_MACHINE\\SOFTWARE: ${ key }/${ isAdmin }`);\n      }\n    } catch (error) {\n      this._logger.error(`Error accessing registry: ${ error }`);\n    } finally {\n      reg.closeKey(key);\n    }\n\n    return isAdmin;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/main/update/__tests__/LonghornProvider.spec.ts",
    "content": "import { jest } from '@jest/globals';\nimport semver from 'semver';\n\nimport type { spawnFile as spawnFileType } from '@pkg/utils/childProcess';\nimport mockModules from '@pkg/utils/testUtils/mockModules';\nimport type getWSLVersionType from '@pkg/utils/wslVersion';\nimport type { WSLVersionInfo } from '@pkg/utils/wslVersion';\n\nimport type { queryUpgradeResponder as queryUpgradeResponderType, UpgradeResponderRequestPayload } from '../LonghornProvider';\n\nconst itWindows = process.platform === 'win32' ? it : it.skip;\nconst itUnix = process.platform !== 'win32' ? it : it.skip;\nconst describeWindows = process.platform === 'win32' ? describe : describe.skip;\nconst standardMockedVersion: WSLVersionInfo = {\n  installed:       true,\n  inbox:           false,\n  has_kernel:      true,\n  outdated_kernel: false,\n  version:         {\n    major:    1,\n    minor:    2,\n    revision: 5,\n    build:    0,\n  },\n  kernel_version: {\n    major:    5,\n    minor:    0,\n    revision: 13,\n    build:    0,\n  },\n};\n\nconst modules = mockModules({\n  '@pkg/utils/childProcess': { spawnFile: jest.fn<typeof spawnFileType>() },\n  '@pkg/utils/osVersion':    { getMacOsVersion: jest.fn(() => new semver.SemVer('12.0.0')) },\n  '@pkg/utils/wslVersion':   { default: jest.fn<typeof getWSLVersionType>() },\n  electron:                  {\n    net: {\n      // We only return a subset of the values, so we need a complicated type here.\n      fetch: jest.fn<(...args: Parameters<typeof fetch>) => Promise<Partial<Awaited<ReturnType<typeof fetch>>>>>(),\n    },\n  },\n});\n\ndescribe('queryUpgradeResponder', () => {\n  let queryUpgradeResponder: typeof queryUpgradeResponderType;\n\n  beforeAll(async() => {\n    ({ queryUpgradeResponder } = await import('../LonghornProvider'));\n  });\n  afterEach(() => {\n    modules['@pkg/utils/childProcess'].spawnFile.mockReset();\n    modules.electron.net.fetch.mockReset();\n  });\n\n  it('should return the latest version', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n          {\n            Name:        'v3.2.1',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n          {\n            Name:        'v2.1.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const result = await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n\n    expect(result.latest.Name).toEqual('v3.2.1');\n  });\n\n  it('should set unsupportedUpdateAvailable to true when a newer-than-latest version is unsupported', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n          {\n            Name:        'v3.2.1',\n            ReleaseDate: 'testreleasedate',\n            Supported:   false,\n            Tags:        [],\n          },\n          {\n            Name:        'v2.1.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const result = await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n\n    expect(result.unsupportedUpdateAvailable).toBe(true);\n    expect(result.latest.Name).toEqual('v2.1.3');\n  });\n\n  it('should set unsupportedUpdateAvailable to false when no newer-than-latest versions are unsupported', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n          {\n            Name:        'v3.2.1',\n            ReleaseDate: 'testreleasedate',\n            Supported:   true,\n            Tags:        [],\n          },\n          {\n            Name:        'v2.1.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   false,\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const result = await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n\n    expect(result.unsupportedUpdateAvailable).toBe(false);\n    expect(result.latest.Name).toEqual('v3.2.1');\n  });\n\n  it('should throw an error if no versions are supported', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   false,\n            Tags:        [],\n          },\n          {\n            Name:        'v3.2.1',\n            ReleaseDate: 'testreleasedate',\n            Supported:   false,\n            Tags:        [],\n          },\n          {\n            Name:        'v2.1.3',\n            ReleaseDate: 'testreleasedate',\n            Supported:   false,\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const result = queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n\n    await expect(result).rejects.toThrow('Could not find latest version');\n  });\n\n  it('should treat all versions as supported when server does not include Supported key', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n          {\n            Name:        'v3.2.1',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n          {\n            Name:        'v2.1.3',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const result = await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n\n    expect(result.unsupportedUpdateAvailable).toBe(false);\n    expect(result.latest.Name).toEqual('v3.2.1');\n  });\n\n  it('should format the current app version properly and include it in request to Upgrade Responder', async() => {\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    const appVersion = '1.2.3';\n\n    await queryUpgradeResponder('testurl', new semver.SemVer(appVersion));\n    expect(modules.electron.net.fetch.mock.calls.length).toBe(1);\n    const rawBody = modules.electron.net.fetch.mock.calls[0][1]?.body;\n\n    expect(typeof rawBody).toBe('string');\n    const body: UpgradeResponderRequestPayload = JSON.parse(rawBody as string);\n\n    expect(body.appVersion).toBe(appVersion);\n  });\n\n  describeWindows('when we can get WSL version', () => {\n    it('should include wslVersion when using store WSL', async() => {\n      modules['@pkg/utils/wslVersion'].default.mockResolvedValue(standardMockedVersion);\n      modules.electron.net.fetch.mockResolvedValueOnce({\n        json: () => Promise.resolve({\n          requestIntervalInMinutes: 100,\n          versions:                 [\n            {\n              Name:        'v1.2.3',\n              ReleaseDate: 'testreleasedate',\n              Tags:        [],\n            },\n          ],\n        }),\n      });\n      await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n      expect(modules.electron.net.fetch.mock.calls.length).toBe(1);\n      const rawBody = modules.electron.net.fetch.mock.calls[0][1]?.body;\n\n      expect(typeof rawBody).toBe('string');\n      const body: UpgradeResponderRequestPayload = JSON.parse(rawBody as string);\n\n      expect(body.extraInfo.wslVersion).toBe('1.2.5.0');\n    });\n    it('should include wslVersion when using inbox WSL', async() => {\n      modules['@pkg/utils/wslVersion'].default.mockResolvedValue({ ...standardMockedVersion, inbox: true });\n      modules.electron.net.fetch.mockResolvedValueOnce({\n        json: () => Promise.resolve({\n          requestIntervalInMinutes: 100,\n          versions:                 [\n            {\n              Name:        'v1.2.3',\n              ReleaseDate: 'testreleasedate',\n              Tags:        [],\n            },\n          ],\n        }),\n      });\n      await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n      expect(modules.electron.net.fetch.mock.calls.length).toBe(1);\n      const rawBody = modules.electron.net.fetch.mock.calls[0][1]?.body;\n\n      expect(typeof rawBody).toBe('string');\n      const body: UpgradeResponderRequestPayload = JSON.parse(rawBody as string);\n\n      expect(body.extraInfo.wslVersion).toBe('1.0.0');\n    });\n  });\n\n  itWindows('should not include wslVersion in request to Upgrade Responder when wsl --version is unsuccessful', async() => {\n    modules['@pkg/utils/wslVersion'].default.mockRejectedValue('test rejected value');\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n    expect(modules.electron.net.fetch.mock.calls.length).toBe(1);\n    const rawBody = modules.electron.net.fetch.mock.calls[0][1]?.body;\n\n    expect(typeof rawBody).toBe('string');\n    const body: UpgradeResponderRequestPayload = JSON.parse(rawBody as string);\n\n    expect(body.extraInfo.wslVersion).toBe(undefined);\n  });\n\n  itUnix('should not check wsl.exe --version or include wslVersion if not on Windows', async() => {\n    modules.electron.net.fetch.mockResolvedValueOnce({\n      json: () => Promise.resolve({\n        requestIntervalInMinutes: 100,\n        versions:                 [\n          {\n            Name:        'v1.2.3',\n            ReleaseDate: 'testreleasedate',\n            Tags:        [],\n          },\n        ],\n      }),\n    });\n    await queryUpgradeResponder('testurl', new semver.SemVer('v1.2.3'));\n    expect(modules['@pkg/utils/childProcess'].spawnFile.mock.calls.length).toBe(0);\n    expect(modules.electron.net.fetch.mock.calls.length).toBe(1);\n    const rawBody = modules.electron.net.fetch.mock.calls[0][1]?.body;\n\n    expect(typeof rawBody).toBe('string');\n    const body: UpgradeResponderRequestPayload = JSON.parse(rawBody as string);\n\n    expect(body.extraInfo.wslVersion).toBe(undefined);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/main/update/index.ts",
    "content": "/**\n * This module contains code for handling auto-updates.\n */\n\nimport fs from 'fs';\nimport os from 'os';\nimport timers from 'timers';\n\nimport { CustomPublishOptions } from 'builder-util-runtime';\nimport Electron from 'electron';\nimport {\n  AppImageUpdater, MacUpdater, AppUpdater, ProgressInfo, UpdateInfo,\n} from 'electron-updater';\nimport { ElectronAppAdapter } from 'electron-updater/out/ElectronAppAdapter';\nimport yaml from 'yaml';\n\nimport LonghornProvider, { hasQueuedUpdate, LonghornUpdateInfo, setHasQueuedUpdate } from './LonghornProvider';\nimport MsiUpdater from './MSIUpdater';\n\nimport { Settings } from '@pkg/config/settings';\nimport mainEvent from '@pkg/main/mainEvents';\nimport Logging from '@pkg/utils/logging';\nimport * as window from '@pkg/window';\n\nconst console = Logging.update;\n\n/** State describes how for into start up we are. */\nenum State {\n  /** Startup hasn't been attempted yet. */\n  UNCONFIGURED,\n  /** No update configuration; updates are not available. */\n  NO_CONFIGURATION,\n  /** Updater has been configured, but no checks have been triggered. */\n  CONFIGURED,\n  /** We have triggered at least one update check. */\n  CHECKED,\n  /** An update is being downloaded; suppress checks. */\n  DOWNLOADING,\n  /** An update is pending; we should not check again. */\n  UPDATE_PENDING,\n  /** An error has occurred configuring the updater. */\n  ERROR,\n}\nlet state: State = State.UNCONFIGURED;\n\nlet autoUpdater: AppUpdater;\n/** When we should check for updates next. */\nlet updateTimer: NodeJS.Timeout;\n/** The update interval reported by the server. */\nlet updateInterval = 0;\n\nexport interface UpdateState {\n  configured: boolean;\n  available:  boolean;\n  downloaded: boolean;\n  error?:     Error;\n  info?:      LonghornUpdateInfo;\n  progress?:  ProgressInfo;\n}\nconst updateState: UpdateState = {\n  configured: false, available: false, downloaded: false,\n};\n\nElectron.ipcMain.on('update-state', () => {\n  window.send('update-state', updateState);\n});\n\nElectron.ipcMain.on('update-apply', () => {\n  if (!autoUpdater || process.env.RD_FORCE_UPDATES_ENABLED) {\n    return;\n  }\n  autoUpdater.quitAndInstall();\n});\n\nfunction isLonghornUpdateInfo(info: UpdateInfo | LonghornUpdateInfo): info is LonghornUpdateInfo {\n  return (info as LonghornUpdateInfo).nextUpdateTime !== undefined;\n}\n\n/**\n * Return a new AppUpdater; if no update configuration is available, returns\n * undefined.\n */\nasync function getUpdater(): Promise<AppUpdater | undefined> {\n  let updater: AppUpdater;\n\n  try {\n    const { appUpdateConfigPath } = new ElectronAppAdapter();\n    let fileContents : string;\n\n    try {\n      fileContents = await fs.promises.readFile(appUpdateConfigPath, { encoding: 'utf8' });\n    } catch (ex) {\n      if ((ex as NodeJS.ErrnoException).code === 'ENOENT') {\n        console.debug(`No update configuration found in ${ appUpdateConfigPath }`);\n\n        return undefined;\n      }\n      throw ex;\n    }\n    const options: CustomPublishOptions = yaml.parse(fileContents);\n\n    options.updateProvider = LonghornProvider;\n\n    if (process.env.RD_UPGRADE_RESPONDER_URL) {\n      console.log(`using custom upgrade responder URL ${ process.env.RD_UPGRADE_RESPONDER_URL }`);\n      options.upgradeServer = process.env.RD_UPGRADE_RESPONDER_URL;\n    }\n\n    switch (os.platform()) {\n    case 'win32': {\n      updater = new MsiUpdater(options);\n      break;\n    }\n    case 'darwin':\n      updater = new MacUpdater(options);\n      break;\n    case 'linux':\n      updater = new AppImageUpdater(options);\n      break;\n    default:\n      throw new Error(`Don't know how to create updater for platform ${ os.platform() }`);\n    }\n  } catch (e) {\n    console.error(e);\n    throw e;\n  }\n\n  if (process.env.RD_FORCE_UPDATES_ENABLED) {\n    updater.forceDevUpdateConfig = true;\n  }\n\n  updater.logger = console;\n  updater.autoDownload = true;\n  updater.autoInstallOnAppQuit = false;\n  updater.on('error', (error) => {\n    console.debug('update: error:', error);\n    updateState.error = error;\n    updateState.downloaded = false;\n    window.send('update-state', updateState);\n  });\n  updater.on('checking-for-update', () => {\n    console.debug('update: checking for update');\n    updateState.available = false;\n    updateState.downloaded = false;\n    setHasQueuedUpdate(false);\n  });\n  updater.on('update-available', (info) => {\n    if (!isLonghornUpdateInfo(info)) {\n      throw new Error('updater: event update-available: info is not of type LonghornUpdateInfo');\n    }\n    console.debug('update: update available:', info);\n    updateState.available = true;\n    updateState.info = info;\n    updateState.downloaded = state === State.UPDATE_PENDING;\n    window.send('update-state', updateState);\n  });\n  updater.on('update-not-available', (info) => {\n    if (!isLonghornUpdateInfo(info)) {\n      throw new Error('updater: event update-not-available: info is not of type LonghornUpdateInfo');\n    }\n    console.debug('update: not available:', info);\n    updateState.available = false;\n    updateState.info = info;\n    updateState.downloaded = false;\n    setHasQueuedUpdate(false);\n    window.send('update-state', updateState);\n  });\n  updater.on('download-progress', (progress) => {\n    if (state === State.CHECKED || state === State.UPDATE_PENDING) {\n      state = State.DOWNLOADING;\n    }\n    updateState.progress = progress;\n    updateState.downloaded = false;\n    window.send('update-state', updateState);\n  });\n  updater.on('update-downloaded', (info) => {\n    if (!isLonghornUpdateInfo(info)) {\n      throw new Error('updater: event update-downloaded: info is not of type LonghornUpdateInfo');\n    }\n    if (state === State.DOWNLOADING) {\n      state = State.UPDATE_PENDING;\n    }\n    console.debug('update: downloaded:', info);\n    updateState.info = info;\n    updateState.downloaded = true;\n    // Prevent the updater from downloading the update again; it will clobber\n    // the existing download.\n    updater.autoDownload = false;\n    setHasQueuedUpdate(true);\n    window.send('update-state', updateState);\n  });\n\n  return updater;\n}\n\nmainEvent.on('settings-update', (settings: Settings) => {\n  if (settings.application.updater.enabled && state === State.CONFIGURED) {\n    // We have a configured updater, but haven't done the actual check yet.\n    // This means the setting was disabled when we configured the updater.\n    // Start checking now.\n    doInitialUpdateCheck();\n  }\n});\n\n/**\n * Set up the updater, and possibly run the updater if it has already been\n * downloaded and is ready to install.\n *\n * @param enabled Whether updates are enabled\n * @param doInstall Install updates if available.\n * @returns Whether the update is being installed.\n */\nexport default async function setupUpdate(enabled: boolean, doInstall = false): Promise<boolean> {\n  console.debug(`Setting up updater... enabled=${ enabled } doInstall=${ doInstall }`);\n  if (state === State.UNCONFIGURED) {\n    try {\n      const newUpdater = await getUpdater();\n\n      if (!newUpdater?.isUpdaterActive()) {\n        console.debug(`No update configuration found.`);\n        state = State.NO_CONFIGURATION;\n\n        return false;\n      }\n      autoUpdater = newUpdater;\n    } catch (ex) {\n      state = State.ERROR;\n      throw ex;\n    }\n  }\n  updateState.configured = true;\n  window.send('update-state', updateState);\n  state = State.CONFIGURED;\n\n  if (!enabled) {\n    return false;\n  }\n\n  try {\n    const result = await doInitialUpdateCheck(doInstall);\n\n    state = State.CHECKED;\n\n    return result;\n  } catch (ex) {\n    // If the initial update check fails, don't prevent application startup.\n    state = State.ERROR;\n    console.error(`Error setting up updater:`, ex);\n\n    return false;\n  }\n}\n\n/**\n * Do the initial update check.\n * @precondition autoUpdater has been set up.\n * @param doInstall If true, install the update immediately.\n * @returns Whether the update is being installed.\n */\nasync function doInitialUpdateCheck(doInstall = false): Promise<boolean> {\n  if (doInstall && await hasQueuedUpdate() && !process.env.RD_FORCE_UPDATES_ENABLED) {\n    console.log('Update is cached; forcing re-check to install.');\n\n    return await new Promise((resolve) => {\n      let hasError = false;\n\n      autoUpdater.once('error', (e) => {\n        console.error('Updater got error', e);\n        hasError = true;\n      });\n      autoUpdater.once('update-downloaded', () => {\n        console.log('Update download complete; restarting app');\n        setHasQueuedUpdate(true);\n        autoUpdater.quitAndInstall(true, true);\n        console.log(`Install complete, result: ${ !hasError }`);\n        resolve(!hasError);\n      });\n      autoUpdater.checkForUpdates();\n    });\n  }\n\n  triggerUpdateCheck();\n\n  return false;\n}\n\n/**\n * Trigger an update check, and set up the timer to re-check again later.\n */\nasync function triggerUpdateCheck() {\n  if (state !== State.DOWNLOADING) {\n    const result = await autoUpdater.checkForUpdates();\n\n    if (!result) {\n      // App update is disabled (likely because the app is not packaged).\n      return;\n    }\n\n    if (!isLonghornUpdateInfo(result.updateInfo)) {\n      throw new Error('result.updateInfo is not of type LonghornUpdateInfo');\n    }\n    const updateInfo = result.updateInfo;\n    const givenTimeDelta = (updateInfo.nextUpdateTime || 0) - Date.now();\n\n    // Enforce at least one minute between checks, even if the server is reporting\n    // bad times.\n    updateInterval = Math.max(givenTimeDelta, 60_000);\n  }\n\n  // regardless of whether we actually made the check, schedule the next check.\n\n  if (updateTimer) {\n    timers.clearTimeout(updateTimer);\n  }\n  updateTimer = timers.setTimeout(triggerUpdateCheck, updateInterval);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/middleware/i18n.js",
    "content": "export default async function({\n  isHMR, app, store, route, params, error, redirect,\n}) {\n  // If middleware is called from hot module replacement, ignore it\n  if (isHMR) {\n    return;\n  }\n\n  await store.dispatch('i18n/init');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/middleware/indexRedirect.js",
    "content": "/**\n * This middleware redirects / to /General\n */\nexport default ({ route, next, redirect }) => {\n  switch (route.path) {\n  case process.env.RD_ENV_PLUGINS_DEV:\n    next();\n    break;\n  case '/':\n    redirect(301, '/General');\n    break;\n  default:\n    next();\n  }\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/mixins/compact-input.ts",
    "content": "export default {\n  props: {\n    compact: {\n      type:    Boolean,\n      default: null,\n    },\n    label: {\n      type:    String,\n      default: null,\n    },\n\n    labelKey: {\n      type:    String,\n      default: null,\n    },\n  },\n\n  computed: {\n    isCompact(): boolean {\n      // Compact if explicitly set - otherwise compact if there is no label\n      return this.compact !== null ? this.compact : !(this.label || this.labelKey);\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/mixins/labeled-form-element.ts",
    "content": "import { _EDIT, _VIEW } from '@pkg/config/query-params';\nimport { getWidth, setWidth } from '@pkg/utils/width';\n\ninterface LabeledFormElement {\n  raised:  boolean;\n  focused: boolean;\n  blurred: number | null;\n}\n\nexport default {\n  inheritAttrs: false,\n\n  props: {\n    mode: {\n      type:    String,\n      default: _EDIT,\n    },\n\n    label: {\n      type:    String,\n      default: null,\n    },\n\n    labelKey: {\n      type:    String,\n      default: null,\n    },\n\n    placeholderKey: {\n      type:    String,\n      default: null,\n    },\n\n    tooltip: {\n      type:    [String, Object],\n      default: null,\n    },\n\n    hoverTooltip: {\n      type:    Boolean,\n      default: true,\n    },\n\n    tooltipKey: {\n      type:    String,\n      default: null,\n    },\n\n    required: {\n      type:    Boolean,\n      default: false,\n    },\n\n    disabled: {\n      type:    Boolean,\n      default: false,\n    },\n\n    placeholder: {\n      type:    [String, Number],\n      default: '',\n    },\n\n    value: {\n      type:    [String, Number, Object],\n      default: '',\n    },\n\n    options: {\n      default: null,\n      type:    Array,\n    },\n\n    searchable: {\n      default: false,\n      type:    Boolean,\n    },\n\n    filterable: {\n      default: true,\n      type:    Boolean,\n    },\n\n    rules: {\n      default:   () => [],\n      type:      Array,\n      // we only want functions in the rules array\n      validator: (rules: any) => rules.every((rule: any) => ['function'].includes(typeof rule)),\n    },\n\n    requireDirty: {\n      default: true,\n      type:    Boolean,\n    },\n  },\n\n  data(): LabeledFormElement {\n    return {\n      raised:  this.mode === _VIEW || !!`${ this.value }`,\n      focused: false,\n      blurred: null,\n    };\n  },\n\n  computed: {\n    requiredField(): boolean {\n      // using \"any\" for a type on \"rule\" here is dirty but the use of the optional chaining operator makes it safe for what we're doing here.\n      return (this.required || this.rules.some((rule: any): boolean => rule?.name === 'required'));\n    },\n    empty(): boolean {\n      return !!`${ this.value }`;\n    },\n\n    isView(): boolean {\n      return this.mode === _VIEW;\n    },\n\n    isDisabled(): boolean {\n      return this.disabled || this.isView;\n    },\n\n    isSearchable(): boolean {\n      const { searchable, canPaginate } = this as any; // This will be resolved when we migrate from mixin\n\n      if (canPaginate) {\n        return true;\n      }\n      const options = ( this.options || [] );\n\n      if (searchable || options.length >= 10) {\n        return true;\n      }\n\n      return false;\n    },\n\n    isFilterable(): boolean {\n      const { filterable, canPaginate } = this as any; // This will be resolved when we migrate from mixin\n\n      if (canPaginate) {\n        return false;\n      }\n\n      return filterable;\n    },\n\n    validationMessage(): string | undefined {\n      // we want to grab the required rule passed in if we can but if it's not there then we can just grab it from the formRulesGenerator\n      const requiredRule = this.rules.find((rule: any) => rule?.name === 'required') as Function;\n      const ruleMessages = [];\n      const value = this?.value;\n\n      if (requiredRule && this.blurred && !this.focused) {\n        const message = requiredRule(value);\n\n        if (message) {\n          this.$emit('update:validation', false);\n\n          return message;\n        }\n      }\n\n      for (const rule of this.rules as Function[]) {\n        const message = rule(value);\n\n        if (!!message && rule.name !== 'required') { // we're catching 'required' above so we can ignore it here\n          ruleMessages.push(message);\n        }\n      }\n      if (ruleMessages.length > 0 && (this.blurred || this.focused || !this.requireDirty)) {\n        this.$emit('update:validation', false);\n\n        return ruleMessages.join(', ');\n      } else {\n        this.$emit('update:validation', true);\n\n        return undefined;\n      }\n    },\n  },\n\n  methods: {\n    resizeHandler() {\n      // since the DD is positioned there is no way to 'inherit' the size of the input, this calcs the size of the parent and set the dd width if it is smaller. If not let it grow with the regular styles\n      this.$nextTick(() => {\n        const DD = (this.$refs.select as HTMLElement).querySelector('ul.vs__dropdown-menu');\n\n        const selectWidth = getWidth(this.$refs.select as Element) || 0;\n        const dropWidth = getWidth(DD!) || 0;\n\n        if (dropWidth < selectWidth) {\n          setWidth(DD!, selectWidth);\n        }\n      });\n    },\n    onFocus() {\n      this.$emit('on-focus');\n\n      return this.onFocusLabeled();\n    },\n\n    onFocusLabeled() {\n      this.raised = true;\n      this.focused = true;\n    },\n\n    onBlur() {\n      this.$emit('on-blur');\n\n      return this.onBlurLabeled();\n    },\n\n    onBlurLabeled() {\n      this.focused = false;\n\n      if ( !this.value ) {\n        this.raised = false;\n      }\n\n      this.blurred = Date.now();\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/mixins/vue-select-overrides.js",
    "content": "export default {\n  methods: {\n    mappedKeys(map, vm) {\n      // Defaults found at - https://github.com/sagalbot/vue-select/blob/master/src/components/Select.vue#L947\n      const out = { ...map };\n\n      // tab\n      (out[9] = (e) => {\n        // user esc'd\n        if (!vm.open) {\n          return;\n        }\n\n        e.preventDefault();\n\n        const optsLen = vm.filteredOptions.length;\n        const typeAheadPointer = vm.typeAheadPointer;\n\n        if (e.shiftKey) {\n          if (typeAheadPointer === 0) {\n            return vm.onEscape();\n          }\n\n          return vm.typeAheadUp();\n        }\n        if (typeAheadPointer + 1 === optsLen) {\n          return vm.onEscape();\n        }\n\n        return vm.typeAheadDown();\n      });\n\n      (out[27] = (e) => {\n        vm.open = false;\n        vm.search = '';\n\n        return false;\n      });\n\n      (out[13] = (e, opt) => {\n        if (!vm.open) {\n          vm.open = true;\n\n          return;\n        }\n\n        let option = vm.filteredOptions[vm.typeAheadPointer];\n\n        vm.$emit('option:selecting', option);\n\n        if (!vm.isOptionSelected(option)) {\n          if (vm.taggable && !vm.optionExists(option)) {\n            vm.$emit('option:created', option);\n          }\n          if (vm.multiple) {\n            option = vm.selectedValue.concat(option);\n          }\n          vm.updateValue(option);\n          vm.$emit('option:selected', option);\n\n          if (vm.closeOnSelect) {\n            vm.open = false;\n            vm.typeAheadPointer = -1;\n          }\n\n          if (vm.clearSearchOnSelect) {\n            vm.search = '';\n          }\n        }\n      });\n\n      //  up.prevent\n      (out[38] = (e) => {\n        e.preventDefault();\n\n        if (!vm.open) {\n          vm.open = true;\n        }\n\n        return vm.typeAheadUp();\n      });\n\n      //  down.prevent\n      (out[40] = (e) => {\n        e.preventDefault();\n\n        if (!vm.open) {\n          vm.open = true;\n        }\n\n        return vm.typeAheadDown();\n      });\n\n      return out;\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Containers.vue",
    "content": "<template>\n  <div class=\"containers\">\n    <banner\n      v-if=\"errorMessage\"\n      color=\"error\"\n      @close=\"clearError\"\n    >\n      {{ errorMessage }}\n    </banner>\n    <SortableTable\n      ref=\"sortableTableRef\"\n      class=\"containersTable\"\n      :headers=\"headers\"\n      key-field=\"id\"\n      :rows=\"rows\"\n      no-rows-key=\"containers.sortableTables.noRows\"\n      :row-actions=\"true\"\n      :paging=\"true\"\n      :rows-per-page=\"10\"\n      :has-advanced-filtering=\"false\"\n      :loading=\"containers === null\"\n      group-by=\"projectGroup\"\n      :group-sort=\"['projectGroup']\"\n    >\n      <template #header-middle>\n        <div class=\"header-middle\">\n          <div v-if=\"supportsNamespaces\">\n            <label>Namespace</label>\n            <select\n              class=\"select-namespace\"\n              :value=\"namespace\"\n              @change=\"onChangeNamespace($event)\"\n            >\n              <option\n                v-for=\"item in namespaces\"\n                :key=\"item\"\n                :value=\"item\"\n                :selected=\"item === namespace\"\n              >\n                {{ item }}\n              </option>\n            </select>\n          </div>\n        </div>\n      </template>\n      <template #col:containerState=\"{ row }\">\n        <td>\n          <badge-state\n            :color=\"isRunning(row) ? 'bg-success' : 'bg-darker'\"\n            :label=\"row.state\"\n          />\n        </td>\n      </template>\n      <template #col:imageName=\"{ row }\">\n        <td>\n          <span v-tooltip=\"getTooltipConfig(row.imageName)\">\n            {{ shortSha(row.imageName) }}\n          </span>\n        </td>\n      </template>\n      <template #col:containerName=\"{ row }\">\n        <td>\n          <a\n            v-tooltip=\"getTooltipConfig(row.containerName)\"\n            class=\"container-name-link\"\n            @click.stop.prevent=\"viewInfo(row)\"\n          >\n            {{ shortSha(row.containerName) }}\n          </a>\n        </td>\n      </template>\n      <template #col:ports=\"{ row }\">\n        <td>\n          <div class=\"port-container\">\n            <a\n              v-for=\"[hostPort, containerPort] in row.portList.slice(0, 2)\"\n              :key=\"hostPort\"\n              target=\"_blank\"\n              class=\"link\"\n              @click=\"openUrl(hostPort)\"\n            >\n              {{ hostPort }}:{{ containerPort }}\n            </a>\n\n            <div\n              v-if=\"shouldHaveDropdown(row.portList)\"\n              class=\"dropdown\"\n              @mouseenter=\"addDropDownPosition\"\n              @mouseleave=\"clearDropDownPosition\"\n            >\n              <span>\n                ...\n              </span>\n              <div class=\"dropdown-content\">\n                <a\n                  v-for=\"[hostPort, containerPort] in row.portList.slice(2)\"\n                  :key=\"hostPort\"\n                  target=\"_blank\"\n                  class=\"link\"\n                  @click=\"openUrl(hostPort)\"\n                >\n                  {{ hostPort }}:{{ containerPort }}\n                </a>\n              </div>\n            </div>\n          </div>\n        </td>\n      </template>\n      <template #group-row=\"{ group }\">\n        <tr\n          class=\"group-row\"\n          :aria-expanded=\"!collapsed[group.ref]\"\n        >\n          <td :colspan=\"headers.length + 1\">\n            <div class=\"group-tab\">\n              <i\n                data-title=\"Toggle Expand\"\n                :class=\"{\n                  icon: true,\n                  'icon-chevron-right': !!collapsed[group.ref],\n                  'icon-chevron-down': !collapsed[group.ref],\n                }\"\n                @click.stop=\"toggleExpand(group.ref)\"\n              />\n              {{ group.ref }}\n              <span v-if=\"!!collapsed[group.ref]\"> ({{ group.rows.length }})</span>\n            </div>\n          </td>\n        </tr>\n      </template>\n    </SortableTable>\n  </div>\n</template>\n\n<script>\nimport { BadgeState, Banner } from '@rancher/components';\nimport dayjs from 'dayjs';\nimport { shell } from 'electron';\nimport merge from 'lodash/merge';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport SortableTable from '@pkg/components/SortableTable';\nimport { mapTypedGetters, mapTypedState } from '@pkg/entry/store';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\n/**\n * @import { Container } from '@pk/store/containers'\n */\n\n/**\n * @typedef { Object } Action\n * @property { string } label\n * @property { string } action\n * @property { boolean } enabled\n * @property { boolean } bulkable\n * @property { boolean } [bulkAction]\n */\n\n/**\n * @typedef { Container } RowItem An item in the table row\n * @property { Action[] } [availableActions]\n * @property { (this: Container, containers?: Container[]) => void } [stopContainer]\n * @property { (this: Container, containers?: Container[]) => void } [startContainer]\n * @property { (this: Container, containers?: Container[]) => void } [deleteContainer]\n * @property { (this: Container) => void } [viewInfo]\n * @property { (readonly [number, number])[] } portList\n */\n\nexport default defineComponent({\n  name:       'Containers',\n  title:      'Containers',\n  components: { SortableTable, BadgeState, Banner },\n  data() {\n    return {\n      /** @type import('@pkg/config/settings').Settings | undefined */\n      settings:                   undefined,\n      /** @type string | null */\n      execError:                  null,\n      /** @type Record<string, boolean> */\n      collapsed:                   {},\n      /**\n       * This timer is used to retry subscribing to events until the backend is\n       * ready enough to update the vuex store.\n       * @type ReturnType<typeof setTimeout> | undefined\n       */\n      subscribeTimer:       undefined,\n      headers:              [\n        {\n          name:  'containerState',\n          label: this.t('containers.manage.table.header.state'),\n        },\n        {\n          name:  'containerName',\n          label: this.t('containers.manage.table.header.containerName'),\n          sort:  ['containerName', 'image', 'imageName'],\n        },\n        {\n          name:  'imageName',\n          label: this.t('containers.manage.table.header.image'),\n          sort:  ['imageName', 'containerName', 'imageName'],\n        },\n        {\n          name:  'ports',\n          label: this.t('containers.manage.table.header.ports'),\n          sort:  ['ports', 'containerName', 'imageName'],\n        },\n        {\n          name:  'uptime',\n          label: this.t('containers.manage.table.header.started'),\n          sort:  ['si', 'containerName', 'imageName'],\n          width: 120,\n        },\n      ],\n    };\n  },\n  computed: {\n    ...mapGetters('k8sManager', { isK8sReady: 'isReady' }),\n    ...mapTypedState('container-engine', ['containers', 'namespaces']),\n    ...mapTypedGetters('container-engine', ['namespace', 'supportsNamespaces', 'error']),\n    /** @returns {RowItem[]} */\n    rows() {\n      if (!this.containers) {\n        return [];\n      }\n      return Object.values(this.containers)\n        .filter(container => {\n          // Filter out containers from the 'kube-system' namespace\n          return this.supportsNamespaces || container.labels['io.kubernetes.pod.namespace'] !== 'kube-system';\n        })\n        .sort((a, b) => {\n          // Sort by status, showing running first.\n          if ((a.state === 'running' || b.state === 'running') && a.state !== b.state) {\n            // One of the two is running; put that first.\n            return a.state === 'running' ? -1 : 1;\n          }\n          // Both or running, or neither.\n          return a.state.localeCompare(b.state) || a.id.localeCompare(b.id);\n        })\n        .map(container => merge({}, container, {\n          uptime:           container.started && dayjs(container.started).toNow(true),\n          availableActions: [\n            {\n              label:      'Info',\n              action:     'viewInfo',\n              enabled:    true,\n              bulkable:   false,\n            },\n            {\n              label:      'Stop',\n              action:     'stopContainer',\n              enabled:    this.isRunning(container),\n              bulkable:   true,\n              bulkAction: 'stopContainer',\n            },\n            {\n              label:      'Start',\n              action:     'startContainer',\n              enabled:    this.isStopped(container),\n              bulkable:   true,\n              bulkAction: 'startContainer',\n            },\n            {\n              label:      this.t('images.manager.table.action.delete'),\n              action:     'deleteContainer',\n              enabled:    this.isStopped(container),\n              bulkable:   true,\n              bulkAction: 'deleteContainer',\n            },\n          ],\n          stopContainer:   (args) => {\n            this.execCommand('stop', args?.length ? args : container);\n          },\n          startContainer:   (args) => {\n            this.execCommand('start', args?.length ? args : container);\n          },\n          deleteContainer:   (args) => {\n            this.execCommand('rm', args?.length ? args : container);\n          },\n          viewInfo: () => {\n            this.viewInfo(container);\n          },\n          portList: this.getPortList(container),\n        }));\n    },\n    errorMessage() {\n      if (this.execError) {\n        return this.execError;\n      }\n      switch (this.error?.source) {\n      case 'containers': case 'namespaces': {\n        const error = this.error.error;\n\n        return `${ error?.stderr ?? error }`;\n      }\n      }\n      return null;\n    },\n  },\n  mounted() {\n    this.$store.dispatch('page/setHeader', {\n      title:       this.t('containers.title'),\n      description: '',\n    });\n\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.settings = settings;\n      this.subscribe().catch(console.error);\n    });\n\n    ipcRenderer.send('settings-read');\n\n    ipcRenderer.on('settings-update', this.updateSettings);\n\n    this.subscribe().catch(console.error);\n  },\n  beforeUnmount() {\n    ipcRenderer.removeListener('settings-update', this.updateSettings);\n    this.$store.dispatch('container-engine/unsubscribe').catch(console.error);\n    clearTimeout(this.subscribeTimer);\n  },\n  methods: {\n    async subscribe() {\n      clearTimeout(this.subscribeTimer);\n      try {\n        if (!window.ddClient || !this.isK8sReady || !this.settings) {\n          setTimeout(() => this.subscribe(), 1_000);\n          return;\n        }\n        await this.$store.dispatch('container-engine/subscribe', {\n          type:      'containers',\n          client:    window.ddClient,\n        });\n      } catch (error) {\n        console.error('There was a problem subscribing to container events:', { error });\n      }\n    },\n\n    updateSettings(_event, settings) {\n      this.settings = settings;\n      this.checkSelectedNamespace();\n    },\n\n    checkSelectedNamespace() {\n      if (!this.supportsNamespaces || !this.namespaces?.length) {\n        // Nothing to verify yet\n        return;\n      }\n      if (!this.namespaces.includes(this.namespace)) {\n        const K8S_NAMESPACE = 'k8s.io';\n        const defaultNamespace = this.namespaces.includes(K8S_NAMESPACE) ? K8S_NAMESPACE : this.namespaces[0];\n\n        ipcRenderer.invoke('settings-write',\n          { containers: { namespace: defaultNamespace } } );\n      }\n    },\n    async onChangeNamespace(event) {\n      const { value } = event.target;\n      if (value !== this.namespace) {\n        await ipcRenderer.invoke('settings-write',\n          { containers: { namespace: value } } );\n        this.$store.dispatch('container-engine/subscribe', {\n          type:      'containers',\n          client:    window.ddClient,\n          namespace: value,\n        });\n      }\n    },\n    clearDropDownPosition(e) {\n      const target = e.target;\n\n      const dropdownContent = target.querySelector('.dropdown-content');\n\n      if (dropdownContent) {\n        dropdownContent.style.top = '';\n      }\n    },\n    addDropDownPosition(e) {\n      const table = this.$refs.sortableTableRef.$el;\n      const target = e.target;\n\n      const dropdownContent = target.querySelector('.dropdown-content');\n\n      if (dropdownContent) {\n        const dropdownRect = target.getBoundingClientRect();\n        const tableRect = table.getBoundingClientRect();\n        const targetTopPos = dropdownRect.top - tableRect.top;\n        const tableHeight = tableRect.height;\n\n        if (targetTopPos < tableHeight / 2) {\n          // Show dropdownContent below the target\n          dropdownContent.style.top = `${ dropdownRect.bottom }px`;\n        } else {\n          // Show dropdownContent above the target\n          dropdownContent.style.top = `${ dropdownRect.top - dropdownContent.getBoundingClientRect().height }px`;\n        }\n      }\n    },\n    viewInfo(container) {\n      this.$router.push(`/containers/info/${ container.id }`);\n    },\n\n    /** @param container {RowItem} */\n    isRunning(container) {\n      return container.state === 'running' || container.status === 'Up';\n    },\n    /** @param container {RowItem} */\n    isStopped(container) {\n      return container.state === 'created' || container.state === 'exited';\n    },\n    /**\n     * Execute a command against some containers\n     * @param command {string} The command to run\n     * @param _ids {Container | Container[]} The containers to affect\n     */\n    async execCommand(command, _ids) {\n      try {\n        const ids = Array.isArray(_ids) ? _ids.map(c => c.id) : [_ids.id];\n        const options = { cwd: '/' };\n\n        console.info(`Executing command ${ command } on container ${ ids }`);\n        if (this.supportsNamespaces) {\n          options.namespace = this.namespace;\n        }\n\n        const { stderr, stdout } = await window.ddClient.docker.cli.exec(command, [...ids], options);\n\n        if (stderr) {\n          throw new Error(stderr);\n        }\n\n        return stdout;\n      } catch (error) {\n        const extractErrorMessage = (err) => {\n          const rawMessage = err?.message || err?.stderr || err || '';\n\n          if (typeof rawMessage === 'string') {\n            // Extract message from fatal/error format: time=\"...\" level=fatal msg=\"actual message\"\n            const msgMatch = rawMessage.match(/msg=\"((?:[^\"\\\\]|\\\\.)*)\"/);\n            if (msgMatch) {\n              return msgMatch[1];\n            }\n\n            // Fallback: remove timestamp and level prefixes\n            const cleanedMessage = rawMessage\n              .replace(/time=\"[^\"]*\"\\s*/g, '')\n              .replace(/level=(fatal|error|info)\\s*/g, '')\n              .replace(/msg=\"/g, '')\n              .replace(/\"\\s*Error: exit status \\d+/g, '')\n              .trim();\n\n            if (cleanedMessage) {\n              return cleanedMessage;\n            }\n          }\n\n          return `Failed to execute command: ${ command }`;\n        };\n\n        this.execError = extractErrorMessage(error);\n        console.error(`Error executing command ${ command }`, error);\n      }\n    },\n    shortSha(sha) {\n      const prefix = 'sha256:';\n\n      if (sha.includes(prefix)) {\n        const startIndex = sha.indexOf(prefix) + prefix.length;\n        const actualSha = sha.slice(startIndex);\n\n        return `${ sha.slice(0, startIndex) }${ actualSha.slice(0, 3) }..${ actualSha.slice(-3) }`;\n      }\n\n      return sha;\n    },\n    getTooltipConfig(sha) {\n      if (!sha.includes('sha256:')) {\n        return { content: undefined };\n      }\n\n      return { content: sha };\n    },\n    /**\n     * @param container {Container}\n     * @returns {[number, number][]} (host port, container port) tuples, sorted by host port.\n     */\n    getPortList(container) {\n      /** @type [string, { HostIp: string, HostPort: string}][] */\n      const rawPorts = Object.entries(container.ports).filter(([, host]) => !!host);\n      // Convert to a map to make sure it's unique by host port\n      /** @type Record<string, string> */\n      const mapping = Object.fromEntries(rawPorts.flatMap(([portDef, entries]) => {\n        return entries.map(({ HostPort }) => [HostPort, portDef.split('/')[0]]);\n      }));\n      return Object.entries(mapping).map(([h, c]) => [parseInt(h, 10), parseInt(c, 10)]);\n    },\n    /** @param ports {(readonly [number, number])[]} */\n    shouldHaveDropdown(ports) {\n      if (!ports) {\n        return false;\n      }\n\n      return ports.length >= 3;\n    },\n    openUrl(hostPort) {\n      if ([80, 443].includes(hostPort)) {\n        hostPort === 80 ? shell.openExternal(`http://localhost`) : shell.openExternal(`https://localhost`);\n      } else {\n        return shell.openExternal(`http://localhost:${ hostPort }`);\n      }\n    },\n\n    toggleExpand(group) {\n      this.collapsed[group] = !this.collapsed[group];\n    },\n\n    clearError() {\n      this.execError = null;\n      switch (this.error?.source) {\n      case 'namespaces': case 'containers':\n        this.$store.commit('container-engine/SET_ERROR', null);\n      }\n    },\n  },\n  watch: {\n    isK8sReady(isK8sReady) {\n      if (!isK8sReady) {\n        // The backend went from ready -> unready, unsubscribe and restart.\n        this.$store.dispatch('container-engine/unsubscribe').catch(console.error);\n        this.subscribe().catch(console.error);\n      }\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.containers {\n  &-status {\n    padding: 8px 5px;\n  }\n\n  .group-row {\n    .group-tab {\n      font-weight: bold;\n      .icon {\n        cursor: pointer;\n      }\n    }\n    &[aria-expanded=\"false\"] {\n      :deep(~ .main-row) {\n        visibility: collapse;\n        .checkbox-container {\n          /* When using visibility:collapse, the row selection checkbox produces\n           * some artifacts; force it to display:none to avoid flickering. */\n          display: none;\n        }\n      }\n    }\n  }\n}\n\n.dropdown {\n  position: relative;\n  display: inline-block;\n\n  span {\n    cursor: pointer;\n    padding: 5px;\n  }\n\n  &-content {\n    display: none;\n    position: fixed;\n    z-index: 1;\n    border-start-start-radius: var(--border-radius);\n    background: var(--default);\n    padding: 5px;\n    transition: all 0.5s ease-in-out;\n\n    a {\n      display: block;\n      padding: 5px 0;\n    }\n  }\n\n  &:hover {\n    & > .dropdown-content {\n      display: block;\n    }\n  }\n}\n\n.link {\n  cursor: pointer;\n  text-decoration: none;\n}\n\n.state-container {\n  padding: 8px 5px;\n  margin-top: 5px;\n}\n\n.select-namespace {\n  max-width: 24rem;\n  min-width: 8rem;\n}\n\n.containersTable :deep(.search-box) {\n  align-self: flex-end;\n}\n.containersTable :deep(.bulk) {\n  align-self: flex-end;\n}\n\n.container-name-link {\n  color: var(--link);\n  cursor: pointer;\n  text-decoration: none;\n\n  &:hover {\n    text-decoration: underline;\n    color: var(--link-hover);\n  }\n}\n\n.port-container {\n  display: flex;\n  gap: 5px;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/DenyRoot.vue",
    "content": "<template>\n  <div>\n    <h2>\n      Cannot run as Root\n    </h2>\n    <p>\n      Rancher Desktop cannot be run with root privileges.\n      Please run again as a regular user.\n    </p>\n    <div class=\"button-area\">\n      <button\n        data-test=\"accept-btn\"\n        class=\"role-primary\"\n        @click=\"close\"\n      >\n        OK\n      </button>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:   'page-deny-root',\n  layout: 'dialog',\n  mounted() {\n    ipcRenderer.send('dialog/ready');\n  },\n  methods: {\n    close() {\n      window.close();\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .button-area {\n    align-self: flex-end;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Diagnostics.vue",
    "content": "<script lang=\"ts\">\n\nimport { defineComponent } from 'vue';\n\nimport DiagnosticsBody from '@pkg/components/DiagnosticsBody.vue';\nimport { mapTypedState } from '@pkg/entry/store';\n\nexport default defineComponent({\n  name:       'diagnostics',\n  components: { DiagnosticsBody },\n\n  computed: mapTypedState('diagnostics', ['diagnostics', 'timeLastRun']),\n  async beforeMount() {\n    await this.$store.dispatch('credentials/fetchCredentials');\n    await this.$store.dispatch('preferences/fetchPreferences');\n    await this.$store.dispatch('diagnostics/fetchDiagnostics');\n  },\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: 'Diagnostics' },\n    );\n  },\n});\n</script>\n\n<template>\n  <diagnostics-body\n    :rows=\"diagnostics\"\n    :time-last-run=\"timeLastRun\"\n  />\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Dialog.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { Checkbox } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'rd-dialog',\n  components: { Checkbox },\n  layout:     'dialog',\n\n  data() {\n    return {\n      message:         '',\n      detail:          '',\n      checkboxLabel:   '',\n      buttons:         [],\n      response:        0,\n      checkboxChecked: false,\n      cancelId:        0,\n    };\n  },\n\n  mounted() {\n    ipcRenderer.on('dialog/options', (_event, options: any) => {\n      this.message = options.message;\n      this.detail = options.detail;\n      this.checkboxLabel = options.checkboxLabel;\n      this.buttons = options.buttons;\n      this.cancelId = options.cancelId;\n      ipcRenderer.send('dialog/ready');\n    });\n\n    ipcRenderer.send('dialog/mounted');\n  },\n\n  beforeUnmount() {\n    ipcRenderer.removeAllListeners('dialog/options');\n  },\n\n  methods: {\n    close(index: number) {\n      ipcRenderer.send('dialog/close', { response: index, checkboxChecked: this.checkboxChecked });\n    },\n    isDarwin() {\n      return os.platform().startsWith('darwin');\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"dialog-container\">\n    <div\n      v-if=\"message\"\n      class=\"message\"\n    >\n      <slot name=\"message\">\n        {{ message }}\n      </slot>\n    </div>\n    <div\n      v-if=\"detail\"\n      class=\"detail\"\n    >\n      <slot name=\"detail\">\n        <span\n          class=\"detail-span\"\n          v-html=\"detail\"\n        />\n      </slot>\n    </div>\n    <div\n      v-if=\"checkboxLabel\"\n      class=\"checkbox\"\n    >\n      <slot name=\"checkbox\">\n        <checkbox\n          v-model:value=\"checkboxChecked\"\n          :label=\"checkboxLabel\"\n        />\n      </slot>\n    </div>\n    <div\n      class=\"actions\"\n      :class=\"{ 'actions-reverse': isDarwin() }\"\n    >\n      <slot name=\"actions\">\n        <template v-if=\"!buttons.length\">\n          <button class=\"btn role-primary\">\n            OK\n          </button>\n        </template>\n        <template v-else>\n          <button\n            v-for=\"(buttonText, index) in buttons\"\n            :key=\"index\"\n            class=\"btn\"\n            :class=\"index === 0 ? 'role-primary' : 'role-secondary'\"\n            @click=\"close(index)\"\n          >\n            {{ buttonText }}\n          </button>\n        </template>\n      </slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .dialog-container {\n    display: flex;\n    width: 32rem;\n    max-width: 40rem;\n  }\n\n  .message {\n    font-size: 1.5rem;\n    line-height: 2rem;\n    font-weight: 600;\n  }\n\n  .detail {\n    font-size: 1rem;\n    line-height: 1.5rem;\n  }\n\n  .detail-span {\n    display: flex;\n    flex-direction: column;\n    gap: 0.75rem;\n  }\n\n  .checkbox {\n    padding-left: 0.25rem;\n  }\n\n  .actions {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-end;\n    gap: 0.25rem;\n    padding-top: 1rem;\n  }\n\n  .actions-reverse {\n    justify-content: flex-start;\n    flex-direction: row-reverse;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Extensions.vue",
    "content": "<template>\n  <div class=\"extensions-page\">\n    <rd-tabbed\n      :active-tab=\"activeTab\"\n    >\n      <tab\n        data-test=\"extensions-tab-installed\"\n        :label=\"t('marketplace.tabs.installed')\"\n        name=\"extensions-installed\"\n        :weight=\"0\"\n        @active=\"tabActivate('extensions-installed')\"\n      />\n      <tab\n        data-test=\"extensions-tab-catalog\"\n        :label=\"t('marketplace.tabs.catalog')\"\n        name=\"marketplace-catalog\"\n        :weight=\"1\"\n        @active=\"tabActivate('marketplace-catalog')\"\n      />\n      <div class=\"marketplace-container\">\n        <component\n          :is=\"activeTab\"\n          @click:browse=\"tabActivate('marketplace-catalog')\"\n        />\n      </div>\n    </rd-tabbed>\n  </div>\n</template>\n\n<script>\n\nimport MarketplaceCatalog from '@pkg/components/MarketplaceCatalog.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport { defaultSettings } from '@pkg/config/settings';\nimport ExtensionsInstalled from '@pkg/pages/extensions/installed.vue';\n\nexport default {\n  title:      'Marketplace',\n  components: {\n    RdTabbed,\n    Tab,\n    MarketplaceCatalog,\n    ExtensionsInstalled,\n  },\n  data() {\n    return {\n      settings:           defaultSettings,\n      imageNamespaces:    [],\n      supportsNamespaces: true,\n      activeTab:          'marketplace-catalog',\n    };\n  },\n  mounted() {\n    this.$store.dispatch('page/setHeader', {\n      title:       this.t('marketplace.title'),\n      description: '',\n    });\n  },\n  methods: {\n    tabActivate(tab) {\n      this.activeTab = tab;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.extensions-content {\n  display: grid;\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));\n  gap: 1.5rem;\n\n  &-missing {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    width: 100%;\n    height: 100%;\n    // font-size: 1.5rem;\n  }\n}\n\n.marketplace-container {\n  padding: 1rem 0.25rem;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/FirstRun.vue",
    "content": "<template>\n  <div class=\"first-run-container\">\n    <h2 data-test=\"k8s-settings-header\">\n      Welcome to Rancher Desktop by SUSE\n    </h2>\n    <rd-checkbox\n      label=\"Enable Kubernetes\"\n      :value=\"hasVersions && settings.kubernetes.enabled\"\n      :is-locked=\"kubernetesLocked\"\n      :disabled=\"!hasVersions\"\n      @update:value=\"handleDisableKubernetesCheckbox\"\n    />\n    <rd-fieldset\n      :legend-text=\"t('firstRun.kubernetesVersion.legend') + offlineCheck()\"\n    >\n      <rd-select\n        v-model=\"settings.kubernetes.version\"\n        :is-locked=\"kubernetesVersionLocked\"\n        class=\"select-k8s-version\"\n        @change=\"onChange\"\n      >\n        <!--\n            - On macOS Chrome / Electron can't style the <option> elements.\n            - We do the best we can by instead using <optgroup> for a recommended section.\n            -->\n        <optgroup\n          v-if=\"recommendedVersions.length > 0\"\n          label=\"Recommended Versions\"\n        >\n          <option\n            v-for=\"item in recommendedVersions\"\n            :key=\"item.version\"\n            :value=\"item.version\"\n            :selected=\"item.version === unwrappedDefaultVersion\"\n          >\n            {{ versionName(item) }}\n          </option>\n        </optgroup>\n        <optgroup\n          v-if=\"nonRecommendedVersions.length > 0\"\n          label=\"Other Versions\"\n        >\n          <option\n            v-for=\"item in nonRecommendedVersions\"\n            :key=\"item.version\"\n            :value=\"item.version\"\n            :selected=\"item.version === unwrappedDefaultVersion\"\n          >\n            v{{ item.version }}\n          </option>\n        </optgroup>\n      </rd-select>\n    </rd-fieldset>\n    <rd-fieldset\n      :legend-text=\"t('containerEngine.label')\"\n      :is-locked=\"engineSelectorLocked\"\n    >\n      <engine-selector\n        :container-engine=\"settings.containerEngine.name\"\n        :is-locked=\"engineSelectorLocked\"\n        @change=\"onChangeEngine\"\n      />\n    </rd-fieldset>\n    <rd-fieldset\n      v-if=\"pathManagementRelevant\"\n      :legend-text=\"t('pathManagement.label')\"\n      :legend-tooltip=\"t('pathManagement.tooltip', { }, true)\"\n      :is-locked=\"pathManagementSelectorLocked\"\n    >\n      <path-management-selector\n        :value=\"pathManagementStrategy\"\n        :is-locked=\"pathManagementSelectorLocked\"\n        :show-label=\"false\"\n        @input=\"setPathManagementStrategy\"\n      />\n    </rd-fieldset>\n    <div class=\"button-area\">\n      <button\n        v-focus\n        data-test=\"accept-btn\"\n        class=\"role-primary\"\n        @click=\"close\"\n      >\n        {{ t('firstRun.ok') }}\n      </button>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport os from 'os';\n\nimport _ from 'lodash';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport EngineSelector from '@pkg/components/EngineSelector.vue';\nimport PathManagementSelector from '@pkg/components/PathManagementSelector.vue';\nimport RdSelect from '@pkg/components/RdSelect.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport RdFieldset from '@pkg/components/form/RdFieldset.vue';\nimport { defaultSettings } from '@pkg/config/settings';\nimport type { ContainerEngine, Settings } from '@pkg/config/settings';\nimport { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { highestStableVersion, VersionEntry } from '@pkg/utils/kubeVersions';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nexport default defineComponent({\n  name:       'first-run-dialog',\n  components: {\n    RdFieldset,\n    RdCheckbox,\n    EngineSelector,\n    PathManagementSelector,\n    RdSelect,\n  },\n  layout: 'dialog',\n  data() {\n    return {\n      settings:                     defaultSettings,\n      kubernetesLocked:             false,\n      kubernetesVersionLocked:      false,\n      engineSelectorLocked:         false,\n      pathManagementSelectorLocked: false,\n      versions:                     [] as VersionEntry[],\n\n      // If cachedVersionsOnly is true, it means we're offline and showing only the versions in the cache,\n      // not all the versions listed in <cache>/rancher-desktop/k3s-versions.json\n      cachedVersionsOnly: false,\n    };\n  },\n  computed: {\n    ...mapGetters('applicationSettings', { pathManagementStrategy: 'pathManagementStrategy' }),\n    /** The version that should be pre-selected as the default value. */\n    defaultVersion(): VersionEntry {\n      return highestStableVersion(this.recommendedVersions) ?? this.nonRecommendedVersions[0];\n    },\n    // This field is needed because the template-parser doesn't like `defaultVersion?.version.version`\n    unwrappedDefaultVersion(): string {\n      const wrappedSemver = this.defaultVersion;\n\n      return wrappedSemver ? wrappedSemver.version : '';\n    },\n    hasVersions(): boolean {\n      return this.versions.length > 0;\n    },\n    /** Versions that are the tip of a channel */\n    recommendedVersions(): VersionEntry[] {\n      return this.versions.filter(v => !!v.channels);\n    },\n    /** Versions that are not supported by a channel. */\n    nonRecommendedVersions(): VersionEntry[] {\n      return this.versions.filter(v => !v.channels);\n    },\n    pathManagementRelevant(): boolean {\n      return os.platform() === 'linux' || os.platform() === 'darwin';\n    },\n  },\n  beforeMount() {\n    // Save default settings on closing window.\n    window.addEventListener('beforeunload', this.close);\n  },\n  mounted() {\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.$data.settings = settings;\n    });\n    ipcRenderer.send('settings-read');\n    ipcRenderer.on('k8s-versions', (event, versions, cachedVersionsOnly) => {\n      this.versions = versions;\n      this.cachedVersionsOnly = cachedVersionsOnly;\n      this.settings.kubernetes.version = this.unwrappedDefaultVersion;\n      if (!this.hasVersions) {\n        ipcRenderer.invoke('settings-write', { kubernetes: { enabled: false } });\n      }\n      // Manually send the ready event here, as we do not use the normal\n      // \"dialog/populate\" event.\n      ipcRenderer.send('dialog/ready');\n    });\n    ipcRenderer.on('settings-update', (event, config) => {\n      this.settings.containerEngine.name = config.containerEngine.name;\n      this.settings.kubernetes.enabled = config.kubernetes.enabled;\n    });\n    ipcRenderer.send('k8s-versions');\n    if (this.pathManagementRelevant) {\n      this.setPathManagementStrategy(PathManagementStrategy.RcFiles);\n    }\n    ipcRenderer.invoke('get-locked-fields').then((lockedFields) => {\n      this.$data.kubernetesLocked = _.get(lockedFields, 'kubernetes.enabled');\n      this.$data.kubernetesVersionLocked = _.get(lockedFields, 'kubernetes.version');\n      this.$data.engineSelectorLocked = _.get(lockedFields, 'containerEngine.name');\n      this.$data.pathManagementSelectorLocked = _.get(lockedFields, 'application.pathManagementStrategy');\n    });\n  },\n  beforeUnmount() {\n    window.removeEventListener('beforeunload', this.close);\n  },\n  methods: {\n    async commitChanges(settings: RecursivePartial<Settings>) {\n      try {\n        return await ipcRenderer.invoke('settings-write', settings);\n      } catch (ex) {\n        console.log(`invoke settings-write failed: `, ex);\n      }\n    },\n    onChange() {\n      return this.commitChanges({\n        application: { pathManagementStrategy: this.pathManagementStrategy },\n        kubernetes:  {\n          version: this.settings.kubernetes.version,\n          enabled: this.settings.kubernetes.enabled && this.hasVersions,\n        },\n      });\n    },\n    close() {\n      this.onChange();\n      window.close();\n    },\n    onChangeEngine(desiredEngine: ContainerEngine) {\n      return this.commitChanges({ containerEngine: { name: desiredEngine } });\n    },\n    handleDisableKubernetesCheckbox(value: boolean) {\n      return this.commitChanges({ kubernetes: { enabled: value } });\n    },\n    /**\n     * Get the display name of a given version.\n     * @param version The version to format.\n     */\n    versionName(version: VersionEntry) {\n      const names = (version.channels ?? []).filter(ch => !/^v?\\d+/.test(ch));\n\n      if (names.length > 0) {\n        return `v${ version.version } (${ names.join(', ') })`;\n      }\n\n      return `v${ version.version }`;\n    },\n    setPathManagementStrategy(val: PathManagementStrategy) {\n      this.$store.dispatch('applicationSettings/setPathManagementStrategy', val);\n    },\n    offlineCheck() {\n      return this.cachedVersionsOnly ? ` ${ this.t('firstRun.kubernetesVersion.cachedOnly') }` : '';\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\">\n  html {\n    height: initial;\n  }\n</style>\n\n<style lang=\"scss\" scoped>\n  .button-area {\n    align-self: flex-end;\n  }\n\n  .select-k8s-version {\n    margin-top: 0.5rem;\n  }\n\n  .first-run-container {\n    width: 26rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/General.vue",
    "content": "<template>\n  <div class=\"general\">\n    <div>\n      <ul>\n        <li>Project Discussions: <b>#rancher-desktop</b> in <a href=\"https://slack.rancher.io/\">Rancher Users</a> Slack</li>\n        <li class=\"project-links\">\n          <span>Project Links:</span>\n          <a href=\"https://github.com/rancher-sandbox/rancher-desktop\">Homepage</a>\n          <a href=\"https://github.com/rancher-sandbox/rancher-desktop/issues\">Issues</a>\n        </li>\n      </ul>\n    </div>\n    <hr>\n    <update-status\n      :enabled=\"settings.application.updater.enabled\"\n      :update-state=\"updateState\"\n      :is-auto-update-locked=\"autoUpdateLocked\"\n      @enabled=\"onUpdateEnabled\"\n      @apply=\"onUpdateApply\"\n    />\n    <hr>\n    <telemetry-opt-in\n      :telemetry=\"settings.application.telemetry.enabled\"\n      :is-telemetry-locked=\"telemetryLocked\"\n      @update-telemetry=\"updateTelemetry\"\n    />\n    <hr>\n    <div class=\"network-status\">\n      <network-status />\n    </div>\n  </div>\n</template>\n\n<script>\n\nimport _ from 'lodash';\n\nimport NetworkStatus from '@pkg/components/NetworkStatus.vue';\nimport TelemetryOptIn from '@pkg/components/TelemetryOptIn.vue';\nimport UpdateStatus from '@pkg/components/UpdateStatus.vue';\nimport { defaultSettings } from '@pkg/config/settings';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default {\n  name:       'General',\n  title:      'General',\n  components: {\n    NetworkStatus, TelemetryOptIn, UpdateStatus,\n  },\n  data() {\n    return {\n      settings:         defaultSettings,\n      telemetryLocked:  null,\n      autoUpdateLocked: null,\n      /** @type import('@pkg/main/update').UpdateState | null */\n      updateState:      null,\n    };\n  },\n\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      {\n        title:       this.t('general.title'),\n        description: this.t('general.description'),\n        icon:        'icon icon-rancher-desktop',\n      },\n    );\n    ipcRenderer.on('settings-update', this.onSettingsUpdate);\n    ipcRenderer.on('update-state', this.onUpdateState);\n    ipcRenderer.send('update-state');\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.$data.settings = settings;\n    });\n    ipcRenderer.send('settings-read');\n    ipcRenderer.invoke('get-locked-fields').then((lockedFields) => {\n      this.$data.telemetryLocked = _.get(lockedFields, 'application.telemetry.enabled');\n      this.$data.autoUpdateLocked = _.get(lockedFields, 'application.updater.enabled');\n    });\n  },\n\n  beforeUnmount() {\n    ipcRenderer.off('settings-update', this.onSettingsUpdate);\n    ipcRenderer.off('update-state', this.onUpdateState);\n  },\n\n  methods: {\n    onSettingsUpdate(event, settings) {\n      this.$data.settings = settings;\n    },\n    onUpdateEnabled(value) {\n      ipcRenderer.invoke('settings-write', { application: { updater: { enabled: value } } });\n    },\n    onUpdateApply() {\n      ipcRenderer.send('update-apply');\n    },\n    onUpdateState(event, state) {\n      this.$data.updateState = state;\n    },\n    updateTelemetry(value) {\n      ipcRenderer.invoke('settings-write', { application: { telemetry: { enabled: value } } });\n    },\n  },\n};\n</script>\n\n<!-- Add \"scoped\" attribute to limit CSS to this component only -->\n<style scoped lang=\"scss\">\n.general {\n  display: flex;\n  flex-direction: column;\n  gap: 0.625rem;\n\n  ul {\n    margin-bottom: 0;\n\n    li {\n      margin-bottom: .5em;\n    }\n  }\n}\n\n.project-links > * {\n  margin-right: .25em;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Images.vue",
    "content": "<template>\n  <div>\n    <RouterView />\n    <Images\n      class=\"content\"\n      data-test=\"imagesTable\"\n      :images=\"images\"\n      :image-namespaces=\"imageNamespaces\"\n      :state=\"state\"\n      :show-all=\"settings.images.showAll\"\n      :selected-namespace=\"settings.images.namespace\"\n      :supports-namespaces=\"supportsNamespaces\"\n      :protected-images=\"protectedImages\"\n      @toggled-show-all=\"onShowAllImagesChanged\"\n      @switch-namespace=\"onChangeNamespace\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\">\n\nimport _ from 'lodash';\nimport { defineComponent } from 'vue';\n\nimport { State as K8sState } from '@pkg/backend/backend';\nimport Images from '@pkg/components/Images.vue';\nimport { defaultSettings } from '@pkg/config/settings';\nimport { mapTypedActions, mapTypedGetters, mapTypedMutations, mapTypedState } from '@pkg/entry/store';\nimport { IpcRendererEvents } from '@pkg/typings/electron-ipc';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\ntype Image = Parameters<IpcRendererEvents['images-changed']>[0][number];\n\nenum ImageManagerStates {\n  UNREADY = 'IMAGE_MANAGER_UNREADY',\n  READY = 'READY',\n}\n\nexport default defineComponent({\n  components: { Images },\n  data() {\n    return {\n      settings:           defaultSettings,\n      images:             [] as Image[],\n      imageNamespaces:    [] as string[],\n      supportsNamespaces: true,\n    };\n  },\n\n  computed: {\n    state() {\n      if ((window as any).imagesListMock) {\n        // Override for screenshots\n        return ImageManagerStates.READY;\n      }\n\n      if (![K8sState.STARTED, K8sState.DISABLED].includes(this.k8sState)) {\n        return ImageManagerStates.UNREADY;\n      }\n\n      return this.imageManagerState ? ImageManagerStates.READY : ImageManagerStates.UNREADY;\n    },\n    rancherImages(): string[] {\n      return this.images\n        .map(image => image.imageName)\n        .filter(name => name.startsWith('rancher/'));\n    },\n    installedExtensionImages(): string[] {\n      return this.installedExtensions.map(image => image.id);\n    },\n    protectedImages(): string[] {\n      return [\n        'moby/buildkit',\n        'ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy',\n        ...this.rancherImages,\n        ...this.installedExtensionImages,\n      ];\n    },\n    ...mapTypedState('imageManager', ['imageManagerState']),\n    ...mapTypedGetters('k8sManager', { k8sState: 'getK8sState' }),\n    ...mapTypedGetters('extensions', ['installedExtensions']),\n  },\n\n  watch: {\n    state: {\n      handler(state: string) {\n        this.setHeader({ title: this.t('images.title') });\n\n        if (!state || state === ImageManagerStates.UNREADY) {\n          return;\n        }\n\n        this.setAction({ action: 'ImagesButtonAdd' });\n      },\n      immediate: true,\n    },\n  },\n\n  mounted() {\n    ipcRenderer.on('images-changed', async(event, images) => {\n      if ((window as any).imagesListMock) {\n        // Override for screenshots\n        images = await (window as any).imagesListMock();\n      }\n      if (_.isEqual(images, this.images)) {\n        return;\n      }\n\n      this.images = images;\n\n      if (this.supportsNamespaces && this.imageNamespaces.length === 0) {\n        // This happens if the user clicked on the Images panel before data was ready,\n        // so no namespaces were available when it initially asked for them.\n        // When the data is ready, images are pushed in, but namespaces aren't.\n        ipcRenderer.send('images-namespaces-read');\n      }\n    });\n\n    ipcRenderer.on('images-check-state', (event, state) => {\n      this.setImageManagerState(state);\n    });\n\n    ipcRenderer.invoke('images-check-state').then((state) => {\n      this.setImageManagerState(state);\n    });\n\n    ipcRenderer.on('settings-update', (event, settings) => {\n      // TODO: put in a status bar\n      this.$data.settings = settings;\n      this.checkSelectedNamespace();\n    });\n\n    (async() => {\n      this.images = await ipcRenderer.invoke('images-mounted', true);\n    })();\n\n    ipcRenderer.on('images-namespaces', (event, namespaces) => {\n      // TODO: Use a specific message to indicate whether or not messages are supported.\n      this.imageNamespaces = namespaces;\n      this.supportsNamespaces = namespaces.length > 0;\n      this.checkSelectedNamespace();\n    });\n    ipcRenderer.send('images-namespaces-read');\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.settings = settings;\n    });\n    ipcRenderer.send('settings-read');\n\n    ipcRenderer.on('extensions/changed', this.fetchExtensions);\n    this.fetchExtensions();\n  },\n  beforeUnmount() {\n    ipcRenderer.invoke('images-mounted', false);\n    ipcRenderer.removeAllListeners('images-changed');\n    ipcRenderer.removeListener('extensions/changed', this.fetchExtensions);\n  },\n\n  methods: {\n    ...mapTypedActions('extensions', { fetchExtensions: 'fetch' }),\n    ...mapTypedActions('page', ['setAction', 'setHeader']),\n    ...mapTypedMutations('imageManager', { setImageManagerState: 'SET_IMAGE_MANAGER_STATE' }),\n    checkSelectedNamespace() {\n      if (!this.supportsNamespaces || this.imageNamespaces.length === 0) {\n        // Nothing to verify yet\n        return;\n      }\n      if (!this.imageNamespaces.includes(this.settings.images.namespace)) {\n        const defaultNamespace = this.imageNamespaces.includes('default') ? 'default' : this.imageNamespaces[0];\n\n        ipcRenderer.invoke('settings-write',\n          { images: { namespace: defaultNamespace } } );\n      }\n    },\n    onShowAllImagesChanged(value: boolean) {\n      if (value !== this.settings.images.showAll) {\n        ipcRenderer.invoke('settings-write',\n          { images: { showAll: value } } );\n      }\n    },\n    onChangeNamespace(value: string) {\n      if (value !== this.settings.images.namespace) {\n        ipcRenderer.invoke('settings-write',\n          { images: { namespace: value } } );\n      }\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/KubernetesError.vue",
    "content": "<template>\n  <div class=\"container\">\n    <div class=\"page-body\">\n      <div class=\"error-header\">\n        <img\n          id=\"logo\"\n          src=\"../../../resources/icons/logo-square-red@2x.png\"\n        >\n        <span>\n          <h2 data-test=\"k8s-error-header\">\n            {{ t('app.name') }} Error\n          </h2>\n          <h5>{{ versionString }}</h5>\n        </span>\n      </div>\n      <div class=\"k8s-error\">\n        <div class=\"error-part\">\n          <h4>{{ titlePart }}</h4>\n          <pre id=\"main-message\">{{ mainMessage }}</pre>\n        </div>\n        <div\n          v-if=\"lastCommand\"\n          class=\"error-part command\"\n        >\n          <h4>Last command run:</h4>\n          <p>{{ lastCommand }}</p>\n        </div>\n        <div\n          v-if=\"lastCommandComment\"\n          class=\"error-part\"\n        >\n          <h4>Context:</h4>\n          <p>{{ lastCommandComment }}</p>\n        </div>\n        <div\n          v-if=\"lastLogLines.length\"\n          class=\"error-part grow\"\n        >\n          <h4>\n            Some recent <a\n              href=\"#\"\n              @click.prevent=\"showLogs\"\n            >logfile</a> lines:\n          </h4>\n          <pre id=\"log-lines\">{{ joinedLastLogLines }}</pre>\n        </div>\n      </div>\n    </div>\n    <button\n      data-test=\"accept-btn\"\n      class=\"role-primary primary-action\"\n      @click=\"close\"\n    >\n      Close\n    </button>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport os from 'os';\n\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:   'kubernetes-error-dialog',\n  layout: 'dialog',\n  data() {\n    return {\n      titlePart:          '',\n      mainMessage:        '',\n      lastCommand:        '',\n      lastCommandComment: '',\n      lastLogLines:       [],\n      appVersion:         '',\n    };\n  },\n  computed: {\n    joinedLastLogLines(): string {\n      return this.lastLogLines.join('\\n');\n    },\n    platform(): string {\n      return os.platform();\n    },\n    arch(): string {\n      const arch = os.arch();\n\n      return arch === 'arm64' ? 'aarch64' : arch;\n    },\n    versionString(): string {\n      return `Rancher Desktop ${ this.appVersion } - ${ this.platform } (${ this.arch })`;\n    },\n  },\n  beforeMount() {\n    ipcRenderer.on('get-app-version', (_event, version) => {\n      this.appVersion = version;\n    });\n    ipcRenderer.send('get-app-version');\n  },\n  mounted() {\n    ipcRenderer.on('dialog/populate', (event, titlePart, mainMessage, failureDetails) => {\n      this.$data.titlePart = titlePart;\n      this.$data.mainMessage = mainMessage;\n      this.$data.lastCommand = failureDetails.lastCommand;\n      this.$data.lastCommandComment = failureDetails.lastCommandComment;\n      this.$data.lastLogLines = failureDetails.lastLogLines;\n    });\n    // Tell the dialog layout to set flex on the height.\n    document.documentElement.setAttribute('data-flex', 'height');\n  },\n  methods: {\n    close() {\n      window.close();\n    },\n    showLogs() {\n      ipcRenderer.send('show-logs');\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .container {\n    min-width: 52rem;\n  }\n\n  .error-header {\n    display: flex;\n    gap: 0.75rem;\n    h2 {\n      margin-top: 0.25rem;\n    }\n  }\n\n  img#logo {\n    height: 32px;\n    width: 32px;\n  }\n  .page-body {\n    display: flex;\n    flex-grow: 1;\n    flex-flow: column;\n  }\n  .k8s-error {\n    display: flex;\n    flex-grow: 1;\n    flex-flow: column;\n  }\n  pre#log-lines {\n    height: 8rem;\n    white-space: pre-wrap;\n    text-indent: -4em;\n    padding-left: 4em;\n    min-width: 80vw; /* 80% of viewport-width as specified in createWindow() in window/index.ts */\n  }\n  pre#main-message {\n    white-space: pre-line;\n    min-width: 80vw; /* See comment for pre#log-lines */\n  }\n\n  .error-part {\n    margin-top: 0.5rem;\n    margin-bottom: 1.5rem;\n    h4 {\n      margin-top: auto;\n    }\n    &.command p {\n      font-family: monospace;\n      white-space: pre-wrap;\n    }\n    &.grow {\n      display: flex;\n      flex-flow: column;\n      flex-grow: 1;\n      & > *:not(h4) {\n        flex-grow: 1;\n      }\n    }\n  }\n\n  .primary-action {\n    align-self: flex-end;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/PortForwarding.vue",
    "content": "<template>\n  <PortForwarding\n    class=\"content\"\n    :services=\"services\"\n    :include-kubernetes-services=\"settings.portForwarding.includeKubernetesServices\"\n    :k8s-state=\"state\"\n    :kubernetes-is-disabled=\"!settings.kubernetes.enabled\"\n    :service-being-edited=\"serviceBeingEdited\"\n    :error-message=\"errorMessage\"\n    @update-port=\"handleUpdatePort\"\n    @toggled-service-filter=\"onIncludeK8sServicesChanged\"\n    @edit-port-forward=\"handleEditPortForward\"\n    @cancel-port-forward=\"handleCancelPortForward\"\n    @cancel-edit-port-forward=\"handleCancelEditPortForward\"\n    @update-port-forward=\"handleUpdatePortForward\"\n    @close-error=\"handleCloseError\"\n  />\n</template>\n\n<script lang=\"ts\">\n\nimport clone from 'lodash/cloneDeep';\nimport { defineComponent } from 'vue';\n\nimport type { ServiceEntry } from '@pkg/backend/k8s';\nimport PortForwarding from '@pkg/components/PortForwarding.vue';\nimport { defaultSettings, Settings } from '@pkg/config/settings';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'port-forwarding',\n  components: { PortForwarding },\n  data() {\n    return {\n      state:              ipcRenderer.sendSync('k8s-state'),\n      settings:           defaultSettings,\n      services:           [] as ServiceEntry[],\n      errorMessage:       null as string | null,\n      serviceBeingEdited: null as ServiceEntry | null,\n    };\n  },\n\n  watch: {\n    services: {\n      handler(newServices: ServiceEntry[]): void {\n        if (this.serviceBeingEdited) {\n          const newService = newServices.find(service => this.compareServices(this.serviceBeingEdited as ServiceEntry, service));\n\n          if (newService) {\n            this.serviceBeingEdited = Object.assign(this.serviceBeingEdited, { listenPort: newService.listenPort });\n          }\n        }\n      },\n      deep: true,\n    },\n  },\n\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.t('portForwarding.title') },\n    );\n    ipcRenderer.on('k8s-check-state', (event, state) => {\n      this.$data.state = state;\n    });\n    ipcRenderer.on('service-changed', (event, services) => {\n      this.$data.services = services;\n    });\n    ipcRenderer.on('service-error', (event, problemService, errorMessage) => {\n      ipcRenderer.invoke('service-forward', problemService, false);\n      this.$data.errorMessage = errorMessage;\n    });\n    ipcRenderer.invoke('service-fetch')\n      .then((services) => {\n        this.$data.services = services;\n      });\n    ipcRenderer.on('settings-update', (event, settings) => {\n      // TODO: put in a status bar\n      this.$data.settings = settings;\n    });\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.$data.settings = settings;\n    });\n    ipcRenderer.send('settings-read');\n  },\n\n  methods: {\n    handleUpdatePort(newPort: number): void {\n      if (this.serviceBeingEdited) {\n        this.serviceBeingEdited.listenPort = newPort;\n      }\n    },\n\n    onIncludeK8sServicesChanged(value: boolean): void {\n      if (value !== this.settings.portForwarding.includeKubernetesServices) {\n        ipcRenderer.invoke('settings-write',\n          { portForwarding: { includeKubernetesServices: value } } );\n      }\n    },\n\n    compareServices(service1: ServiceEntry, service2: ServiceEntry): boolean {\n      return service1.name === service2.name &&\n        service1.namespace === service2.namespace &&\n        service1.port === service2.port;\n    },\n\n    findServiceMatching(serviceToMatch: ServiceEntry | undefined, serviceList: ServiceEntry[]): ServiceEntry | undefined {\n      if (!serviceToMatch) {\n        return undefined;\n      }\n      const compareServices = (service1: ServiceEntry, service2: ServiceEntry) => {\n        return service1.name === service2.name &&\n          service1.namespace === service2.namespace &&\n          service1.port === service2.port;\n      };\n\n      return serviceList.find(service => compareServices(service, serviceToMatch));\n    },\n\n    handleEditPortForward(service: ServiceEntry): void {\n      this.errorMessage = null;\n      if (this.serviceBeingEdited) {\n        ipcRenderer.invoke('service-forward', this.serviceBeingEdited, false);\n      }\n      this.serviceBeingEdited = Object.assign({}, service);\n      // Forward ServiceEntry without listenPort set to get random port.\n      // The user can change this after we get a random port.\n      ipcRenderer.invoke('service-forward', service, true);\n    },\n\n    handleCancelEditPortForward(service: ServiceEntry): void {\n      this.errorMessage = null;\n      ipcRenderer.invoke('service-forward', service, false);\n      this.serviceBeingEdited = null;\n    },\n\n    handleCancelPortForward(service: ServiceEntry): void {\n      this.errorMessage = null;\n      ipcRenderer.invoke('service-forward', service, false);\n    },\n\n    handleUpdatePortForward(): void {\n      this.errorMessage = null;\n      if (this.serviceBeingEdited) {\n        ipcRenderer.invoke('service-forward', clone(this.serviceBeingEdited), true);\n      }\n      this.serviceBeingEdited = null;\n    },\n\n    handleCloseError(): void {\n      this.errorMessage = null;\n    },\n  },\n});\n</script>\n\n<style scoped>\n  .content {\n    padding-top: 13px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Preferences.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { defineComponent } from 'vue';\nimport { mapGetters, mapState } from 'vuex';\n\nimport EmptyState from '@pkg/components/EmptyState.vue';\nimport PreferencesBody from '@pkg/components/Preferences/ModalBody.vue';\nimport PreferencesFooter from '@pkg/components/Preferences/ModalFooter.vue';\nimport PreferencesHeader from '@pkg/components/Preferences/ModalHeader.vue';\nimport PreferencesNav from '@pkg/components/Preferences/ModalNav.vue';\nimport type { TransientSettings } from '@pkg/config/transientSettings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { Direction, RecursivePartial } from '@pkg/utils/typeUtils';\nimport { preferencesNavItems } from '@pkg/window/preferenceConstants';\n\nexport default defineComponent({\n  name:       'preferences-modal',\n  components: {\n    PreferencesHeader, PreferencesNav, PreferencesBody, PreferencesFooter, EmptyState,\n  },\n  layout: 'preferences',\n  data() {\n    return { preferencesLoaded: false };\n  },\n  computed: {\n    ...mapGetters('preferences', ['getPreferences', 'hasError']),\n    ...mapGetters('transientSettings', ['getCurrentNavItem']),\n    ...mapState('credentials', ['credentials']),\n    navItems(): string[] {\n      return preferencesNavItems.map(({ name }) => name);\n    },\n  },\n  async beforeMount() {\n    await this.$store.dispatch('credentials/fetchCredentials');\n    await this.$store.dispatch('preferences/fetchPreferences');\n    await this.$store.dispatch('preferences/fetchLocked');\n    await this.$store.dispatch('transientSettings/fetchTransientSettings');\n    this.preferencesLoaded = true;\n\n    ipcRenderer.on('k8s-integrations', (_, integrations: Record<string, string | boolean>) => {\n      this.$store.dispatch('preferences/setWslIntegrations', integrations);\n    });\n\n    ipcRenderer.send('k8s-integrations');\n\n    this.$store.dispatch('preferences/setPlatformWindows', os.platform().startsWith('win'));\n\n    ipcRenderer.on('route', async(event, args) => {\n      await this.navigateToTab(args);\n    });\n\n    ipcRenderer.invoke('versions/macOs').then((macOsVersion) => {\n      this.$store.dispatch('transientSettings/setMacOsVersion', macOsVersion);\n    });\n\n    ipcRenderer.invoke('host/isArm').then((isArm) => {\n      this.$store.dispatch('transientSettings/setIsArm', isArm);\n    });\n  },\n  beforeUnmount() {\n    /**\n     * Removing the listeners resolves the issue of receiving duplicated messages from 'route' channel.\n     * Originated by: https://github.com/rancher-sandbox/rancher-desktop/issues/3232\n     */\n    ipcRenderer.removeAllListeners('route');\n  },\n  methods: {\n    async navChanged(current: string) {\n      await this.commitNavItem(current);\n    },\n    async commitNavItem(current: string) {\n      await this.$store.dispatch(\n        'transientSettings/commitPreferences',\n        { payload: { preferences: { navItem: { current } } } },\n      );\n    },\n    closePreferences() {\n      ipcRenderer.send('preferences-close');\n    },\n    async applyPreferences() {\n      const resetAccepted = await this.proposePreferences();\n\n      if (!resetAccepted) {\n        return;\n      }\n\n      await this.$store.dispatch('preferences/commitPreferences');\n      this.closePreferences();\n    },\n    async proposePreferences() {\n      const { reset } = await this.$store.dispatch('preferences/proposePreferences');\n\n      if (!reset) {\n        return true;\n      }\n\n      const cancelPosition = 1;\n\n      const result = await ipcRenderer.invoke('show-message-box', {\n        title:    'Rancher Desktop - Reset Kubernetes',\n        type:     'warning',\n        message:  'Apply preferences and reset Kubernetes?',\n        detail:   'These changes will reset the Kubernetes cluster, which will result in a loss of workloads and container images.',\n        cancelId: cancelPosition,\n        buttons:  [\n          'Apply and reset',\n          'Cancel',\n        ],\n      });\n\n      return result.response !== cancelPosition;\n    },\n    reloadPreferences() {\n      window.location.reload();\n    },\n    async navigateToTab(args: { name?: string, direction?: Direction }) {\n      const { name, direction } = args;\n\n      if (name) {\n        await this.commitNavItem(name);\n\n        return;\n      }\n\n      if (direction) {\n        const dir = (direction === 'forward' ? 1 : -1);\n        const idx = (this.navItems.length + this.navItems.indexOf(this.getCurrentNavItem) + dir) % this.navItems.length;\n\n        await this.commitNavItem(this.navItems[idx]);\n      }\n    },\n  },\n});\n</script>\n\n<template>\n  <div\n    v-if=\"preferencesLoaded\"\n    class=\"modal-grid\"\n  >\n    <preferences-header\n      class=\"preferences-header\"\n    />\n    <preferences-nav\n      v-if=\"!hasError\"\n      class=\"preferences-nav\"\n      :current-nav-item=\"getCurrentNavItem\"\n      :nav-items=\"navItems\"\n      @nav-changed=\"navChanged\"\n    />\n    <preferences-body\n      v-bind=\"$attrs\"\n      class=\"preferences-body\"\n      :current-nav-item=\"getCurrentNavItem\"\n      :preferences=\"getPreferences\"\n    >\n      <div\n        v-if=\"hasError\"\n        class=\"preferences-error\"\n      >\n        <empty-state\n          icon=\"icon-warning\"\n          heading=\"Unable to fetch preferences\"\n          body=\"Reload Preferences to try again.\"\n        >\n          <template #primary-action>\n            <button\n              class=\"btn role-primary\"\n              @click=\"reloadPreferences\"\n            >\n              Reload preferences\n            </button>\n          </template>\n        </empty-state>\n      </div>\n    </preferences-body>\n    <preferences-footer\n      class=\"preferences-footer\"\n      @cancel=\"closePreferences\"\n      @apply=\"applyPreferences\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\">\n  .modal .vm--modal {\n    background-color: var(--body-bg);\n  }\n\n  .preferences-header {\n    grid-area: header;\n  }\n\n  .preferences-nav {\n    grid-area: nav;\n  }\n\n  .preferences-body {\n    grid-area: body;\n    max-height: 100%;\n    overflow: auto;\n  }\n\n  .preferences-footer {\n    grid-area: footer;\n  }\n\n  .modal-grid {\n    height: 100vh;\n    display: grid;\n    grid-template-columns: auto 1fr;\n    grid-template-rows: auto 1fr auto;\n    grid-template-areas:\n      \"header header\"\n      \"nav body\"\n      \"footer footer\";\n  }\n\n  .preferences-error {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: row;\n    justify-content: center;\n    align-items: center;\n    padding-bottom: 6rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Snapshots.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport Snapshots from '@pkg/components/Snapshots.vue';\n\nexport default defineComponent({\n  name:       'snapshots',\n  components: { Snapshots },\n\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      {\n        title:  this.t('snapshots.title'),\n        action: 'SnapshotsButtonCreate',\n      },\n    );\n  },\n});\n\n</script>\n\n<template>\n  <div>\n    <RouterView />\n    <Snapshots\n      data-test=\"snapshotsPage\"\n      class=\"snapshots-page\"\n    />\n  </div>\n</template>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/SudoPrompt.vue",
    "content": "<!--\n  - This is a modal dialog displayed before we ask the user for a sudo password\n  - to explain why we're asking for it.\n  -->\n\n<template>\n  <div class=\"contents\">\n    <h2>{{ t('sudoPrompt.title') }}</h2>\n    <p>{{ t('sudoPrompt.message', { }, true) }}</p>\n    <ul class=\"reasons\">\n      <li\n        v-for=\"(paths, reason) in explanations\"\n        :key=\"reason\"\n      >\n        <details>\n          <summary>{{ SUDO_REASON_DESCRIPTION[reason].title }}</summary>\n          <p>{{ SUDO_REASON_DESCRIPTION[reason].description.replace(/\\s+/g, ' ') }}</p>\n          <p>{{ t('sudoPrompt.explanation') }}</p>\n          <code>\n            <ul>\n              <li\n                v-for=\"path in paths\"\n                :key=\"path\"\n                class=\"monospace\"\n                v-text=\"path\"\n              />\n            </ul>\n          </code>\n        </details>\n      </li>\n    </ul>\n    <p>{{ t('sudoPrompt.messageSecondPart') }}</p>\n    <checkbox\n      id=\"suppress\"\n      v-model:value=\"suppress\"\n      label=\"Always run without administrative access\"\n    />\n    <button\n      ref=\"accept\"\n      class=\"role-primary primary-action\"\n      @click=\"close\"\n    >\n      {{ t('sudoPrompt.buttonText') }}\n    </button>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { Checkbox } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\ntype SudoReason = 'networking' | 'docker-socket';\n\n/**\n * SUDO_REASON_DESCRIPTION contains text on why we want sudo access.\n * @todo Put this in i18n\n */\nconst SUDO_REASON_DESCRIPTION: Record<SudoReason, { title: string, description: string }> = {\n  networking: {\n    title:       'Configure networking',\n    description: `Provides bridged networking so that it is easier to access your\n                  containers.  If this is not allowed, containers will only be accessible via\n                  port forwarding.`,\n  },\n  'docker-socket': {\n    title:       'Set up default docker socket',\n    description: 'Provides compatibility with tools that use the docker socket without the ability to use docker contexts.',\n  },\n};\n\nexport default defineComponent({\n  name:       'sudo-prompt-dialog',\n  components: { Checkbox },\n  layout:     'dialog',\n  data() {\n    return {\n      explanations: {} as Partial<Record<SudoReason, string[]>>,\n      sized:        false,\n      suppress:     false,\n      SUDO_REASON_DESCRIPTION,\n    };\n  },\n  mounted() {\n    ipcRenderer.on('dialog/populate', (event, explanations: Partial<Record<SudoReason, string[]>>) => {\n      this.explanations = explanations;\n    });\n    window.addEventListener('close', () => {\n      ipcRenderer.send('sudo-prompt/closed', this.suppress);\n    });\n    (this.$refs.accept as HTMLButtonElement)?.focus();\n  },\n  methods: {\n    close() {\n      // Manually send the result, because we won't get an event here.\n      ipcRenderer.send('sudo-prompt/closed', this.suppress);\n      window.close();\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\">\n  :root {\n    min-width: 30em;\n  }\n</style>\n\n<style lang=\"scss\" scoped>\n  .contents {\n    padding: 0.75rem;\n    min-width: 32rem;\n    max-width: 32rem;\n  }\n\n  summary {\n    user-select: none;\n    cursor: pointer;\n  }\n\n  li {\n    &, p {\n      margin: 0.5em;\n    }\n  }\n\n  ul.reasons {\n    list-style-type: none;\n    padding: 0;\n    margin: 0;\n  }\n\n  li.monospace {\n    /* font-family is set in _typography.scss */\n    white-space: pre;\n  }\n\n  .reasons code {\n    display: block;\n    overflow: auto;\n  }\n\n  code::-webkit-scrollbar-corner {\n    background: rgba(0,0,0,0.5);\n  }\n\n  #suppress {\n    margin: 0.5rem;\n  }\n\n  .primary-action {\n    align-self: flex-end;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Troubleshooting.vue",
    "content": "<template>\n  <div class=\"troubleshooting\">\n    <div class=\"troubleshooting-items\">\n      <troubleshooting-line-item>\n        <template #title>\n          <span class=\"text-xl\">\n            {{ t('troubleshooting.general.logs.title') }}\n          </span>\n        </template>\n        <template #description>\n          {{ t('troubleshooting.general.logs.description') }}\n        </template>\n        <template #actions>\n          <button\n            data-test=\"logsButton\"\n            type=\"button\"\n            class=\"btn btn-xs role-secondary\"\n            @click=\"showLogs\"\n          >\n            {{ t('troubleshooting.general.logs.buttonText') }}\n          </button>\n        </template>\n        <template #options>\n          <rd-checkbox\n            :value=\"isDebugging\"\n            :disabled=\"alwaysDebugging\"\n            :tooltip=\"debugModeTooltip\"\n            :is-locked=\"debugLocked\"\n            label=\"Enable debug mode\"\n            @update:value=\"updateDebug\"\n          />\n        </template>\n      </troubleshooting-line-item>\n      <troubleshooting-line-item>\n        <template #title>\n          <span class=\"text-xl\">\n            {{ t('troubleshooting.kubernetes.resetKubernetes.title') }}\n          </span>\n        </template>\n        <template #description>\n          {{ t('troubleshooting.kubernetes.resetKubernetes.description') }}\n        </template>\n        <template #actions>\n          <button\n            data-test=\"k8sResetBtn\"\n            type=\"button\"\n            class=\"btn btn-xs role-secondary\"\n            @click=\"resetKubernetes\"\n          >\n            {{ t('troubleshooting.kubernetes.resetKubernetes.buttonText') }}\n          </button>\n        </template>\n      </troubleshooting-line-item>\n      <troubleshooting-line-item>\n        <template #title>\n          <span class=\"text-xl\">\n            {{ t('troubleshooting.general.factoryReset.title') }}\n          </span>\n        </template>\n        <template #description>\n          {{ t('troubleshooting.general.factoryReset.description') }}\n        </template>\n        <template #actions>\n          <button\n            data-test=\"factoryResetButton\"\n            type=\"button\"\n            class=\"btn btn-xs btn-danger role-secondary\"\n            @click=\"factoryReset\"\n          >\n            {{ t('troubleshooting.general.factoryReset.buttonText') }}\n          </button>\n        </template>\n      </troubleshooting-line-item>\n    </div>\n    <div class=\"need-help\">\n      <hr>\n      <span\n        class=\"description\"\n        v-html=\"t('troubleshooting.needHelp', { }, true)\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\n\nimport _ from 'lodash';\n\nimport TroubleshootingLineItem from '@pkg/components/TroubleshootingLineItem.vue';\nimport RdCheckbox from '@pkg/components/form/RdCheckbox.vue';\nimport { defaultSettings } from '@pkg/config/settings';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default {\n  name:       'Troubleshooting',\n  title:      'Troubleshooting',\n  components: { TroubleshootingLineItem, RdCheckbox },\n  data:       () => ({\n    state:           ipcRenderer.sendSync('k8s-state'),\n    settings:        defaultSettings,\n    debugLocked:     false,\n    isDebugging:     false,\n    alwaysDebugging: false,\n  }),\n  computed: {\n    debugModeTooltip() {\n      return this.alwaysDebugging ? 'Cannot be modified because the RD_DEBUG_ENABLED environment variable is set.' : '';\n    },\n  },\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.t('troubleshooting.title') },\n    );\n    ipcRenderer.on('k8s-check-state', (_, newState) => {\n      this.$data.state = newState;\n    });\n    ipcRenderer.on('settings-read', (_, newSettings) => {\n      this.$data.settings = newSettings;\n      ipcRenderer.send('get-debugging-statuses');\n    });\n    ipcRenderer.on('settings-update', (_, newSettings) => {\n      this.$data.settings = newSettings;\n    });\n    ipcRenderer.on('is-debugging', (_, status) => {\n      this.$data.isDebugging = status;\n    });\n    ipcRenderer.on('always-debugging', (_, status) => {\n      this.$data.alwaysDebugging = status;\n    });\n    ipcRenderer.send('settings-read');\n    ipcRenderer.invoke('get-locked-fields').then((lockedFields) => {\n      this.$data.debugLocked = _.get(lockedFields, 'application.debug');\n    });\n    ipcRenderer.send('get-debugging-statuses');\n  },\n  methods: {\n    async factoryReset() {\n      const cancelPosition = 1;\n      const message = this.t('troubleshooting.general.factoryReset.messageBox.message');\n      const detail = this.t('troubleshooting.general.factoryReset.messageBox.detail', { }, true);\n\n      const confirm = await ipcRenderer.invoke(\n        'show-message-box-rd',\n        {\n          message,\n          detail,\n          type:            'question',\n          title:           this.t('troubleshooting.general.factoryReset.messageBox.title'),\n          checkboxLabel:   this.t('troubleshooting.general.factoryReset.messageBox.checkboxLabel'),\n          checkboxChecked: false,\n          buttons:         [\n            this.t('troubleshooting.general.factoryReset.messageBox.ok'),\n            this.t('troubleshooting.general.factoryReset.messageBox.cancel'),\n          ],\n          cancelId: cancelPosition,\n        },\n        true,\n      );\n\n      const { response, checkboxChecked: keepImages } = confirm;\n\n      if (response === cancelPosition) {\n        return;\n      }\n\n      ipcRenderer.send('factory-reset', keepImages);\n    },\n    showLogs() {\n      ipcRenderer.send('show-logs');\n    },\n    updateDebug(value) {\n      ipcRenderer.invoke('settings-write', { application: { debug: value } });\n      ipcRenderer.send('get-debugging-statuses');\n    },\n    async resetKubernetes() {\n      const cancelPosition = 1;\n      const message = this.t('troubleshooting.kubernetes.resetKubernetes.messageBox.message');\n      const detail = this.t('troubleshooting.kubernetes.resetKubernetes.description');\n\n      const confirm = await ipcRenderer.invoke(\n        'show-message-box-rd',\n        {\n          message,\n          detail,\n          type:            'question',\n          title:           this.t('troubleshooting.kubernetes.resetKubernetes.messageBox.title'),\n          checkboxLabel:   this.t('troubleshooting.kubernetes.resetKubernetes.messageBox.checkboxLabel'),\n          checkboxChecked: false,\n          buttons:         [\n            this.t('troubleshooting.kubernetes.resetKubernetes.messageBox.ok'),\n            this.t('troubleshooting.kubernetes.resetKubernetes.messageBox.cancel'),\n          ],\n          cancelId: cancelPosition,\n        },\n        true,\n      );\n\n      const { response, checkboxChecked } = confirm;\n\n      if (response === cancelPosition) {\n        return;\n      }\n\n      ipcRenderer.send('k8s-reset', checkboxChecked ? 'wipe' : 'fast');\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n  .troubleshooting-items {\n    display: flex;\n    flex-direction: column;\n  }\n\n  .text-xl {\n    font-size: 1.25rem;\n    line-height: 1.75rem;\n  }\n\n  .btn-xs {\n    min-height: 2.25rem;\n    max-height: 2.25rem;\n    line-height: 0.25rem;\n  }\n\n  button.btn-danger {\n    color: var(--error) !important;\n    border-color: var(--error);\n  }\n\n  .description {\n    line-height: 0.50rem;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/UnmetPrerequisites.vue",
    "content": "<template>\n  <div class=\"container\">\n    <h2>{{ t('unmetPrerequisites.title') }}</h2>\n    <p>{{ t('unmetPrerequisites.message') }}</p>\n    <ul>\n      <li>{{ reason }}</li>\n    </ul>\n    <p>{{ t('unmetPrerequisites.action') }}</p>\n    <div class=\"button-area\">\n      <button\n        data-test=\"accept-btn\"\n        class=\"role-primary\"\n        @click=\"close\"\n      >\n        {{ t('unmetPrerequisites.buttonText') }}\n      </button>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from 'vue';\n\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport type { WSLVersionInfo } from '@pkg/utils/wslVersion';\nimport type { reqMessageId } from '@pkg/window';\n\nfunction describeReason(reasonId: Exclude<reqMessageId, 'win32-kernel'>): string;\nfunction describeReason(reasonId: 'win32-kernel', version: WSLVersionInfo): string;\nfunction describeReason(reasonId: reqMessageId, ...extras: any[]): string {\n  switch (reasonId) {\n  case 'win32-release':\n    return 'Requires Windows version 10-1909 or newer';\n  case 'win32-kernel': {\n    const version: WSLVersionInfo = extras[0];\n    const {\n      major, minor, build, revision,\n    } = version.kernel_version;\n    const kernelString = [major, minor, build, revision].join('.');\n\n    return `Requires WSL with kernel 5.15 or newer (have ${ kernelString })`;\n  }\n  case 'macOS-release':\n    return 'Requires macOS version 10.15 or newer';\n  case 'linux-nested':\n    return 'Nested virtualization not enabled on this host';\n  }\n\n  return `Reason ${ reasonId } is unknown`;\n}\n\nexport default defineComponent({\n  name:   'unmet-prerequisites-dialog',\n  layout: 'dialog',\n  data() {\n    return {\n      reason:   '',\n      suppress: false,\n    };\n  },\n  mounted() {\n    ipcRenderer.on('dialog/populate', (event, ...args: Parameters<typeof describeReason>) => {\n      this.$data.reason = describeReason(...args);\n    });\n  },\n  methods: {\n    close() {\n      window.close();\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n  .container {\n    min-width: 30rem;\n  }\n  .button-area {\n    align-self: flex-end;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/Volumes.vue",
    "content": "<template>\n  <div class=\"volumes\">\n    <banner\n      v-if=\"errorMessage\"\n      color=\"error\"\n      data-testid=\"error-banner\"\n      @close=\"clearError\"\n    >\n      {{ errorMessage }}\n    </banner>\n    <SortableTable\n      class=\"volumesTable\"\n      data-testid=\"volumes-table\"\n      :headers=\"headers\"\n      key-field=\"Name\"\n      :rows=\"rows\"\n      no-rows-key=\"volumes.sortableTables.noRows\"\n      :row-actions=\"true\"\n      :paging=\"true\"\n      :rows-per-page=\"10\"\n      :has-advanced-filtering=\"false\"\n      :loading=\"!volumes\"\n    >\n      <template #header-middle>\n        <div class=\"header-middle\">\n          <div v-if=\"supportsNamespaces\">\n            <label>Namespace</label>\n            <select\n              :value=\"namespace\"\n              class=\"select-namespace\"\n              data-testid=\"namespace-selector\"\n              @change=\"onChangeNamespace($event)\"\n            >\n              <option\n                v-for=\"item in namespaces ?? []\"\n                :key=\"item\"\n                :selected=\"item === namespace\"\n                :value=\"item\"\n              >\n                {{ item }}\n              </option>\n            </select>\n          </div>\n        </div>\n      </template>\n      <template #col:Name=\"{ row } : { row: RowItem }\">\n        <td data-testid=\"volume-name-cell\">\n          <span v-tooltip=\"getTooltipConfig(row.Name)\">\n            {{ shortSha(row.Name) }}\n          </span>\n        </td>\n      </template>\n      <template #col:Driver=\"{ row } : { row: RowItem }\">\n        <td data-testid=\"volume-driver-cell\">\n          {{ row.Driver }}\n        </td>\n      </template>\n      <template #col:Mountpoint=\"{ row } : { row: RowItem }\">\n        <td data-testid=\"volume-mountpoint-cell\">\n          <span v-tooltip=\"getTooltipConfig(row.Mountpoint)\">\n            {{ shortPath(row.Mountpoint) }}\n          </span>\n        </td>\n      </template>\n      <template #col:Created=\"{ row } : { row: RowItem }\">\n        <td data-testid=\"volume-created-cell\">\n          {{ row.createdText }} <!-- use the text representation -->\n        </td>\n      </template>\n    </SortableTable>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { Banner } from '@rancher/components';\nimport merge from 'lodash/merge';\nimport { defineComponent } from 'vue';\n\nimport SortableTable from '@pkg/components/SortableTable';\nimport type { Settings } from '@pkg/config/settings';\nimport { mapTypedGetters, mapTypedState } from '@pkg/entry/store';\nimport type { Volume } from '@pkg/store/container-engine';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nconst MAX_PATH_LENGTH = 40;\n\n/**\n * The RowItem type describes the type of one row.\n */\ninterface RowItem extends Volume {\n  createdText:      string;\n  availableActions: {\n    label:       string;\n    action:      string;\n    enabled:     boolean;\n    bulkable:    boolean;\n    bulkAction?: string;\n  }[];\n  deleteVolume: (items?: RowItem[]) => void;\n  browseFiles:  (items?: RowItem[]) => void;\n}\n\nexport default defineComponent({\n  name:       'Volumes',\n  title:      'Volumes',\n  components: { SortableTable, Banner },\n  data() {\n    return {\n      settings:       undefined as Settings | undefined,\n      subscribeTimer: undefined as ReturnType<typeof setTimeout> | undefined,\n      execError:      null as string | null,\n      headers:        [\n        {\n          name:  'Name',\n          label: this.t('volumes.manage.table.header.volumeName'),\n          sort:  ['Name'],\n        },\n        {\n          name:  'Driver',\n          label: this.t('volumes.manage.table.header.driver'),\n          sort:  ['Driver', 'Name'],\n        },\n        {\n          name:  'Mountpoint',\n          label: this.t('volumes.manage.table.header.mountpoint'),\n          sort:  ['Mountpoint', 'Name'],\n        },\n        {\n          name:  'Created',\n          label: this.t('volumes.manage.table.header.created'),\n          sort:  ['Created', 'Name'],\n          width: 120,\n        },\n      ],\n    };\n  },\n  computed: {\n    ...mapTypedGetters('k8sManager', { isK8sReady: 'isReady' }),\n    ...mapTypedState('container-engine', ['namespaces', 'volumes']),\n    ...mapTypedGetters('container-engine', ['namespace', 'supportsNamespaces', 'error']),\n    rows(): RowItem[] {\n      return Object.values(this.volumes ?? {})\n        .sort((a, b) => a.Name.localeCompare(b.Name))\n        .map(volume => merge({}, volume, {\n          createdText:          volume.CreatedAt ? new Date(volume.CreatedAt).toLocaleDateString() : '',\n          availableActions: [\n            {\n              label:    this.t('volumes.manager.table.action.browse'),\n              action:   'browseFiles',\n              enabled:  true,\n              bulkable: false,\n            },\n            {\n              label:      this.t('volumes.manager.table.action.delete'),\n              action:     'deleteVolume',\n              enabled:    true,\n              bulkable:   true,\n              bulkAction: 'deleteVolume',\n            },\n          ],\n          deleteVolume: (args?: Volume[]) => {\n            this.execCommand(['volume', 'rm'], Array.isArray(args) ? args : [volume]);\n          },\n          browseFiles: () => {\n            this.$router.push({ name: 'volumes-files-name', params: { name: volume.Name } });\n          },\n        }));\n    },\n    errorMessage() {\n      if (this.execError) {\n        return this.execError;\n      }\n      switch (this.error?.source) {\n      case 'namespaces': case 'volumes': {\n        const error: any = this.error.error;\n\n        return `${ error?.stderr ?? error }`;\n      }\n      }\n      return null;\n    },\n  },\n  mounted() {\n    this.$store.dispatch('page/setHeader', {\n      title:       this.t('volumes.title'),\n      description: '',\n    });\n\n    ipcRenderer.on('settings-read', (event, settings) => {\n      this.settings = settings;\n      this.subscribe().catch(console.error);\n    });\n\n    ipcRenderer.send('settings-read');\n\n    ipcRenderer.on('settings-update', (_event, settings) => {\n      this.settings = settings;\n      this.checkSelectedNamespace();\n    });\n\n    this.subscribe().catch(console.error);\n  },\n  beforeUnmount() {\n    this.$store.dispatch('container-engine/unsubscribe');\n    clearTimeout(this.subscribeTimer);\n  },\n  methods: {\n    async subscribe() {\n      clearTimeout(this.subscribeTimer);\n      try {\n        if (!window.ddClient || !this.isK8sReady || !this.settings) {\n          setTimeout(() => this.subscribe(), 1_000);\n          return;\n        }\n        await this.$store.dispatch('container-engine/subscribe', {\n          type:      'volumes',\n          client:    window.ddClient,\n        });\n      } catch (error) {\n        console.error('There was a problem subscribing to container events:', { error });\n      }\n    },\n    checkSelectedNamespace() {\n      if (!this.supportsNamespaces || !this.namespaces?.length) {\n        return;\n      }\n      if (!this.namespaces.includes(this.namespace ?? '')) {\n        const K8S_NAMESPACE = 'k8s.io';\n        const defaultNamespace = this.namespaces.includes(K8S_NAMESPACE) ? K8S_NAMESPACE : this.namespaces[0];\n\n        ipcRenderer.invoke('settings-write',\n          { containers: { namespace: defaultNamespace } });\n      }\n    },\n    async onChangeNamespace(event: Event) {\n      const { value } = event.target as HTMLSelectElement;\n      if (value !== this.namespace) {\n        await ipcRenderer.invoke('settings-write',\n          { containers: { namespace: value } });\n        await this.$store.dispatch('container-engine/subscribe', {\n          type:      'volumes',\n          client:    window.ddClient,\n          namespace: value,\n        });\n      }\n    },\n    async execCommand(args: string[], volumes: Volume[]) {\n      try {\n        const names = volumes.map(v => v.Name);\n        const [baseCommand, ...subCommands] = args;\n\n        console.info(`Executing command ${ args.join(' ') } on volume ${ names }`);\n\n        const execOptions: { cwd: string, namespace?: string } = { cwd: '/' };\n        if (this.supportsNamespaces && this.namespace) {\n          execOptions.namespace = this.namespace;\n        }\n\n        const { stderr, stdout } = await window.ddClient.docker.cli.exec(\n          baseCommand,\n          [...subCommands, ...names],\n          execOptions,\n        );\n\n        if (stderr) {\n          throw new Error(stderr);\n        }\n\n        await this.$store.dispatch('container-engine/fetchVolumes');\n\n        return stdout;\n      } catch (error: any) {\n        const errorSources = [\n          error?.message,\n          error?.stderr,\n          error?.error,\n          typeof error === 'string' ? error : null,\n          `Failed to execute command: ${ args.join(' ') }`,\n        ];\n\n        this.execError = errorSources.find(msg => msg);\n        console.error(`Error executing command ${ args.join(' ') }`, error);\n      }\n    },\n    shortSha(sha: string) {\n      if (!sha?.startsWith('sha256:')) return sha || '';\n\n      const hash = sha.replace('sha256:', '');\n      return `sha256:${ hash.slice(0, 3) }..${ hash.slice(-3) }`;\n    },\n    shortPath(path: string) {\n      if (!path || path.length <= MAX_PATH_LENGTH) {\n        return path || '';\n      }\n\n      return `${ path.slice(0, 20) }...${ path.slice(-17) }`;\n    },\n    getTooltipConfig(text: string) {\n      if (!text) {\n        return { content: undefined };\n      }\n\n      // Show tooltip for sha256 hashes or long paths\n      if (text.startsWith('sha256:') || text.length > MAX_PATH_LENGTH) {\n        return { content: text };\n      }\n\n      return { content: undefined };\n    },\n    clearError() {\n      this.execError = null;\n      switch (this.error?.source) {\n      case 'namespaces': case 'volumes':\n        this.$store.commit('container-engine/SET_ERROR', null);\n      }\n    },\n  },\n  watch: {\n    isK8sReady(isK8sReady) {\n      if (!isK8sReady) {\n        // The backend went from ready -> unready, unsubscribe and restart.\n        this.$store.dispatch('container-engine/unsubscribe').catch(console.error);\n        this.subscribe().catch(console.error);\n      }\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.volumes {\n  &-status {\n    padding: 8px 5px;\n  }\n}\n\n.select-namespace {\n  max-width: 24rem;\n  min-width: 8rem;\n}\n\n.volumesTable:v-deep(.search-box) {\n  align-self: flex-end;\n}\n.volumesTable:v-deep(.bulk) {\n  align-self: flex-end;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/containers/ContainerInfo.vue",
    "content": "<template>\n  <div\n    class=\"container-info-page\"\n    data-testid=\"container-info\"\n  >\n    <rd-tabbed\n      :key=\"containerId\"\n      :flat=\"true\"\n    >\n      <!--\n        Tab components are used only to register headers and emit @active events; their slots\n        are intentionally empty. Content is rendered in .tab-content below so we can mix\n        v-if (destroy/recreate on switch) with v-show (preserve the shell terminal's DOM and\n        pty process across tab switches).\n      -->\n      <tab\n        label=\"Logs\"\n        name=\"tab-logs\"\n        :weight=\"1\"\n        @active=\"activeTab = 'tab-logs'\"\n      />\n      <tab\n        label=\"Shell\"\n        name=\"tab-shell\"\n        :weight=\"0\"\n        :disabled=\"!isRunning\"\n        @active=\"activeTab = 'tab-shell'\"\n      />\n      <template #tab-row-extras>\n        <li\n          v-if=\"activeTab === 'tab-logs'\"\n          class=\"search-widget\"\n          data-testid=\"search-widget\"\n        >\n          <input\n            ref=\"searchInput\"\n            v-model=\"searchTerm\"\n            aria-label=\"Search in logs\"\n            class=\"search-input\"\n            data-testid=\"search-input\"\n            placeholder=\"Search logs...\"\n            type=\"search\"\n            @input=\"onSearchInput\"\n            @keydown=\"handleSearchKeydown\"\n          >\n          <button\n            :disabled=\"!searchTerm\"\n            aria-label=\"Previous match\"\n            class=\"search-btn btn role-tertiary\"\n            data-testid=\"search-prev-btn\"\n            title=\"Previous match\"\n            @click=\"searchPrevious\"\n          >\n            <i\n              aria-hidden=\"true\"\n              class=\"icon icon-chevron-up\"\n            />\n          </button>\n          <button\n            :disabled=\"!searchTerm\"\n            aria-label=\"Next match\"\n            class=\"search-btn btn role-tertiary\"\n            data-testid=\"search-next-btn\"\n            title=\"Next match\"\n            @click=\"searchNext\"\n          >\n            <i\n              aria-hidden=\"true\"\n              class=\"icon icon-chevron-down\"\n            />\n          </button>\n          <button\n            :disabled=\"!searchTerm\"\n            aria-label=\"Clear search\"\n            class=\"search-btn btn role-tertiary\"\n            data-testid=\"search-clear-btn\"\n            title=\"Clear search\"\n            @click=\"clearSearch\"\n          >\n            <i\n              aria-hidden=\"true\"\n              class=\"icon icon-x\"\n            />\n          </button>\n        </li>\n      </template>\n      <div class=\"tab-content\">\n        <container-logs\n          v-if=\"containerId && activeTab === 'tab-logs'\"\n          ref=\"containerLogs\"\n          :container-id=\"containerId\"\n          :is-container-running=\"isRunning\"\n          :namespace=\"namespace\"\n        />\n        <container-shell\n          v-if=\"shellEverActivated && containerId\"\n          v-show=\"activeTab === 'tab-shell'\"\n          ref=\"containerShell\"\n          :container-id=\"containerId\"\n          :is-container-running=\"isRunning\"\n          :namespace=\"namespace\"\n        />\n      </div>\n    </rd-tabbed>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { useStore } from 'vuex';\n\nimport ContainerLogs from '@pkg/components/ContainerLogs.vue';\nimport ContainerShell from '@pkg/components/ContainerShell.vue';\nimport RdTabbed from '@pkg/components/Tabbed/RdTabbed.vue';\nimport Tab from '@pkg/components/Tabbed/Tab.vue';\nimport type { Settings } from '@pkg/config/settings';\nimport type { Container } from '@pkg/store/container-engine';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\n// Router and Store\nconst route = useRoute();\nconst store = useStore();\n\n// Template refs with proper typing\nconst containerLogs = ref<InstanceType<typeof ContainerLogs> | null>(null);\nconst containerShell = ref<InstanceType<typeof ContainerShell> | null>(null);\nconst searchInput = ref<HTMLInputElement | null>(null);\n\n// Reactive data\nconst settings = ref<Settings>();\nconst subscribeTimer = ref<ReturnType<typeof setTimeout>>();\nconst searchTerm = ref('');\nconst activeTab = ref<'tab-logs' | 'tab-shell'>('tab-logs');\nconst shellEverActivated = ref(false);\n\n// Vuex integration\nconst isK8sReady = computed(() => store.getters['k8sManager/isReady']);\nconst containers = computed(() => store.state['container-engine'].containers);\nconst supportsNamespaces = computed(() => store.getters['container-engine/supportsNamespaces']);\nconst namespace = computed(() => supportsNamespaces.value ? settings.value?.containers?.namespace : undefined);\n\n// Computed properties\nconst containerId = computed(() => route.params.id as string || '');\n\nconst currentContainer = computed((): Container | null => {\n  if (!containers.value || !containerId.value) {\n    return null;\n  }\n  return containers.value[containerId.value] || null;\n});\n\nconst containerName = computed(() => {\n  if (!currentContainer.value) {\n    return containerId.value.substring(0, 12);\n  }\n  const name = currentContainer.value.containerName;\n  return name.replace(/^\\//, '') || containerId.value.substring(0, 12);\n});\n\nconst isRunning = computed(() => {\n  if (!currentContainer.value) {\n    return false;\n  }\n  return currentContainer.value.state === 'running';\n});\n\n// Watchers\nwatch(containerName, (name) => {\n  store.dispatch('page/setHeader', {\n    title:       name || 'Container Info',\n    description: '',\n    action:      'ContainerStatusBadge',\n  });\n}, { immediate: true });\n\nwatch(activeTab, (tab) => {\n  if (tab === 'tab-shell') {\n    shellEverActivated.value = true;\n    nextTick(() => containerShell.value?.focus());\n  }\n});\n\n// Methods as functions\nconst subscribe = async() => {\n  if (subscribeTimer.value) {\n    clearTimeout(subscribeTimer.value);\n  }\n  try {\n    if (!window.ddClient || !isK8sReady.value || !settings.value) {\n      subscribeTimer.value = setTimeout(subscribe, 1_000);\n      return;\n    }\n    await store.dispatch('container-engine/subscribe', {\n      type:   'containers',\n      client: window.ddClient,\n    });\n  } catch (error) {\n    console.error('There was a problem subscribing to container events:', { error });\n  }\n};\n\nconst onSearchInput = () => {\n  containerLogs.value?.performSearch(searchTerm.value);\n};\n\nconst searchNext = () => {\n  containerLogs.value?.searchNext(searchTerm.value);\n};\n\nconst searchPrevious = () => {\n  containerLogs.value?.searchPrevious(searchTerm.value);\n};\n\nconst clearSearch = () => {\n  searchTerm.value = '';\n  containerLogs.value?.clearSearch();\n  nextTick(() => {\n    searchInput.value?.focus();\n  });\n};\n\nconst handleSearchKeydown = (event: KeyboardEvent) => {\n  if (event.key === 'Enter') {\n    if (event.shiftKey) {\n      searchPrevious();\n    } else {\n      searchNext();\n    }\n    event.preventDefault();\n  } else if (event.key === 'Escape') {\n    clearSearch();\n    event.preventDefault();\n  }\n};\n\nconst handleGlobalKeydown = (event: KeyboardEvent) => {\n  if (event.key === '/') {\n    // Don't trigger if search input is already focused\n    if (!searchInput.value?.contains(document.activeElement)) {\n      event.preventDefault();\n      searchInput.value?.focus();\n      searchInput.value?.select();\n    }\n  }\n};\n\n// Event handlers\nconst handleSettingsUpdate = (_event: any, settingsData: any) => {\n  settings.value = settingsData;\n};\n\nconst handleSettingsRead = (_event: any, settingsData: any) => {\n  settings.value = settingsData;\n  subscribe().catch(console.error);\n};\n\n// Lifecycle hooks\nonMounted(() => {\n  ipcRenderer.send('settings-read');\n\n  ipcRenderer.on('settings-update', handleSettingsUpdate);\n  ipcRenderer.on('settings-read', handleSettingsRead);\n\n  subscribe().catch(console.error);\n\n  window.addEventListener('keydown', handleGlobalKeydown);\n});\n\nonBeforeUnmount(() => {\n  store.dispatch('page/setHeader', { action: null });\n  ipcRenderer.removeListener('settings-update', handleSettingsUpdate);\n  ipcRenderer.removeListener('settings-read', handleSettingsRead);\n  store.dispatch('container-engine/unsubscribe').catch(console.error);\n  if (subscribeTimer.value) {\n    clearTimeout(subscribeTimer.value);\n  }\n  window.removeEventListener('keydown', handleGlobalKeydown);\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.container-info-page {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  overflow: hidden;\n  min-height: 0;\n}\n\n.search-widget {\n  margin-left: auto;\n  list-style: none;\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n  padding: 0.25rem 0.5rem;\n  flex-shrink: 0;\n}\n\n.search-input {\n  border: 1px solid var(--border);\n  border-radius: var(--border-radius);\n  background: var(--input-bg);\n  color: var(--body-text);\n  font-size: 13px;\n  padding: 0 0.75rem;\n  min-width: 200px;\n  height: 32px;\n  transition: border-color 0.2s ease;\n\n  &::placeholder {\n    color: var(--muted);\n  }\n\n  &:focus {\n    border-color: var(--primary);\n    outline: none;\n  }\n}\n\n.search-btn {\n  background: transparent;\n  border: 1px solid var(--border);\n  border-radius: var(--border-radius);\n  padding: 0;\n  cursor: pointer;\n  color: var(--body-text);\n  transition: all 0.2s ease;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  min-width: 32px;\n  min-height: 32px;\n\n  &:hover:not(:disabled) {\n    background: var(--primary);\n    border-color: var(--primary);\n    color: var(--primary-text);\n  }\n\n  &:disabled {\n    opacity: 0.3;\n    cursor: not-allowed;\n  }\n\n  &:focus-visible {\n    outline: 2px solid var(--primary);\n    outline-offset: -2px;\n  }\n\n  .icon {\n    font-size: 12px;\n  }\n}\n\n.tab-content {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n  overflow: hidden;\n}\n\n:deep(.container-logs-component),\n:deep(.container-shell-component) {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/extensions/_root/_src/_id.vue",
    "content": "<script lang=\"ts\">\n\nimport { ipcRenderer } from 'electron';\nimport { defineComponent } from 'vue';\n\nimport ExtensionsError from '@pkg/components/ExtensionsError.vue';\nimport ExtensionsUninstalled from '@pkg/components/ExtensionsUninstalled.vue';\nimport { hexDecode } from '@pkg/utils/string-encode';\n\ninterface ExtensionsData {\n  error:           Error | undefined;\n  isExtensionGone: boolean;\n}\n\nexport default defineComponent({\n  name:       'extension-ui',\n  components: { ExtensionsError, ExtensionsUninstalled },\n  beforeRouteEnter(to, _from, next) {\n    const { params: { root, src, id } } = to;\n\n    next((vm: any) => {\n      vm.openExtension(id, root, src);\n    });\n  },\n  beforeRouteUpdate(to, _from, next) {\n    const { params: { root, src, id } } = to;\n\n    this.openExtension(id, root, src);\n\n    next();\n  },\n  beforeRouteLeave(_to, _from, next) {\n    this.closeExtensionView();\n    next();\n  },\n  data(): ExtensionsData {\n    return {\n      error:           undefined,\n      isExtensionGone: false,\n    };\n  },\n  computed: {\n    extensionId(): string | undefined {\n      return hexDecode(this.$route.params.id);\n    },\n  },\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.extensionId },\n    );\n\n    ipcRenderer.on('err:extensions/open', this.extensionError);\n    ipcRenderer.on('ok:extensions/uninstall', this.extensionUninstalled);\n  },\n  beforeUnmount() {\n    ipcRenderer.off('err:extensions/open', this.extensionError);\n  },\n  methods: {\n    openExtension(id: string, root: string, src: string): void {\n      ipcRenderer.send('extensions/open', id, `${ root }/${ src }`);\n    },\n    closeExtensionView(): void {\n      ipcRenderer.send('extensions/close');\n    },\n    extensionError(_event: any, err: Error): void {\n      this.error = err;\n    },\n    extensionUninstalled(_event: any, extensionId: string): void {\n      if (!this.extensionId) {\n        return;\n      }\n\n      if (extensionId.startsWith(this.extensionId)) {\n        this.isExtensionGone = true;\n        this.closeExtensionView();\n      }\n    },\n    browseCatalog() {\n      this.$router.push({ name: 'Extensions' });\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"extensions-container\">\n    <extensions-uninstalled\n      v-if=\"isExtensionGone\"\n      :extension-id=\"extensionId\"\n      @click:browse=\"browseCatalog\"\n    />\n    <extensions-error\n      v-if=\"error\"\n      :error=\"error\"\n      :extension-id=\"extensionId\"\n    />\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .extensions-container {\n    padding: 0 6rem;\n    max-width: 64rem;\n    justify-self: center;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/extensions/installed.vue",
    "content": "<script lang=\"ts\">\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport EmptyState from '@pkg/components/EmptyState.vue';\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\nimport NavIconExtension from '@pkg/components/NavIconExtension.vue';\nimport SortableTable from '@pkg/components/SortableTable/index.vue';\nimport useCredentials from '@pkg/hocs/withCredentials';\nimport type { ExtensionState } from '@pkg/store/extensions';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'extensions-installed',\n  components: {\n    LoadingIndicator, NavIconExtension, SortableTable, EmptyState,\n  },\n  setup() {\n    useCredentials();\n  },\n  data() {\n    return {\n      headers: [\n        {\n          name:  'icon',\n          label: ' ',\n          width: 35,\n        },\n        {\n          name:  'id',\n          label: 'Name',\n        },\n        {\n          name:  'actions',\n          label: ' ',\n          width: 76,\n        },\n      ],\n      loading: true,\n      busy:    {} as Record<string, boolean>,\n    };\n  },\n  computed: {\n    emptyStateIcon(): string {\n      return this.t('extensions.installed.emptyState.icon');\n    },\n    emptyStateHeading(): string {\n      return this.t('extensions.installed.emptyState.heading');\n    },\n    emptyStateBody(): string {\n      return this.t('extensions.installed.emptyState.body', { }, true);\n    },\n    ...mapGetters('extensions', ['installedExtensions']) as {\n      installedExtensions: () => ExtensionState[],\n    },\n  },\n  async beforeMount() {\n    ipcRenderer.on('extensions/changed', () => {\n      this.$store.dispatch('extensions/fetch');\n    });\n    await this.$store.dispatch('extensions/fetch');\n    this.loading = false;\n  },\n  methods: {\n    browseExtensions() {\n      this.$emit('click:browse');\n    },\n    extensionTitle(ext: { id: string, labels: Record<string, string> }): string {\n      return ext.labels?.['org.opencontainers.image.title'] ?? ext.id;\n    },\n    async uninstall(installed: ExtensionState) {\n      this.busy = { ...this.busy, [installed.id]: true };\n      try {\n        await this.$store.dispatch('extensions/uninstall', { id: installed.id });\n      } finally {\n        const { [installed.id]: _, ...rest } = this.busy;\n\n        this.busy = rest;\n      }\n    },\n    async upgrade(installed: ExtensionState) {\n      const id = `${ installed.id }:${ installed.availableVersion }`;\n\n      if (!installed.availableVersion) {\n        // Should not have reached here.\n        return;\n      }\n      this.busy = { ...this.busy, [installed.id]: true };\n      try {\n        await this.$store.dispatch('extensions/install', { id });\n      } finally {\n        const { [installed.id]: _, ...rest } = this.busy;\n\n        this.busy = rest;\n      }\n    },\n  },\n});\n</script>\n\n<template>\n  <div>\n    <sortable-table\n      key-field=\"id\"\n      :loading=\"loading\"\n      :headers=\"headers\"\n      :rows=\"installedExtensions\"\n      :search=\"false\"\n      :table-actions=\"false\"\n      :row-actions=\"false\"\n    >\n      <template #no-rows>\n        <td :colspan=\"headers.length + 1\">\n          <empty-state\n            :icon=\"emptyStateIcon\"\n            :heading=\"emptyStateHeading\"\n            :body=\"emptyStateBody\"\n          >\n            <template #primary-action>\n              <button\n                class=\"btn role-primary\"\n                @click=\"browseExtensions\"\n              >\n                {{ t('extensions.installed.emptyState.button.text') }}\n              </button>\n            </template>\n          </empty-state>\n        </td>\n      </template>\n      <template #col:icon=\"{ row }\">\n        <td>\n          <nav-icon-extension :extension-id=\"row.id\" />\n        </td>\n      </template>\n      <template #col:id=\"{ row }\">\n        <td>\n          {{ extensionTitle(row) }}\n        </td>\n      </template>\n      <template #col:actions=\"{ row }\">\n        <td>\n          <div class=\"actions\">\n            <span\n              v-if=\"busy[row.id]\"\n              name=\"busy\"\n              :is-loading=\"busy\"\n            >\n              <loading-indicator />\n            </span>\n            <button\n              v-if=\"!busy[row.id] && row.canUpgrade\"\n              class=\"btn btn-sm role-primary\"\n              @click.stop=\"upgrade(row)\"\n            >\n              {{ t('extensions.installed.list.upgrade') }}\n            </button>\n            <button\n              :disabled=\"busy[row.id]\"\n              class=\"btn btn-sm role-danger\"\n              @click.stop=\"uninstall(row)\"\n            >\n              {{ t('extensions.installed.list.uninstall') }}\n            </button>\n          </div>\n        </td>\n      </template>\n    </sortable-table>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.actions {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: end;\n  & > * {\n    margin-left: 10px;\n  }\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/images/add.vue",
    "content": "<template>\n  <div>\n    <image-add-tabs @click=\"updateTabs\">\n      <div class=\"image-input\">\n        <images-form-add\n          :current-command=\"currentCommand\"\n          :keep-output-window-open=\"showOutput\"\n          :action=\"activeTab\"\n          @click=\"doImageAction\"\n        />\n      </div>\n      <alert\n        v-if=\"allowedImagesAlert\"\n        :icon=\"'icon-info-circle'\"\n        :banner-text=\"allowedImagesAlert\"\n        :color=\"'info'\"\n      />\n    </image-add-tabs>\n    <template v-if=\"showOutput\">\n      <hr>\n      <images-output-window\n        ref=\"image-output-window\"\n        :current-command=\"currentCommand\"\n        :action=\"activeTab\"\n        :image-output-culler=\"imageOutputCuller\"\n        :image-to-pull=\"imageToPull\"\n        @ok:process-end=\"resetCurrentCommand\"\n        @ok:show=\"toggleOutput\"\n      />\n    </template>\n  </div>\n</template>\n\n<script>\n\nimport Alert from '@pkg/components/Alert.vue';\nimport ImageAddTabs from '@pkg/components/ImageAddTabs.vue';\nimport ImagesFormAdd from '@pkg/components/ImagesFormAdd.vue';\nimport ImagesOutputWindow from '@pkg/components/ImagesOutputWindow.vue';\nimport getImageOutputCuller from '@pkg/utils/imageOutputCuller';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default {\n  components: {\n    Alert,\n    ImageAddTabs,\n    ImagesFormAdd,\n    ImagesOutputWindow,\n  },\n  data() {\n    return {\n      activeTab:              'pull',\n      currentCommand:         null,\n      imageToPull:            '',\n      imageOutputCuller:      null,\n      showOutput:             false,\n      isAllowedImagesEnabled: false,\n    };\n  },\n  computed: {\n    imageToPullButtonDisabled() {\n      return this.imageToPullTextFieldIsDisabled || !this.imageToPull;\n    },\n    imageToPullTextFieldIsDisabled() {\n      return this.currentCommand;\n    },\n    allowedImagesAlert() {\n      return this.activeTab === 'pull' && this.isAllowedImagesEnabled ? this.t('allowedImages.alert') : '';\n    },\n  },\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.t('images.add.title') },\n    );\n    ipcRenderer.once('settings-read', (_event, settings) => {\n      this.enableAllowedImages(settings);\n    });\n    ipcRenderer.on('settings-update', (_event, settings) => {\n      this.enableAllowedImages(settings);\n    });\n    ipcRenderer.send('settings-read');\n  },\n  methods: {\n    updateTabs(tabName) {\n      this.showOutput = false;\n      this.activeTab = tabName;\n    },\n    startRunningCommand(command) {\n      this.imageOutputCuller = getImageOutputCuller(command);\n    },\n    doImageAction({ action, image }) {\n      this.imageToPull = image;\n      if (action === 'pull') {\n        this.doPullAnImage();\n      }\n\n      if (action === 'build') {\n        this.doBuildAnImage();\n      }\n    },\n    doPullAnImage() {\n      const imageName = this.imageToPull;\n\n      this.currentCommand = `pull ${ imageName }`;\n      this.startRunningCommand('pull');\n      ipcRenderer.send('do-image-pull', imageName);\n      this.showOutput = true;\n    },\n    doBuildAnImage() {\n      const imageName = this.imageToPull;\n\n      this.currentCommand = `build ${ imageName }`;\n      this.startRunningCommand('build');\n      ipcRenderer.send('do-image-build', imageName);\n      this.showOutput = true;\n    },\n    resetCurrentCommand() {\n      this.currentCommand = null;\n    },\n    toggleOutput(val) {\n      this.showOutput = val;\n    },\n    enableAllowedImages(settings) {\n      this.isAllowedImagesEnabled = settings.containerEngine.allowedImages.enabled;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n  div .image-input {\n    display: flex;\n    align-items: center;\n    gap: 16px;\n    padding-top: 0.5rem;\n    margin-left: 1px;\n  }\n\n  .image-input :deep(.labeled-input) {\n    min-height: 42px;\n    padding: 8px;\n  }\n\n  .actions {\n    margin-top: 15px;\n    margin-bottom: 15px;\n    display: flex;\n    flex-flow: row-reverse;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/images/scans/_image-name.vue",
    "content": "<template>\n  <div class=\"image-output-container\">\n    <images-output-window\n      v-if=\"showOutput\"\n      :current-command=\"currentCommand\"\n      :image-output-culler=\"imageOutputCuller\"\n      :image-to-pull=\"image\"\n      @ok:process-end=\"onProcessEnd\"\n    >\n      <template #loading=\"{ isLoading }\">\n        <banner\n          v-if=\"isLoading\"\n        >\n          <loading-indicator>\n            {{ loadingText }}\n          </loading-indicator>\n        </banner>\n      </template>\n      <template #error=\"{ hasError }\">\n        <banner\n          v-if=\"hasError\"\n          color=\"error\"\n        >\n          {{ hasError }}\n          <span class=\"icon icon-info-circle icon-lg \" />\n          {{ errorText }}\n        </banner>\n      </template>\n    </images-output-window>\n    <images-scan-results\n      v-if=\"isFinished && isFinishedWithSuccess\"\n      :image=\"image\"\n      :table-data=\"vulnerabilities\"\n    />\n  </div>\n</template>\n\n<script>\n\nimport { Banner } from '@rancher/components';\n\nimport ImagesOutputWindow from '@pkg/components/ImagesOutputWindow.vue';\nimport ImagesScanResults from '@pkg/components/ImagesScanResults.vue';\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\nimport getImageOutputCuller from '@pkg/utils/imageOutputCuller';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default {\n  name: 'images-scan-details',\n\n  components: {\n    ImagesScanResults,\n    ImagesOutputWindow,\n    Banner,\n    LoadingIndicator,\n  },\n\n  data() {\n    return {\n      image:                            this.$route.params.image,\n      namespace:                        this.$route.params.namespace,\n      showImageOutput:                  true,\n      imageManagerOutput:               '',\n      imageOutputCuller:                null,\n      keepImageManagerOutputWindowOpen: false,\n      currentCommand:                   null,\n      fieldToClear:                     '',\n      completionStatus:                 false,\n      jsonOutput:                       'null',\n      postCloseOutputWindowHandler:     null,\n    };\n  },\n\n  computed: {\n    isFinishedWithSuccess() {\n      return this.isFinished && this.completionStatus;\n    },\n    imageManagerProcessFinishedWithFailure() {\n      return this.isFinished && !this.completionStatus;\n    },\n    isFinished() {\n      return !this.currentCommand;\n    },\n    showImageManagerOutput() {\n      return this.keepImageManagerOutputWindowOpen;\n    },\n    showOutput() {\n      return !this.isFinished && !this.isFinishedWithSuccess;\n    },\n    vulnerabilities() {\n      const results = JSON.parse(this.jsonOutput)?.Results;\n\n      // TODO: rancher-sandbox/rancher-desktop#2007\n      return results\n        ?.reduce((prev, curr) => {\n          return [...prev, ...curr?.Vulnerabilities || []];\n        }, [])\n        ?.map(({ PkgName, VulnerabilityID, ...rest }) => {\n          return {\n            id: `${ PkgName }-${ VulnerabilityID }`,\n            PkgName,\n            VulnerabilityID,\n            ...rest,\n          };\n        });\n    },\n    loadingText() {\n      return this.t('images.scan.loadingText', { image: this.image }, true);\n    },\n    errorText() {\n      return this.t('images.scan.errorText', { image: this.image }, true);\n    },\n  },\n\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.t('images.scan.title', { image: this.$route.params.image }, true) },\n    );\n\n    ipcRenderer.on('ok:images-process-output', (_event, data) => {\n      this.jsonOutput = data;\n    });\n\n    this.currentCommand = `scan image ${ this.image }`;\n    this.scanImage();\n  },\n\n  methods: {\n    scanImage() {\n      this.startRunningCommand('trivy-image');\n      ipcRenderer.send('do-image-scan', this.image, this.namespace);\n    },\n    startRunningCommand(command) {\n      this.imageOutputCuller = getImageOutputCuller(command);\n    },\n    onProcessEnd(val) {\n      this.completionStatus = val;\n      this.currentCommand = null;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n  .image-output-container {\n    padding-bottom: 1rem;\n  }\n\n  textarea#imageManagerOutput {\n    font-family: monospace;\n    font-size: smaller;\n\n    .failure {\n      border: 2px solid var(--error);\n    }\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/snapshots/create.vue",
    "content": "<script lang=\"ts\">\n\nimport { Banner, LabeledInput, TextAreaAutoGrow } from '@rancher/components';\nimport dayjs from 'dayjs';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport { Snapshot, SnapshotEvent } from '@pkg/main/snapshots/types';\nimport { currentTime } from '@pkg/utils/dateUtils';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { escapeHtml } from '@pkg/utils/string';\n\nconst defaultName = () => {\n  const dateString = dayjs().format('YYYY-MM-DD_HH_mm_ss');\n\n  return `Snap_${ dateString }`;\n};\n\nexport default defineComponent({\n  name:       'snapshots-create',\n  components: {\n    Banner,\n    LabeledInput,\n    TextAreaAutoGrow,\n  },\n\n  data() {\n    return {\n      name:        defaultName(),\n      description: '',\n      creating:    false,\n    };\n  },\n\n  computed: {\n    ...mapGetters('snapshots', { snapshots: 'list' }),\n    valid(): boolean {\n      return !!this.name && !this.snapshots.find((s: Snapshot) => s.name === this.name);\n    },\n  },\n\n  mounted() {\n    this.$store.dispatch(\n      'page/setHeader',\n      { title: this.t('snapshots.create.title') },\n    );\n    (this.$refs.nameInput as any)?.select();\n  },\n\n  methods: {\n    goBack(event: SnapshotEvent | null) {\n      this.$router.push({\n        name:   'Snapshots',\n        params: { ...event },\n      });\n    },\n\n    async submit() {\n      ipcRenderer.send('preferences-close');\n      this.creating = true;\n      document.getSelection()?.removeAllRanges();\n\n      /** TODO limit description length */\n      const { name, description } = this;\n\n      let snapshotCancelled = false;\n\n      ipcRenderer.once('snapshot/cancel', () => {\n        snapshotCancelled = true;\n\n        this.goBack({\n          type:         'create',\n          result:       'cancel',\n          snapshotName: name,\n          eventTime:    currentTime(),\n        });\n      });\n\n      ipcRenderer.on('dialog/mounted', async() => {\n        const error = await this.$store.dispatch('snapshots/create', { name, description });\n\n        if (error) {\n          ipcRenderer.send('dialog/error', { dialog: 'SnapshotsDialog', error });\n        } else {\n          ipcRenderer.send('dialog/close', { dialog: 'SnapshotsDialog', snapshotEventType: 'create' });\n\n          this.goBack({\n            type:         'create',\n            result:       snapshotCancelled ? 'cancel' : 'success',\n            snapshotName: name,\n            eventTime:    currentTime(),\n          });\n        }\n      });\n\n      await this.showCreatingSnapshotDialog();\n\n      this.creating = false;\n      ipcRenderer.removeAllListeners('dialog/mounted');\n    },\n\n    async showCreatingSnapshotDialog() {\n      const name = this.name.length > 32 ? `${ this.name.substring(0, 30) }...` : this.name;\n\n      await ipcRenderer.invoke(\n        'show-snapshots-blocking-dialog',\n        {\n          window: {\n            buttons: [\n              this.t(`snapshots.dialog.creating.actions.cancel`),\n            ],\n            cancelId: 0,\n          },\n          format: {\n            header:            this.t('snapshots.dialog.creating.header', { snapshot: name }),\n            showProgressBar:   true,\n            message:           this.t('snapshots.dialog.creating.message', { snapshot: escapeHtml(name) }, true),\n            snapshotEventType: 'create',\n          },\n        },\n      );\n    },\n  },\n});\n</script>\n\n<template>\n  <div>\n    <Banner\n      class=\"banner mb-20\"\n      color=\"info\"\n    >\n      {{ t('snapshots.create.info') }}\n    </Banner>\n    <div class=\"snapshot-form\">\n      <div class=\"field name-field\">\n        <label>{{ t('snapshots.create.name.label') }}</label>\n        <LabeledInput\n          ref=\"nameInput\"\n          v-model:value=\"name\"\n          v-focus\n          data-test=\"createSnapshotNameInput\"\n          class=\"input\"\n          type=\"text\"\n          :disabled=\"creating\"\n        />\n      </div>\n      <div class=\"field description-field\">\n        <label>{{ t('snapshots.create.description.label') }}</label>\n        <TextAreaAutoGrow\n          ref=\"descriptionInput\"\n          v-model:value=\"description\"\n          data-test=\"createSnapshotDescInput\"\n          class=\"input\"\n          :disabled=\"creating\"\n        />\n      </div>\n      <div class=\"actions\">\n        <button\n          class=\"btn btn-xs role-primary create\"\n          :disabled=\"creating || !valid\"\n          @click=\"submit\"\n        >\n          <span>{{ t('snapshots.create.actions.submit') }}</span>\n        </button>\n        <button\n          class=\"btn btn-xs role-secondary back\"\n          :disabled=\"creating\"\n          @click=\"goBack(null)\"\n        >\n          <span>{{ t('snapshots.create.actions.back') }}</span>\n        </button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .snapshot-form {\n    max-width: 500px;\n    margin-top: 10px;\n\n    .field {\n      margin-bottom: 20px;\n    }\n\n    .input {\n      margin-top: 5px;\n    }\n\n    .description-field .input {\n      min-height: 200px;\n    }\n\n    .actions {\n      display: flex;\n      flex-direction: row-reverse;\n      gap: 15px;\n      flex-grow: 1;\n\n      .btn {\n        min-width: 150px;\n      }\n    }\n  }\n  .banner {\n    margin-top: 10px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/snapshots/dialog.vue",
    "content": "<script lang=\"ts\">\nimport os from 'os';\n\nimport { Banner } from '@rancher/components';\nimport { defineComponent } from 'vue';\n\nimport BackendProgress from '@pkg/components/BackendProgress.vue';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport default defineComponent({\n  name:       'snapshots-dialog',\n  components: { Banner, BackendProgress },\n  layout:     'dialog',\n  data() {\n    return {\n      header:            '',\n      message:           '',\n      snapshot:          null,\n      info:              '',\n      bodyStyle:         {},\n      error:             '',\n      errorTitle:        '',\n      errorDescription:  '',\n      errorButton:       '',\n      buttons:           [],\n      response:          0,\n      cancelId:          0,\n      snapshotEventType: '',\n      showProgressBar:   false,\n      credentials:       {\n        user:     '',\n        password: '',\n        port:     0,\n      },\n    };\n  },\n\n  async beforeMount() {\n    this.credentials = await this.$store.dispatch(\n      'credentials/fetchCredentials',\n    );\n  },\n\n  mounted() {\n    ipcRenderer.on('dialog/error', (_event, args) => {\n      this.error = args.error;\n      this.errorTitle = args.errorTitle;\n      this.errorDescription = args.errorDescription;\n      this.errorButton = args.errorButton;\n    });\n\n    ipcRenderer.on('dialog/info', (_event, args) => {\n      this.info = this.t(args.infoKey, {}, true);\n    });\n\n    ipcRenderer.on('dialog/options', (_event, { window, format }) => {\n      this.header = format.header;\n      this.message = format.message;\n      this.snapshot = format.snapshot;\n      this.info = format.info;\n      this.snapshotEventType = format.snapshotEventType;\n      this.showProgressBar = format.showProgressBar;\n      this.bodyStyle = this.calculateBodyStyle(format.type);\n      this.buttons = window.buttons || [];\n      this.cancelId = window.cancelId;\n\n      ipcRenderer.send('dialog/ready');\n    });\n\n    ipcRenderer.on('dialog/close', (_event, args) => {\n      if (args.snapshotEventType !== this.snapshotEventType) {\n        return;\n      }\n      ipcRenderer.send(\n        'dialog/close',\n        {\n          response:  this.response,\n          eventType: this.snapshotEventType,\n        });\n    });\n\n    ipcRenderer.send('dialog/mounted');\n  },\n\n  beforeUnmount() {\n    ipcRenderer.removeAllListeners('dialog/error');\n    ipcRenderer.removeAllListeners('dialog/options');\n    ipcRenderer.removeAllListeners('dialog/close');\n  },\n\n  methods: {\n    async close(index: number) {\n      if (this.error && this.snapshotEventType === 'restore') {\n        this.quit();\n\n        return;\n      }\n\n      if (!this.error && this.snapshotEventType !== 'confirm' && index === this.cancelId) {\n        await this.cancelSnapshot();\n      }\n\n      ipcRenderer.send('dialog/close', { response: index, eventType: this.snapshotEventType });\n    },\n    isDarwin() {\n      return os.platform().startsWith('darwin');\n    },\n    calculateBodyStyle(type: string) {\n      return { height: `${ type === 'question' ? 265 : 400 }px` };\n    },\n    showLogs() {\n      ipcRenderer.send('show-logs');\n    },\n    quit() {\n      fetch(\n        `http://localhost:${ this.credentials?.port }/v1/shutdown`,\n        {\n          method:  'PUT',\n          headers: new Headers({\n            Authorization: `Basic ${ window.btoa(\n              `${ this.credentials?.user }:${ this.credentials?.password }`,\n            ) }`,\n            'Content-Type': 'application/x-www-form-urlencoded',\n          }),\n        },\n      );\n    },\n    async cancelSnapshot() {\n      await fetch(\n        `http://localhost:${ this.credentials?.port }/v1/snapshots/cancel`,\n        {\n          method:  'POST',\n          headers: new Headers({\n            Authorization: `Basic ${ window.btoa(\n              `${ this.credentials?.user }:${ this.credentials?.password }`,\n            ) }`,\n            'Content-Type': 'application/x-www-form-urlencoded',\n          }),\n        },\n      );\n      ipcRenderer.send('snapshot/cancel');\n    },\n  },\n});\n</script>\n\n<template>\n  <div class=\"dialog-container\">\n    <div\n      :style=\"bodyStyle\"\n      class=\"dialog-body\"\n    >\n      <div\n        v-if=\"header\"\n        class=\"header\"\n      >\n        <slot name=\"header\">\n          <h1 v-if=\"errorTitle\">\n            {{ errorTitle }}\n          </h1>\n          <h1\n            v-else\n            v-clean-html=\"header\"\n          />\n        </slot>\n      </div>\n      <hr class=\"separator\">\n      <div\n        v-if=\"snapshot\"\n        class=\"snapshot\"\n      >\n        <slot name=\"snapshot\">\n          <div class=\"content\">\n            <div class=\"header\">\n              <h2>\n                {{ snapshot.name }}\n              </h2>\n              <div class=\"created\">\n                <span\n                  v-if=\"snapshot.formattedCreateDate\"\n                  v-clean-html=\"t('snapshots.card.created', { date: snapshot.formattedCreateDate.date, time: snapshot.formattedCreateDate.time }, true)\"\n                  class=\"value\"\n                />\n              </div>\n            </div>\n            <div\n              v-if=\"snapshot.description\"\n              class=\"description\"\n            >\n              <span class=\"value\">{{ snapshot.description }}</span>\n            </div>\n          </div>\n        </slot>\n      </div>\n      <div\n        v-if=\"errorDescription\"\n        class=\"message\"\n      >\n        <span\n          v-clean-html=\"errorDescription\"\n          class=\"value\"\n        />\n      </div>\n      <div\n        v-else-if=\"message\"\n        class=\"message\"\n      >\n        <slot name=\"message\">\n          <span\n            v-clean-html=\"message\"\n            class=\"value\"\n          />\n        </slot>\n      </div>\n      <div\n        v-if=\"info\"\n        class=\"info\"\n      >\n        <slot name=\"info\">\n          <Banner\n            class=\"banner mb-20 info-banner\"\n            color=\"info\"\n          >\n            <span v-clean-html=\"info\" />\n          </Banner>\n        </slot>\n      </div>\n      <div\n        v-if=\"error\"\n        class=\"error\"\n      >\n        <slot name=\"error\">\n          <Banner\n            class=\"banner mb-20\"\n            color=\"error\"\n          >\n            <span v-clean-html=\"error\" />\n            <a\n              href=\"#\"\n              @click.prevent=\"showLogs\"\n            >{{ t('snapshots.dialog.showLogs') }}</a>\n          </Banner>\n        </slot>\n      </div>\n    </div>\n    <backend-progress\n      v-if=\"showProgressBar\"\n      class=\"progress\"\n    />\n    <div\n      class=\"dialog-actions\"\n      :class=\"{ 'dialog-actions-reverse': isDarwin() }\"\n    >\n      <slot name=\"actions\">\n        <template v-if=\"error\">\n          <button\n            class=\"btn\"\n            :class=\"'role-secondary'\"\n            @click=\"close(cancelId)\"\n          >\n            <template v-if=\"errorButton\">\n              {{ errorButton }}\n            </template>\n            <template v-else>\n              {{ t('snapshots.dialog.buttons.error') }}\n            </template>\n          </button>\n        </template>\n        <template v-else>\n          <button\n            v-for=\"(buttonText, index) in buttons\"\n            :key=\"index\"\n            class=\"btn\"\n            :class=\"index ? 'role-primary' : 'role-secondary'\"\n            @click=\"close(index)\"\n          >\n            {{ buttonText }}\n          </button>\n        </template>\n      </slot>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n  .dialog-container {\n    display: block;\n    width: 45rem;\n    padding: 10px;\n\n    .dialog-body {\n      margin-top: 0.25rem;\n      display: flex;\n      flex-direction: column;\n      gap: 1.25rem;\n\n      .header {\n        H1 {\n          margin: 0;\n        }\n      }\n\n      .separator {\n        height: 0;\n        border: 0;\n        border-top: 1px solid var(--border);\n        width: 100%;\n      }\n\n      .snapshot {\n        .content {\n          display: flex;\n          flex-direction: column;\n          gap: 15px;\n          flex-grow: 1;\n          min-width: 300px;\n          .header {\n            h2 {\n              max-width: 500px;\n              white-space: nowrap;\n              overflow: hidden;\n              text-overflow: ellipsis;\n              margin: 0 0 5px 0;\n            }\n          }\n          .description {\n            max-width: 500px;\n            white-space: nowrap;\n            overflow: hidden;\n            text-overflow: ellipsis;\n          }\n        }\n\n        .value {\n          color: var(--input-label);\n        }\n      }\n\n      .message {\n        .value {\n          color: var(--input-label);\n        }\n\n        display: flex;\n        font-size: 1.3rem;\n        line-height: 2rem;\n      }\n\n      .info, .error {\n        margin: 0;\n        padding: 5px 0 0 0;\n        span {\n          max-width: 500px;\n          word-wrap: break-word;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          margin-right: 5px;\n        }\n        A {\n          text-decoration: underline;\n          color: unset;\n        }\n      }\n    }\n\n    .progress {\n      margin: 1.25rem 0;\n    }\n\n    .dialog-actions {\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-end;\n      gap: 0.25rem;\n    }\n\n    .dialog-actions-reverse {\n      justify-content: flex-start;\n      flex-direction: row-reverse;\n    }\n  }\n\n  .info-banner :deep(code) {\n    padding: 2px;\n  }\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/pages/volumes/files/_name.vue",
    "content": "<template>\n  <div class=\"volume-files\">\n    <div class=\"volume-info\">\n      <span class=\"volume-name\">{{ volumeName }}</span>\n      <badge-state\n        :color=\"volumeExists ? 'bg-success' : 'bg-darker'\"\n        :label=\"volumeExists ? 'Available' : 'Not Found'\"\n      />\n    </div>\n\n    <div class=\"path-breadcrumb\">\n      <span\n        class=\"breadcrumb-item\"\n        @click=\"navigateToPath('/')\"\n      >\n        <i class=\"icon icon-folder-open\" />root</span>\n      <template\n        v-for=\"(segment, index) in pathSegments\"\n        :key=\"`path-${index}`\"\n      >\n        <span class=\"breadcrumb-separator\">/</span>\n        <span\n          class=\"breadcrumb-item\"\n          :class=\"{ 'is-current': index === pathSegments.length - 1 }\"\n          @click=\"index < pathSegments.length - 1 ? navigateToPath(getPathUpTo(index)) : null\"\n        >\n          {{ segment }}\n        </span>\n      </template>\n    </div>\n\n    <loading-indicator\n      v-if=\"isLoading\"\n      class=\"content-state\"\n    >\n      {{ t('volumes.files.loading') }}\n    </loading-indicator>\n\n    <banner\n      v-else-if=\"error\"\n      class=\"content-state\"\n      color=\"error\"\n    >\n      <span class=\"icon icon-info-circle icon-lg\" />\n      {{ error }}\n    </banner>\n\n    <div\n      v-else\n      class=\"file-browser\"\n    >\n      <sortable-table\n        :headers=\"headers\"\n        :paging=\"false\"\n        :row-actions=\"false\"\n        :rows=\"files\"\n        :search=\"false\"\n        :table-actions=\"false\"\n        class=\"files-table\"\n        key-field=\"path\"\n        no-rows-key=\"volumes.files.noFiles\"\n      >\n        <template #col:name=\"{ row }\">\n          <td>\n            <span\n              :class=\"{ 'is-directory': row.isDirectory, 'is-clickable': row.isDirectory }\"\n              class=\"file-name\"\n              @click=\"row.isDirectory ? navigateToPath(row.path) : null\"\n            >\n              <i\n                :class=\"getFileIcon(row)\"\n                class=\"file-icon\"\n              />\n              {{ row.name }}\n            </span>\n          </td>\n        </template>\n        <template #col:size=\"{ row }\">\n          <td>{{ row.isDirectory ? '-' : formatSize(row.size) }}</td>\n        </template>\n        <template #col:modified=\"{ row }\">\n          <td>{{ formatDate(row.modified) }}</td>\n        </template>\n        <template #col:permissions=\"{ row }\">\n          <td class=\"permissions\">\n            {{ row.permissions }}\n          </td>\n        </template>\n      </sortable-table>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { ExecProcess } from '@docker/extension-api-client-types/dist/v1';\nimport { BadgeState, Banner } from '@rancher/components';\nimport { defineComponent } from 'vue';\nimport { mapGetters } from 'vuex';\n\nimport LoadingIndicator from '@pkg/components/LoadingIndicator.vue';\nimport SortableTable from '@pkg/components/SortableTable';\nimport { ContainerEngine } from '@pkg/config/settings';\nimport { mapTypedState } from '@pkg/entry/store';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport Latch from '@pkg/utils/latch';\n\ninterface fileEntry {\n  name:        string;\n  path:        string;\n  permissions: string;\n  owner:       string;\n  group:       string;\n  size:        number;\n  modified:    Date;\n  isDirectory: boolean;\n}\n\nexport default defineComponent({\n  name:       'VolumeFiles',\n  title:      'Volume Files',\n  components: {\n    BadgeState,\n    Banner,\n    LoadingIndicator,\n    SortableTable,\n  },\n  data() {\n    const queryPath = this.$route.query.path || this.$route.query.initialPath;\n\n    return {\n      ddClient:        null as typeof window.ddClient | null,\n      isLoading:       true,\n      error:           null as string | null,\n      volumeExists:    false,\n      currentPath:     Array.isArray(queryPath) ? queryPath.join('/') : queryPath || '/',\n      files:           [] as fileEntry[],\n      refreshInterval: null as ReturnType<typeof setInterval> | null,\n      process:         undefined as ExecProcess | undefined,\n      headers:         [\n        {\n          name:  'name',\n          label: this.t('volumes.files.table.header.name'),\n          sort:  ['name'],\n        },\n        {\n          name:  'size',\n          label: this.t('volumes.files.table.header.size'),\n          sort:  ['size', 'name'],\n          width: 100,\n        },\n        {\n          name:  'modified',\n          label: this.t('volumes.files.table.header.modified'),\n          sort:  ['modified', 'name'],\n          width: 180,\n        },\n        {\n          name:  'permissions',\n          label: this.t('volumes.files.table.header.permissions'),\n          sort:  ['permissions', 'name'],\n          width: 120,\n        },\n      ],\n    };\n  },\n  computed: {\n    ...mapGetters('k8sManager', { isK8sReady: 'isReady' }),\n    ...mapTypedState('preferences', { settings: 'initialPreferences' }),\n    volumeName(): string {\n      return Array.isArray(this.$route.params.name) ? this.$route.params.name.join('.') : this.$route.params.name || '';\n    },\n    isValidVolumeName(): boolean {\n      const name = this.volumeName;\n      return !!name && /^[a-zA-Z0-9._-]+$/.test(name);\n    },\n    selectedNamespace(): string | undefined {\n      if (this.settings.containerEngine.name === ContainerEngine.CONTAINERD) {\n        return this.settings.containers.namespace;\n      }\n      return undefined;\n    },\n    pathSegments() {\n      return this.currentPath\n        .split('/')\n        .filter(segment => segment !== '');\n    },\n  },\n  watch: {\n    '$route.query.path': {\n      async handler(newPath) {\n        const path = newPath || '/';\n        if (this.currentPath !== path) {\n          this.currentPath = path;\n          if (this.ddClient && this.volumeExists) {\n            this.isLoading = true;\n            try {\n              await this.listFiles();\n            } catch (error) {\n              console.error('Error in path watcher:', error);\n            }\n          }\n        }\n      },\n      immediate: false,\n    },\n  },\n  mounted() {\n    if (!this.isValidVolumeName) {\n      this.error = this.t('volumes.files.invalidVolumeName', { name: this.volumeName });\n      return;\n    }\n\n    this.$store.dispatch('page/setHeader', {\n      title:       this.t('volumes.files.title'),\n      description: this.volumeName,\n    });\n\n    ipcRenderer.on('settings-read', this.onSettingsRead);\n    ipcRenderer.send('settings-read');\n\n    this.refreshInterval = setInterval(() => {\n      if (!this.isLoading && this.volumeExists) {\n        this.listFiles();\n      }\n    }, 30000);\n  },\n  beforeUnmount() {\n    ipcRenderer.removeListener('settings-read', this.onSettingsRead);\n    if (this.refreshInterval) {\n      clearInterval(this.refreshInterval);\n    }\n    this.process?.close();\n  },\n  methods: {\n    async onSettingsRead() {\n      await this.initializeFileBrowser();\n    },\n    async initializeFileBrowser() {\n      if (window.ddClient && this.isK8sReady && this.settings) {\n        this.ddClient = window.ddClient;\n        await this.checkVolumeExists();\n        if (this.volumeExists) {\n          await this.listFiles();\n        }\n      }\n    },\n    async checkVolumeExists() {\n      try {\n        const volumes = await this.ddClient?.docker.rdListVolumes({ namespace: this.selectedNamespace });\n        this.volumeExists = volumes?.some(v => v.Name === this.volumeName) || false;\n\n        if (!this.volumeExists) {\n          this.error = this.t('volumes.files.volumeNotFound', { name: this.volumeName });\n        }\n      } catch (error) {\n        console.error('Error checking volume:', error);\n        this.volumeExists = false;\n        this.error = this.t('volumes.files.checkError');\n      }\n    },\n    async listFiles() {\n      try {\n        this.error = null;\n        if (!this.ddClient) {\n          throw new Error('Client not configured');\n        }\n\n        const containerPath = `/volume${ this.currentPath }`;\n        const lsCommand = [\n          'run', '--rm', '--quiet',\n          '-v', `${ this.volumeName }:/volume:ro`,\n          'busybox',\n          'ls', '-la', '--full-time', '--group-directories-first',\n          containerPath,\n        ];\n\n        let stdout = ''; let stderr = '';\n        const latch = Latch();\n        this.process = this.ddClient.docker.cli.exec(\n          lsCommand[0],\n          lsCommand.slice(1),\n          {\n            cwd:       '/',\n            namespace: this.selectedNamespace,\n            stream:    {\n              onOutput(data: { stdout?: string, stderr?: string }) {\n                stdout += data.stdout ?? '';\n                stderr += data.stderr ?? '';\n              },\n              onClose(exitCode: number) {\n                if (exitCode) {\n                  latch.reject(exitCode);\n                } else {\n                  latch.resolve();\n                }\n              },\n              onError(error: any) {\n                latch.reject(error);\n              },\n            },\n          },\n        );\n\n        try {\n          await latch;\n          if (stderr && !stderr.includes('level=warning')) {\n            throw new Error(stderr);\n          }\n\n          this.files = this.parseLsOutput(stdout);\n          this.isLoading = false;\n        } finally {\n          try {\n            this.process?.close();\n          } catch (ex) {\n            console.debug(`Failed to stop volume list process:`, ex);\n          }\n        }\n      } catch (error: any) {\n        const errorSources = [\n          error?.message,\n          error?.stderr,\n          error?.error,\n          typeof error === 'string' ? error : null,\n          'Failed to list files',\n        ];\n\n        console.error('Error listing files:', error);\n        this.error = this.t('volumes.files.listError', { error: errorSources.find(msg => msg) });\n        this.isLoading = false;\n      }\n    },\n    parseLsOutput(output: string): fileEntry[] {\n      const lines = output.trim().split('\\n').filter(line => line.trim());\n      const files = [];\n\n      for (const line of lines) {\n        // Skip the \"total\" line\n        if (line.startsWith('total ')) {\n          continue;\n        }\n        const match = /^(?<permissions>[drwxst-]+)\\s+(?<links>\\d+)\\s+(?<owner>\\S+)\\s+(?<group>\\S+)\\s+(?<size>\\d+)\\s+(?<date>\\d{4}-\\d{2}-\\d{2})\\s+(?<time>\\d{2}:\\d{2}:\\d{2})\\s+(?<timezone>[+-]\\d{4})\\s+(?<name>.+)$/.exec(line);\n        if (match?.groups) {\n          const { permissions, owner, group, size, date, time, name } = match.groups;\n\n          if (name === '.' || name === '..') {\n            continue;\n          }\n\n          const isDirectory = permissions.startsWith('d');\n          const path = this.currentPath === '/'\n            ? `/${ name }`\n            : `${ this.currentPath }/${ name }`;\n\n          const modified = new Date(`${ date }T${ time }`);\n\n          files.push({\n            name,\n            path,\n            permissions,\n            owner,\n            group,\n            size: parseInt(size, 10),\n            modified,\n            isDirectory,\n          });\n        }\n      }\n\n      return files;\n    },\n    navigateToPath(path: string) {\n      if (this.currentPath === path) {\n        return;\n      }\n\n      // Use router to create history entry for directory navigation\n      this.$router.push({\n        name:   'volumes-files-name',\n        params: { name: this.volumeName },\n        query:  { path },\n      }).catch(err => {\n        if (err.name !== 'NavigationDuplicated') {\n          console.error('Navigation error:', err);\n        }\n      });\n    },\n    getPathUpTo(index: number) {\n      const segments = this.pathSegments.slice(0, index + 1);\n      return '/' + segments.join('/');\n    },\n    getFileIcon(file: fileEntry) {\n      if (file.isDirectory) {\n        return 'icon icon-folder';\n      }\n\n      return 'icon icon-file';\n    },\n    formatSize(bytes: number) {\n      if (bytes === 0) return '0 B';\n      const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n      const i = Math.floor(Math.log(bytes) / Math.log(1024));\n      return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];\n    },\n    formatDate(date: Date) {\n      return date.toLocaleString();\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.volume-files {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  gap: 1rem;\n  padding: 1.25rem;\n  overflow: hidden;\n  min-height: 0;\n  height: 100%;\n}\n\n.volume-info {\n  display: flex;\n  align-items: center;\n  gap: 0.625rem;\n  padding-left: 0.625rem;\n\n  .volume-name {\n    font-family: monospace;\n    font-weight: bold;\n    color: var(--primary);\n  }\n}\n\n.path-breadcrumb {\n  display: flex;\n  align-items: center;\n  gap: 0.25rem;\n  padding: 0.5rem 0.625rem;\n  background: var(--nav-bg);\n  border: 1px solid var(--border);\n  border-radius: var(--border-radius);\n  font-family: monospace;\n  font-size: 0.875rem;\n  overflow-x: auto;\n\n  .breadcrumb-item {\n    color: var(--link);\n    cursor: pointer;\n    white-space: nowrap;\n\n    &:hover:not(.is-current) {\n      color: var(--link-hover);\n      text-decoration: underline;\n    }\n\n    &.is-current {\n      color: var(--body-text);\n      cursor: default;\n      font-weight: 500;\n    }\n\n    .icon {\n      margin-right: 0.25rem;\n    }\n  }\n\n  .breadcrumb-separator {\n    color: var(--muted);\n    user-select: none;\n  }\n}\n\n.content-state {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 2.5rem;\n  flex: 1;\n}\n\n.file-browser {\n  flex: 1;\n  overflow: auto;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n}\n\n.file-name {\n  display: flex;\n  align-items: center;\n  gap: 0.5rem;\n\n  &.is-directory {\n    color: var(--link);\n    font-weight: 500;\n  }\n\n  &.is-clickable {\n    cursor: pointer;\n\n    &:hover {\n      color: var(--link-hover);\n      text-decoration: underline;\n    }\n  }\n}\n\n.file-icon {\n  font-size: 1rem;\n  color: var(--muted);\n\n  .is-directory & {\n    color: var(--warning);\n  }\n}\n\n.permissions {\n  font-family: monospace;\n  font-size: 0.875rem;\n  color: var(--muted);\n}\n\n.files-table {\n  flex: 1;\n  overflow: auto;\n  min-height: 0;\n}\n\n.files-table::v-deep .sortable-table-header {\n  position: sticky;\n  top: 0;\n  background: var(--body-bg);\n  z-index: 1;\n}\n\n.files-table::v-deep tbody {\n  overflow-y: auto;\n}\n</style>\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/clean-html-directive.js",
    "content": "import DOMPurify from 'dompurify';\n\nconst ALLOWED_TAGS = [\n  'code',\n  'li',\n  'a',\n  'p',\n  'b',\n  'br',\n  'ul',\n  'pre',\n  'span',\n  'div',\n  'i',\n  'em',\n  'strong',\n];\n\nconst purifyHTML = value => DOMPurify.sanitize(value, { ALLOWED_TAGS });\n\nexport default {\n  name: 'clean-html-directive',\n  install(app) {\n    app.directive('clean-html', {\n      mounted(el, binding) {\n        el.innerHTML = purifyHTML(binding.value);\n      },\n      updated(el, binding) {\n        el.innerHTML = purifyHTML(binding.value);\n      },\n      unmounted(el) {\n        el.innerHTML = '';\n      },\n    });\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/clean-tooltip-directive.ts",
    "content": "import DOMPurify from 'dompurify';\nimport { vTooltip } from 'floating-vue';\nimport { App, DirectiveHook } from 'vue';\n\nconst ALLOWED_TAGS = [\n  'code',\n  'li',\n  'a',\n  'p',\n  'b',\n  'br',\n  'ul',\n  'pre',\n  'span',\n  'div',\n  'i',\n  'em',\n  'strong',\n];\n\nexport default ({\n  name: 'clean-tooltip-directive',\n  install(app: App, ..._options: any) {\n    const fn: DirectiveHook<HTMLElement, any, any> = (el, binding) => {\n      let { value } = binding;\n\n      if (typeof value === 'string') {\n        value = DOMPurify.sanitize(value, { ALLOWED_TAGS });\n      } else if (typeof value?.content === 'string') {\n        value.content = DOMPurify.sanitize(value.content, { ALLOWED_TAGS });\n      }\n\n      return vTooltip.beforeMount(el, { ...binding, value });\n    };\n\n    app.directive('clean-tooltip', {\n      ...vTooltip,\n      beforeMount: fn,\n      updated:     fn,\n    });\n  },\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/directives.js",
    "content": "export default {\n  name: 'directives',\n  install(app) {\n    app.directive('focus', {\n      mounted(_el, _binding, vnode) {\n        const { components, refs } = vnode.ctx;\n\n        if ('LabeledTooltip' in components) {\n          refs.value?.focus();\n        } else {\n          _el.focus();\n        }\n      },\n    });\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/i18n.js",
    "content": "import { watchEffect, ref, h } from 'vue';\nimport { useStore } from 'vuex';\n\nimport { escapeHtml } from '../utils/string';\n\nexport function stringFor(store, key, args, raw = false, escapehtml = true) {\n  const translation = store.getters['i18n/t'](key, args);\n\n  let out;\n\n  if ( translation !== undefined ) {\n    out = translation;\n  } else if ( args && Object.keys(args).length ) {\n    const argStr = Object.keys(args).map(k => `${ k }: ${ args[k] }`).join(', ');\n\n    out = `%${ key }(${ argStr })%`;\n    raw = true;\n  } else {\n    out = `%${ key }%`;\n  }\n\n  if ( raw ) {\n    return out;\n  } else if (escapehtml) {\n    return escapeHtml(out);\n  } else {\n    return out;\n  }\n}\n\nfunction directive(el, binding, vnode /*, oldVnode */) {\n  const { instance } = binding;\n  const raw = binding.modifiers && binding.modifiers.raw === true;\n  const str = stringFor(instance.$store, binding.value, {}, raw);\n\n  if ( binding.arg ) {\n    el.setAttribute(binding.arg, str);\n  } else {\n    el.innerHTML = str;\n  }\n}\n\nexport function directiveSsr(vnode, binding) {\n  console.warn('Function `directiveSsr` is deprecated. Please install i18n as a vue plugin: `vueApp.use(i18n)`');\n\n  const { context } = vnode;\n  const raw = binding.modifiers && binding.modifiers.raw === true;\n  const str = stringFor(context.$store, binding.value, {}, raw);\n\n  if ( binding.arg ) {\n    vnode.data.attrs[binding.arg] = str;\n  } else {\n    vnode.data.domProps = { innerHTML: str };\n  }\n}\n\nconst i18n = {\n  name:    'i18n',\n  install: (vueApp, _options) => {\n    if (vueApp.config.globalProperties.t && vueApp.directive('t') && vueApp.component('t')) {\n      console.debug('Skipping i18n install. Directive, component, and option already exist.');\n    }\n\n    vueApp.config.globalProperties.t = function(key, args, raw) {\n      return stringFor(this.$store, key, args, raw);\n    };\n\n    // InnerHTML: <some-tag v-t=\"'some.key'\" />\n    // As an attribute: <some-tag v-t:title=\"'some.key'\" />\n    vueApp.directive('t', {\n      beforeMount() {\n        directive(...arguments);\n      },\n      updated() {\n        directive(...arguments);\n      },\n    });\n\n    // Basic (but you might want the directive above): <t k=\"some.key\" />\n    // With interpolation: <t k=\"some.key\" count=\"1\" :foo=\"bar\" />\n    vueApp.component('t', {\n      inheritAttrs: false,\n      props:        {\n        k: {\n          type:     String,\n          required: true,\n        },\n        raw: {\n          type:    Boolean,\n          default: false,\n        },\n        tag: {\n          type:    [String, Object],\n          default: 'span',\n        },\n        escapehtml: {\n          type:    Boolean,\n          default: true,\n        },\n      },\n      setup(props, ctx) {\n        const msg = ref('');\n        const store = useStore();\n\n        // Update msg whenever k, $attrs, raw, or escapehtml changes\n        watchEffect(() => {\n          msg.value = stringFor(store, props.k, ctx.attrs, props.raw, props.escapehtml);\n        });\n\n        return { msg };\n      },\n      render() {\n        if (this.raw) {\n          return h(\n            this.tag,\n            { innerHTML: this.msg },\n          );\n        } else {\n          return h(\n            this.tag,\n            { },\n            [this.msg],\n          );\n        }\n      },\n    });\n  },\n};\n\nexport default i18n;\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/shortkey.js",
    "content": "// I grabbed the source code of this plugin from https://github.com/rodrigopv/vue3-shortkey\n//\n// At the time of writing this the released plugin was emitting debug console messages and\n// there was a PR open for about 1 year to address this https://github.com/rodrigopv/vue3-shortkey/pull/1\n// If another library becomes available I think we should use it instead\n\nconst ShortKey = {};\nconst mapFunctions = {};\nlet objAvoided = [];\nlet elementAvoided = [];\nlet containerAvoided = [];\nlet keyPressed = false;\n\nconst parseValue = (value) => {\n  value = typeof value === 'string' ? JSON.parse(value.replace(/\\'/gi, '\"')) : value;\n  if (value instanceof Array) {\n    return { '': value };\n  }\n\n  return value;\n};\n\nconst bindValue = (value, el, binding, vnode) => {\n  const push = binding.modifiers.push === true;\n  const avoid = binding.modifiers.avoid === true;\n  const focus = !binding.modifiers.focus === true;\n  const once = binding.modifiers.once === true;\n  const propagate = binding.modifiers.propagate === true;\n\n  if (avoid) {\n    objAvoided = objAvoided.filter((item) => {\n      return !item === el;\n    });\n    objAvoided.push(el);\n  } else {\n    mappingFunctions({\n      b: value, push, once, focus, propagate, el: vnode.el,\n    });\n  }\n};\n\nconst unbindValue = (value, el) => {\n  for (const key in value) {\n    const k = ShortKey.encodeKey(value[key]);\n    const idxElm = mapFunctions[k].el.indexOf(el);\n\n    if (mapFunctions[k].el.length > 1 && idxElm > -1) {\n      mapFunctions[k].el.splice(idxElm, 1);\n    } else {\n      delete mapFunctions[k];\n    }\n  }\n};\n\nShortKey.install = (VueApp, options) => {\n  elementAvoided = [...(options && options.prevent ? options.prevent : [])];\n  containerAvoided = [...(options && options.preventContainer ? options.preventContainer : [])];\n  VueApp.directive('shortkey', {\n    beforeMount: (el, binding, vnode) => {\n      // Mapping the commands\n      const value = parseValue(binding.value);\n\n      bindValue(value, el, binding, vnode);\n    },\n    updated: (el, binding, vnode) => {\n      const oldValue = parseValue(binding.oldValue);\n\n      unbindValue(oldValue, el);\n\n      const newValue = parseValue(binding.value);\n\n      bindValue(newValue, el, binding, vnode);\n    },\n    unmounted: (el, binding) => {\n      const value = parseValue(binding.value);\n\n      unbindValue(value, el);\n    },\n  });\n};\n\nShortKey.decodeKey = pKey => createShortcutIndex(pKey);\nShortKey.encodeKey = (pKey) => {\n  const shortKey = {};\n\n  shortKey.shiftKey = pKey.includes('shift');\n  shortKey.ctrlKey = pKey.includes('ctrl');\n  shortKey.metaKey = pKey.includes('meta');\n  shortKey.altKey = pKey.includes('alt');\n  let indexedKeys = createShortcutIndex(shortKey);\n  const vKey = pKey.filter(item => !['shift', 'ctrl', 'meta', 'alt'].includes(item));\n\n  indexedKeys += vKey.join('');\n\n  return indexedKeys;\n};\n\nconst createShortcutIndex = (pKey) => {\n  let k = '';\n\n  if (pKey.key === 'Shift' || pKey.shiftKey) {\n    k += 'shift';\n  }\n  if (pKey.key === 'Control' || pKey.ctrlKey) {\n    k += 'ctrl';\n  }\n  if (pKey.key === 'Meta' || pKey.metaKey) {\n    k += 'meta';\n  }\n  if (pKey.key === 'Alt' || pKey.altKey) {\n    k += 'alt';\n  }\n  if (pKey.key === 'ArrowUp') {\n    k += 'arrowup';\n  }\n  if (pKey.key === 'ArrowLeft') {\n    k += 'arrowleft';\n  }\n  if (pKey.key === 'ArrowRight') {\n    k += 'arrowright';\n  }\n  if (pKey.key === 'ArrowDown') {\n    k += 'arrowdown';\n  }\n  if (pKey.key === 'AltGraph') {\n    k += 'altgraph';\n  }\n  if (pKey.key === 'Escape') {\n    k += 'esc';\n  }\n  if (pKey.key === 'Enter') {\n    k += 'enter';\n  }\n  if (pKey.key === 'Tab') {\n    k += 'tab';\n  }\n  if (pKey.key === ' ') {\n    k += 'space';\n  }\n  if (pKey.key === 'PageUp') {\n    k += 'pageup';\n  }\n  if (pKey.key === 'PageDown') {\n    k += 'pagedown';\n  }\n  if (pKey.key === 'Home') {\n    k += 'home';\n  }\n  if (pKey.key === 'End') {\n    k += 'end';\n  }\n  if (pKey.key === 'Delete') {\n    k += 'del';\n  }\n  if (pKey.key === 'Backspace') {\n    k += 'backspace';\n  }\n  if (pKey.key === 'Insert') {\n    k += 'insert';\n  }\n  if (pKey.key === 'NumLock') {\n    k += 'numlock';\n  }\n  if (pKey.key === 'CapsLock') {\n    k += 'capslock';\n  }\n  if (pKey.key === 'Pause') {\n    k += 'pause';\n  }\n  if (pKey.key === 'ContextMenu') {\n    k += 'contextmenu';\n  }\n  if (pKey.key === 'ScrollLock') {\n    k += 'scrolllock';\n  }\n  if (pKey.key === 'BrowserHome') {\n    k += 'browserhome';\n  }\n  if (pKey.key === 'MediaSelect') {\n    k += 'mediaselect';\n  }\n  if ((pKey.key && pKey.key !== ' ' && pKey.key.length === 1) || /F\\d{1,2}|\\//g.test(pKey.key)) {\n    k += pKey.key.toLowerCase();\n  }\n\n  return k;\n};\n\nconst dispatchShortkeyEvent = (pKey) => {\n  const e = new CustomEvent('shortkey', { bubbles: false });\n\n  if (mapFunctions[pKey].key) {\n    e.srcKey = mapFunctions[pKey].key;\n  }\n  const elm = mapFunctions[pKey].el;\n\n  if (!mapFunctions[pKey].propagate) {\n    elm[elm.length - 1].dispatchEvent(e);\n  } else {\n    elm.forEach(elmItem => elmItem.dispatchEvent(e));\n  }\n};\n\nShortKey.keyDown = (pKey) => {\n  if ((!mapFunctions[pKey].once && !mapFunctions[pKey].push) || (mapFunctions[pKey].push && !keyPressed)) {\n    dispatchShortkeyEvent(pKey);\n  }\n};\n\nif (process && process.env && process.env.NODE_ENV !== 'test') {\n  (function() {\n    document.addEventListener('keydown', (pKey) => {\n      const decodedKey = ShortKey.decodeKey(pKey);\n\n      // Check avoidable elements\n      if (availableElement(decodedKey)) {\n        if (!mapFunctions[decodedKey].propagate) {\n          pKey.preventDefault();\n          pKey.stopPropagation();\n        }\n        if (mapFunctions[decodedKey].focus) {\n          ShortKey.keyDown(decodedKey);\n          keyPressed = true;\n        } else if (!keyPressed) {\n          const elm = mapFunctions[decodedKey].el;\n\n          elm[elm.length - 1].focus();\n          keyPressed = true;\n        }\n      }\n    }, true);\n\n    document.addEventListener('keyup', (pKey) => {\n      const decodedKey = ShortKey.decodeKey(pKey);\n\n      if (availableElement(decodedKey)) {\n        if (!mapFunctions[decodedKey].propagate) {\n          pKey.preventDefault();\n          pKey.stopPropagation();\n        }\n        if (mapFunctions[decodedKey].once || mapFunctions[decodedKey].push) {\n          dispatchShortkeyEvent(decodedKey);\n        }\n      }\n      keyPressed = false;\n    }, true);\n  })();\n}\n\nconst mappingFunctions = ({\n  b, push, once, focus, propagate, el,\n}) => {\n  for (const key in b) {\n    const k = ShortKey.encodeKey(b[key]);\n    const elm = mapFunctions[k] && mapFunctions[k].el ? mapFunctions[k].el : [];\n    const propagated = mapFunctions[k] && mapFunctions[k].propagate;\n\n    elm.push(el);\n    mapFunctions[k] = {\n      push,\n      once,\n      focus,\n      key,\n      propagate: propagated || propagate,\n      el:        elm,\n    };\n  }\n};\n\nconst availableElement = (decodedKey) => {\n  const objectIsAvoided = !!objAvoided.find(r => r === document.activeElement);\n  const filterAvoided = !!(elementAvoided.find(selector => document.activeElement && document.activeElement.matches(selector)));\n  const filterAvoidedContainer = !!(containerAvoided.find(selector => isActiveElementChildOf(selector)));\n\n  return !!mapFunctions[decodedKey] && !(objectIsAvoided || filterAvoided) && !filterAvoidedContainer;\n};\n\nconst isActiveElementChildOf = (container) => {\n  const activeElement = document.activeElement;\n\n  return activeElement && activeElement.closest(container) !== null;\n};\n\nexport default ShortKey;\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/tooltip.ts",
    "content": "import FloatingVue from 'floating-vue';\nimport { App } from 'vue';\n\nexport default ({\n  name: 'tooltip',\n  install(app: App, ..._options: any) {\n    app.use(FloatingVue);\n  },\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/trim-whitespace.js",
    "content": "function trimWhitespace(el, dir) {\n  for (const node of el.childNodes) {\n    if (node.nodeType === Node.TEXT_NODE ) {\n      const trimmed = node.data.trim();\n\n      if ( trimmed === '') {\n        node.remove();\n      } else if ( trimmed !== node.data ) {\n        node.data = trimmed;\n      }\n    }\n  }\n}\n\nexport default {\n  name: 'trim-whitespace-directive',\n  install(app) {\n    app.directive('trim-whitespace', {\n      mounted: trimWhitespace,\n      updated: trimWhitespace,\n    });\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/plugins/v-select.js",
    "content": "import vSelect from 'vue-select';\n\nexport default {\n  name: 'v-select',\n  install(app) {\n    app.component('v-select', vSelect);\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/preload/README.md",
    "content": "# Preload Scripts\n\nThis directory contains code exposed to the renderer as [preload scripts].\n\n[preload scripts]: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload\n\n## Extensions\n\nSee https://docs.docker.com/desktop/extensions-sdk/ for details.\n\ne.g. splatform/epinio-docker-desktop\n"
  },
  {
    "path": "pkg/rancher-desktop/preload/extensions.ts",
    "content": "/**\n * This is the preload script that is exposed to extension frontends.\n * It implements the \"ddClient\" API.\n */\n\nimport Electron from 'electron';\n\nimport type { SpawnOptions } from '@pkg/main/extensions/types';\nimport clone from '@pkg/utils/clone';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nimport type { v1 } from '@docker/extension-api-client-types';\n\n// We use a bunch of symbols for names of properties we do not want to reflect\n// over.\nconst stream = Symbol('stream');\nconst stdout = Symbol('stdout');\nconst stderr = Symbol('stderr');\nconst id = Symbol('id');\n\n/**\n * DockerListContainersOptions describes the arguments for\n * ddClient.docker.listContainers()\n */\ninterface DockerListContainersOptions {\n  all?:       boolean;\n  limit?:     number;\n  size?:      boolean;\n  filters?:   string;\n  namespace?: string;\n}\n\n/**\n * DockerListImagesOptions describes the arguments for\n * ddClient.docker.listImages()\n */\ninterface DockerListImagesOptions {\n  all?:       boolean;\n  filters?:   string;\n  digests?:   boolean;\n  namespace?: string;\n}\n\n/**\n * DockerListVolumesOptions describes the arguments for\n * ddClient.docker.rdListVolumes()\n */\ninterface DockerListVolumesOptions {\n  filters?:   string;\n  namespace?: string;\n}\n\n/**\n * DockerEventCallback is the callback function for Docker events\n */\ntype DockerEventCallback = (event: DockerEvent) => void;\n\n/**\n * DockerEvent represents a Docker event\n */\ninterface DockerEvent {\n  Type:   string;    // container, volume, network, etc.\n  Action: string;  // create, start, stop, destroy, etc.\n  Actor: {\n    ID:         string;\n    Attributes: Record<string, string>;\n  };\n  time:     number;\n  timeNano: number;\n  status?:  string;\n  id?:      string;\n  from?:    string;\n}\n\n/**\n * DockerEventSubscriptionOptions for subscribing to Docker events\n */\ninterface DockerEventSubscriptionOptions {\n  filters?: {\n    type?:      string[];\n    event?:     string[];\n    container?: string[];\n    label?:     string[];\n  };\n  namespace?: string;\n}\n\n/**\n * DockerEventSubscription represents an active event subscription\n */\ninterface DockerEventSubscription {\n  unsubscribe(): void;\n}\n\n/** execProcess holds the state associated with a v1.ExecProcess. */\ninterface execProcess {\n  /** The identifier for this process. */\n  [id]:     string;\n  [stdout]: string;\n  [stderr]: string;\n  [stream]: v1.ExecStreamOptions;\n}\n\ninterface RDXExecOptions extends v1.ExecOptions {\n  namespace?: string;\n}\n\ninterface RDXSpawnOptions extends v1.SpawnOptions {\n  namespace?: string;\n}\n\n/**\n * The identifier for the extension (the name of the image).\n */\nconst extensionId = location.protocol === 'app:' ? '<app>' : decodeURIComponent(location.hostname.replace(/(..)/g, '%$1'));\n\n/**\n * Context passed from the main process via additionalArguments.\n */\nconst extensionContext: { arch?: string, hostname?: string, extensionVersion?: string } = (() => {\n  try {\n    return JSON.parse(process.argv.slice(-1).pop() ?? '{}');\n  } catch {\n    return {};\n  }\n})();\n\n/**\n * The processes that are waiting to complete, keyed by the process ID.\n * For compatibility reasons, we need a strong reference here.\n */\nconst outstandingProcesses: Record<string, execProcess> = {};\n\n/**\n * pageLoadId is a random string that differs on each page load, to ensure that\n * we don't end up reusing processes from previous loads.\n */\nconst pageLoadId = Array.from(window.crypto.getRandomValues(new Uint8Array(16))).map(v => `00${ v.toString(16) }`.slice(-2)).join('');\n\n/**\n * Store for active event subscriptions\n */\nconst eventSubscriptions = new Map<string, v1.ExecProcess>();\n\n/**\n * Construct a TypeError message that is similar to what the browser would\n * have constructed.\n * @param name The name of the argument.\n * @param expectedType: The name of the type that was expected.\n * @param object The actual object that was passed in (of the incorrect type).\n */\nfunction getTypeErrorMessage(name: string, expectedType: string, object: any) {\n  let message = `[ERROR_INVALID_ARG_TYPE]: The \"${ name }\" argument must be of type ${ expectedType }.`;\n\n  if (typeof object === 'object' && 'constructor' in object && 'name' in object.constructor) {\n    message += ` Received an instance of ${ object.constructor.name }`;\n  } else {\n    message += ` Received ${ typeof object }`;\n  }\n\n  return message;\n}\n\n/**\n * Given an options object passed to exec(), check if it's a v1.SpawnOptions.\n */\nfunction isSpawnOptions(options: RDXExecOptions | RDXSpawnOptions): options is RDXSpawnOptions {\n  return 'stream' in options;\n}\n\n/**\n * Return an exec function for the given scope.\n * @param scope The scope to run the execution in.\n */\nfunction getExec(scope: SpawnOptions['scope']): v1.Exec {\n  let nextId = 0;\n\n  function exec(cmd: string, args: string[], options?: RDXExecOptions): Promise<v1.ExecResult>;\n  function exec(cmd: string, args: string[], options: RDXSpawnOptions): v1.ExecProcess;\n  function exec(cmd: string, args: string[], options?: RDXExecOptions | RDXSpawnOptions): Promise<v1.ExecResult> | v1.ExecProcess {\n    // Do some minimal parameter validation, since passing these to the backend\n    // directly can end up with confusing messages otherwise.\n    if (typeof cmd !== 'string') {\n      throw new TypeError(getTypeErrorMessage('cmd', 'string', cmd));\n    }\n    if (!Array.isArray(args)) {\n      throw new TypeError(getTypeErrorMessage('args', 'array', args));\n    }\n    for (const [i, arg] of Object.entries(args)) {\n      if (typeof arg !== 'string') {\n        throw new TypeError(getTypeErrorMessage(`args[${ i }]`, 'string', arg));\n      }\n    }\n    if (!['undefined', 'string'].includes(typeof options?.cwd)) {\n      throw new TypeError(getTypeErrorMessage('options.cwd', 'string', options?.cwd));\n    }\n    if (typeof options?.env !== 'undefined') {\n      if (typeof options.env !== 'object') {\n        throw new TypeError(getTypeErrorMessage('options.env', 'object', options.env));\n      }\n      for (const [k, v] of Object.entries(options.env)) {\n        if (!['undefined', 'string'].includes(typeof v)) {\n          throw new TypeError(getTypeErrorMessage(`options.env.${ k }`, 'string', v));\n        }\n      }\n    }\n    if ('namespace' in (options ?? {})) {\n      if (!['string', 'undefined'].includes(typeof options?.namespace)) {\n        throw new TypeError(getTypeErrorMessage('options.namespace', 'string', options?.namespace));\n      }\n    }\n\n    const execId = `${ pageLoadId }-${ scope }-${ nextId++ }`;\n    // Build options to pass to the main process, while not trusting the input\n    // too much.\n\n    if (options?.namespace) {\n      args.unshift(`--namespace=${ options.namespace }`);\n    }\n\n    const safeOptions: SpawnOptions = {\n      command: [`${ cmd }`].concat(Array.from(args).map((arg) => {\n        return `${ arg }`.replace(/^([\"'])(.*)\\1$/, '$2');\n      })),\n      execId,\n      scope,\n      ...options?.cwd ? { cwd: options.cwd } : {},\n      ...options?.env ? { env: options.env } : {},\n    };\n\n    if (options && isSpawnOptions(options)) {\n      // Build the object to return to the caller.  We manually define\n      // properties with symbol keys so they can't be enumerated (to avoid\n      // people accidentally clobbering our stuff).\n      const proc = Object.defineProperties({}, {\n        [id]: {\n          enumerable: false, value: execId, writable: false,\n        },\n        [stdout]: {\n          enumerable: false, value: '', writable: true,\n        },\n        [stderr]: {\n          enumerable: false, value: '', writable: true,\n        },\n        [stream]: {\n          enumerable: false, value: options.stream, writable: false,\n        },\n        close: {\n          enumerable: true,\n          value() {\n            ipcRenderer.send('extensions/spawn/kill', execId);\n            delete outstandingProcesses[execId];\n          },\n        },\n      }) as execProcess & v1.ExecProcess;\n\n      outstandingProcesses[execId] = proc;\n      ipcRenderer.send('extensions/spawn/streaming', safeOptions);\n\n      return proc;\n    }\n\n    return (async() => {\n      const response = await ipcRenderer.invoke('extensions/spawn/blocking', safeOptions);\n\n      console.debug(`spawn/blocking got result:`, (process.env.RD_TEST ?? '').includes('e2e') ? JSON.stringify(response) : response);\n\n      const result = {\n        cmd:    response.cmd,\n        signal: typeof response.result === 'string' ? response.result : undefined,\n        code:   typeof response.result === 'number' ? response.result : undefined,\n        stdout: response.stdout,\n        stderr: response.stderr,\n        lines() {\n          return response.stdout.split(/\\r?\\n/);\n        },\n        parseJsonLines() {\n          return response.stdout.split(/\\r?\\n/).filter(line => line).map(line => JSON.parse(line));\n        },\n        parseJsonObject() {\n          return JSON.parse(response.stdout);\n        },\n      };\n\n      if (result.signal || result.code) {\n        throw result;\n      }\n\n      return result;\n    })();\n  }\n\n  return exec;\n}\n\nfunction getProcess(id: string, reason: string): execProcess | undefined {\n  const process = outstandingProcesses[id];\n\n  if (process) {\n    return process;\n  }\n\n  // The process handle has gone away on our side, just try to kill it.\n  ipcRenderer.send('extensions/spawn/kill', id);\n  delete outstandingProcesses[id];\n  console.debug(`Process ${ id } not found (${ reason }), discarding.`);\n}\n\nipcRenderer.on('extensions/spawn/output', (event, id, data) => {\n  const process = getProcess(id, 'extensions/spawn/output');\n  const streamOpts = process?.[stream];\n\n  if (!process || !streamOpts?.onOutput) {\n    // Process died, or there's no output handler.\n    return;\n  }\n\n  if (!streamOpts.splitOutputLines) {\n    try {\n      streamOpts.onOutput(data);\n    } catch (ex) {\n      console.error(ex);\n    }\n\n    return;\n  }\n\n  for (const key of ['stdout', 'stderr'] as const) {\n    const input = (data as Record<string, string>)[key];\n    const keySym = { stdout, stderr }[key] as typeof stdout | typeof stderr;\n\n    if (input) {\n      process[keySym] += input;\n      while (true) {\n        const [_match, line, rest] = /^(.*?)\\r?\\n(.*)$/s.exec(process[keySym]) ?? [];\n\n        if (typeof line === 'undefined') {\n          return;\n        }\n        try {\n          process[stream].onOutput?.({ [key]: line } as { stdout: string } | { stderr: string });\n        } catch (ex) {\n          console.error(ex);\n        }\n        process[keySym] = rest;\n      }\n    }\n  }\n});\n\nipcRenderer.on('extensions/spawn/error', (_, id, error) => {\n  console.debug(`RDX: Extension ${ id } errored:`, error);\n  try {\n    getProcess(id, 'extensions/spawn/error')?.[stream].onError?.(error);\n  } catch (ex) {\n    console.error(ex);\n  }\n  delete outstandingProcesses[id];\n});\n\nipcRenderer.on('extensions/spawn/close', (_, id, returnValue) => {\n  console.debug(`RDX: Extension ${ id } closed:`, returnValue);\n  try {\n    getProcess(id, 'extensions/spawn/close')?.[stream]?.onClose?.(typeof returnValue === 'number' ? returnValue : -1);\n  } catch (ex) {\n    console.error(ex);\n  }\n  delete outstandingProcesses[id];\n});\n\n// During the nuxt removal, import/namespace started failing\n\nexport class RDXClient implements v1.DockerDesktopClient {\n  constructor(info?: { arch: string, hostname: string }) {\n    if (info) {\n      Object.assign(this.host, info);\n    }\n  }\n\n  /**\n   * makeRequest is a helper for ddClient.extension.vm.service.<HTTP method>\n   * that wraps ddClient.extension.vm.service.request().\n   */\n  protected makeRequest(method: string, url: string, data?: any): Promise<unknown> {\n    const headers: Record<string, string> = {};\n\n    if (typeof data === 'object') {\n      // For objects, pass the value as JSON.\n      headers['Content-Type'] = 'application/json';\n      data = JSON.stringify(data);\n    }\n\n    return this.request({\n      method, url, data, headers,\n    });\n  }\n\n  protected async request(config: v1.RequestConfig): Promise<unknown> {\n    try {\n      const result = await ipcRenderer.invoke('extensions/vm/http-fetch', config);\n\n      if (!result) {\n        return;\n      }\n\n      // Parse as JSON if possible (API is unclear).\n      let { statusCode, message } = result;\n\n      try {\n        if (message) {\n          message = JSON.parse(message);\n        }\n      } catch {\n        // Body is not JSON, return it as-is.\n      }\n\n      if (statusCode >= 200 && statusCode < 300) {\n        return message;\n      }\n\n      return Promise.reject(result);\n    } catch (ex) {\n      console.debug(`${ config.method } ${ config.url } error:`, ex);\n      throw ex;\n    }\n  }\n\n  extension: v1.Extension & { id?: string, version?: string } = {\n    vm: {\n      cli:     { exec: getExec('container') },\n      service: {\n        request: (config: v1.RequestConfig) => this.request(config),\n        get:     (url: string) => this.makeRequest('GET', url),\n        post:    (url: string, data: any) => this.makeRequest('POST', url, data),\n        put:     (url: string, data: any) => this.makeRequest('PUT', url, data),\n        patch:   (url: string, data: any) => this.makeRequest('PATCH', url, data),\n        delete:  (url: string) => this.makeRequest('DELETE', url),\n        head:    (url: string) => this.makeRequest('HEAD', url),\n      },\n    },\n    host:    { cli: { exec: getExec('host') } },\n    image:   extensionContext.extensionVersion ? `${ extensionId }:${ extensionContext.extensionVersion }` : extensionId,\n    id:      extensionId,\n    version: extensionContext.extensionVersion ?? '',\n  };\n\n  desktopUI = {\n    dialog: {\n      showOpenDialog(options: any): Promise<v1.OpenDialogResult> {\n        // Use the clone() here to ensure we only pass plain data structures to\n        // the main process.\n        return ipcRenderer.invoke('extensions/ui/show-open', clone(options ?? {}));\n      },\n    },\n    navigate: {} as v1.NavigationIntents,\n    toast:    {\n      success(msg: string) {\n        ipcRenderer.send('extensions/ui/toast', 'success', `${ msg }`);\n      },\n      warning(msg: string) {\n        ipcRenderer.send('extensions/ui/toast', 'warning', `${ msg }`);\n      },\n      error(msg: string) {\n        ipcRenderer.send('extensions/ui/toast', 'error', `${ msg }`);\n      },\n    },\n  };\n\n  host: v1.Host = {\n    openExternal: (url: string) => {\n      ipcRenderer.send('extensions/open-external', url);\n    },\n    platform: process.platform,\n    arch:     extensionContext.arch ?? '<unknown>',\n    hostname: extensionContext.hostname ?? '<unknown>',\n  };\n\n  docker = {\n    cli:            { exec: getExec('docker-cli') },\n    listNamespaces: async() => {\n      const results = await this.docker.cli.exec('namespace', ['list', '--quiet']);\n\n      if (results.code || results.signal) {\n        throw new Error(`failed to inspect namespaces: ${ results.stderr }`);\n      }\n\n      return results.lines().map(n => n.trim()).filter(n => n);\n    },\n    listContainers: async(options: DockerListContainersOptions = {}) => {\n      // Unfortunately, there's no command line option to just make an API call,\n      // and `container ls` by itself doesn't provide all the info.\n      const lsArgs = ['ls', '--format={{json .}}', '--no-trunc'];\n\n      lsArgs.push(`--all=${ options.all ?? false }`);\n      if ((options.limit ?? -1) > -1) {\n        lsArgs.push(`--last=${ options.limit }`);\n      }\n      if (options.filters !== undefined) {\n        lsArgs.push(`--filter=${ options.filters }`);\n      }\n      if (options.namespace) {\n        lsArgs.unshift(`--namespace=${ options.namespace }`);\n      }\n\n      const lsResult = await this.docker.cli.exec('container', lsArgs);\n\n      if (lsResult.code || lsResult.signal) {\n        throw new Error(`failed to list containers: ${ lsResult.stderr }`);\n      }\n\n      const lsContainers = lsResult.parseJsonLines();\n\n      if (lsContainers.length === 0) {\n        return [];\n      }\n\n      // We need to run `container inspect` to add more info.\n      const inspectArgs = [\n        '--format={{json .}}',\n        options.size ? ['--size=true'] : [],\n        lsContainers.map(c => c.ID),\n        options.namespace ? [`--namespace=${ options.namespace }`] : [],\n      ].flat();\n\n      const inspectResults = await this.docker.cli.exec('inspect', inspectArgs);\n\n      if (inspectResults.code || inspectResults.signal) {\n        throw new Error(`failed to inspect containers: ${ inspectResults.stderr }`);\n      }\n\n      const inspectContainers = inspectResults.parseJsonLines().flat();\n\n      return lsContainers.map((c) => {\n        const details = inspectContainers.find(i => i.Id.startsWith(c.ID));\n\n        function pick<K extends string | readonly [string, string]>(object: any, ...prop: K[])\n          : { [key in K as key extends string ? key : key[1]]: any } {\n          const result: Record<string, any> = {};\n\n          for (const p of prop) {\n            const [key, newKey] = (Array.isArray(p) ? p : [p, p]) as [string, string];\n\n            if (key in (object ?? {})) {\n              result[newKey] = object[key];\n            }\n          }\n\n          return result as any;\n        }\n\n        return {\n          ...pick(c, 'Image', 'Command', 'Status'),\n          ...pick(details, 'Id', 'NetworkSettings', 'Mounts'),\n          ...pick(details, ['Image', 'ImageID'] as const),\n          HostConfig: details.HostConfig ?? {},\n          SizeRootFs: details.SizeRootFs ?? -1,\n          SizeRw:     details.SizeRw ?? -1,\n          Ports:      details.NetworkSettings?.Ports ?? {},\n          ...pick(details.Config, 'Labels'),\n          ...pick(details.State, ['Status', 'State'] as const),\n          Names:      typeof c.Names === 'string' ? c.Names.split(/\\s+/g) : Array.from(c.Names),\n          Created:    Date.parse(c.CreatedAt).valueOf(),\n          Started:    Date.parse(details.State.StartedAt).valueOf(),\n        };\n      });\n    },\n    listImages: async(options: DockerListImagesOptions = {}) => {\n      const lsArgs = ['ls', '--format={{json .}}', '--no-trunc'];\n\n      lsArgs.push(`--all=${ options.all ?? false }`);\n      if (options.filters !== undefined) {\n        lsArgs.push(`--filter=${ options.filters }`);\n      }\n      lsArgs.push(`--digests=${ options.digests ?? false }`);\n      if (options.namespace) {\n        lsArgs.unshift(`--namespace=${ options.namespace }`);\n      }\n\n      const lsResult = await this.docker.cli.exec('image', lsArgs);\n\n      if (lsResult.code || lsResult.signal) {\n        throw new Error(`failed to list images: ${ lsResult.stderr }`);\n      }\n\n      const lsImages = lsResult.parseJsonLines();\n\n      if (lsImages.length === 0) {\n        return [];\n      }\n\n      const inspectArgs = [\n        options.namespace ? [`--namespace=${ options.namespace }`] : [],\n        ['--format', 'json'],\n        lsImages.map(i => i.ID),\n      ].flat();\n      const inspectResults = await this.docker.cli.exec('inspect', inspectArgs);\n\n      if (inspectResults.code || inspectResults.signal) {\n        throw new Error(`failed to inspect images: ${ inspectResults.stderr }`);\n      }\n\n      // When doing JSON format, docker CLI returns an array, but nerdctl\n      // returns JSON lines.  ParseJsonLines + flat() deals with the difference.\n      const inspectImages = inspectResults.parseJsonLines().flat();\n      const mergedImages = lsImages.map((image) => {\n        let inspected = inspectImages.find(i => i.Id === image.ID);\n\n        // nerdctl uses the config digest for inspectImages[*].Id (or at least\n        // a different value than image.ID); we need to try to match it up to\n        // the desired inspect result via digests instead.\n        inspected ||= inspectImages.find(i => (i.RepoDigests as any[]).some(d => d.endsWith(image.Digest)));\n\n        return { ...image, ...inspected ?? {} };\n      });\n\n      return mergedImages.map((i) => {\n        const containers = parseInt(i.Containers, 10);\n\n        return {\n          Id:          i.Id,\n          ParentId:    i.Parent ?? '',\n          RepoTags:    i.RepoTags,\n          Created:     Date.parse(i.Created).valueOf(),\n          Size:        i.Size,\n          SharedSize:  -1,\n          VirtualSize: i.VirtualSize ?? i.Size,\n          Labels:      i.Config?.Labels ?? {},\n          Containers:  isNaN(containers) ? -1 : containers,\n        };\n      });\n    },\n    rdListVolumes: async(options: DockerListVolumesOptions = {}) => {\n      const lsArgs = ['ls', '--format={{json .}}'];\n\n      if (options.filters !== undefined) {\n        lsArgs.push(`--filter=${ options.filters }`);\n      }\n      if (options.namespace) {\n        lsArgs.unshift(`--namespace=${ options.namespace }`);\n      }\n\n      const lsResult = await this.docker.cli.exec('volume', lsArgs);\n\n      if (lsResult.code || lsResult.signal) {\n        throw new Error(`failed to list volumes: ${ lsResult.stderr }`);\n      }\n\n      const lsVolumes = lsResult.parseJsonLines();\n\n      return lsVolumes.map((v) => {\n        return {\n          Name:       v.Name,\n          Driver:     v.Driver,\n          Mountpoint: v.Mountpoint,\n          Labels:     v.Labels ?? {},\n          Scope:      v.Scope,\n          Options:    v.Options ?? {},\n          UsageData:  v.UsageData ?? {},\n          CreatedAt:  v.CreatedAt,\n          Created:    v.CreatedAt ? Date.parse(v.CreatedAt).valueOf() : 0,\n        };\n      });\n    },\n    rdSubscribeToEvents: (callback: DockerEventCallback, options: DockerEventSubscriptionOptions = {}): DockerEventSubscription => {\n      const eventArgs = ['events', '--format', '{{json .}}'];\n\n      options.filters?.type?.forEach(type => {\n        eventArgs.push('--filter', `type=${ type }`);\n      });\n      options.filters?.event?.forEach(event => {\n        eventArgs.push('--filter', `event=${ event }`);\n      });\n      options.filters?.container?.forEach(container => {\n        eventArgs.push('--filter', `container=${ container }`);\n      });\n      options.filters?.label?.forEach(label => {\n        eventArgs.push('--filter', `label=${ label }`);\n      });\n\n      const subscriptionId = `events-${ Date.now() }-${ Math.random().toString(36).substring(2, 11) }`;\n\n      const eventProcess = this.docker.cli.exec('system', eventArgs, {\n        stream: {\n          onOutput: ({ stdout, stderr }) => {\n            if (stdout) {\n              try {\n                const event = JSON.parse(stdout) as DockerEvent;\n                callback(event);\n              } catch (error) {\n                console.error('Failed to parse Docker event:', error, stdout);\n              }\n            }\n            if (stderr) {\n              console.error('Docker events stderr:', stderr);\n            }\n          },\n          onError: (error: any) => {\n            console.error('Docker events stream error:', error);\n          },\n          onClose: (code: number) => {\n            console.debug(`Docker events stream closed with code ${ code }`);\n          },\n          splitOutputLines: true,\n        },\n      });\n\n      eventSubscriptions.set(subscriptionId, eventProcess);\n\n      return {\n        unsubscribe: () => {\n          eventSubscriptions.get(subscriptionId)?.close();\n          eventSubscriptions.delete(subscriptionId);\n        },\n      };\n    },\n  };\n}\n\nexport default function initExtensions(): void {\n  switch (document.location.protocol) {\n  case 'x-rd-extension:': {\n    Electron.contextBridge.exposeInMainWorld('ddClient', new RDXClient());\n    break;\n  }\n  case 'app:': {\n    import('os').then(({ arch, hostname }) => {\n      Object.defineProperty(window, 'ddClient', {\n        value:        new RDXClient({ arch: arch(), hostname: hostname() }),\n        configurable: true,\n        enumerable:   true,\n        writable:     true,\n      });\n    });\n    break;\n  }\n  default: {\n    console.debug(`Not adding extension API to ${ document.location.protocol }`);\n\n    return;\n  }\n  }\n\n  window.addEventListener('unload', () => {\n    function canClose(proc: execProcess): proc is execProcess & v1.ExecProcess {\n      return 'close' in proc;\n    }\n\n    for (const [id, proc] of Object.entries(outstandingProcesses)) {\n      if (canClose(proc)) {\n        try {\n          proc.close();\n        } catch (ex) {\n          console.debug(`failed to close process ${ id }:`, ex);\n        }\n      }\n    }\n\n    // Clean up event subscriptions\n    for (const [id, proc] of eventSubscriptions) {\n      try {\n        proc.close();\n      } catch (ex) {\n        console.debug(`failed to kill event subscription ${ id }:`, ex);\n      }\n    }\n    eventSubscriptions.clear();\n  });\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/preload/index.ts",
    "content": "import initExtensions from './extensions';\n\nfunction init() {\n  initExtensions();\n}\n\ntry {\n  init();\n} catch (ex) {\n  console.error(ex);\n  throw ex;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/product.js",
    "content": "export const NAME = 'rancher-desktop';\n\nexport function init(plugin, store) {\n  const { product } = plugin.DSL(store, NAME);\n\n  product({\n    inStore:             'management',\n    icon:                'globe',\n    label:               'Rancher Desktop',\n    removable:           false,\n    showClusterSwitcher: false,\n    category:            'global',\n    to:                  { name: 'rancher-desktop-general' },\n  });\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "pkg/rancher-desktop/store/action-menu.js",
    "content": "import { filterBy, isArray } from '@pkg/utils/array';\n\nexport const state = function() {\n  return {\n    show:              false,\n    tableSelected:     [],\n    tableAll:          [],\n    resources:         [],\n    elem:              null,\n    event:             null,\n    showPromptMove:    false,\n    showPromptRemove:  false,\n    showPromptRestore: false,\n    showAssignTo:      false,\n    showPromptUpdate:  false,\n    showModal:         false,\n    toMove:            [],\n    toRemove:          [],\n    toRestore:         [],\n    toAssign:          [],\n    toUpdate:          [],\n    modalData:         {},\n  };\n};\n\nexport const getters = {\n  showing:       state => state.show,\n  elem:          state => state.elem,\n  event:         state => state.event,\n  tableSelected: state => state.tableSelected || [],\n\n  options(state) {\n    let selected = state.resources;\n\n    if ( !selected ) {\n      return [];\n    }\n\n    if ( !isArray(selected) ) {\n      selected = [];\n    }\n\n    const map = {};\n\n    for ( const node of selected ) {\n      if (node.availableActions) {\n        for ( const act of node.availableActions ) {\n          _add(map, act);\n        }\n      }\n    }\n\n    const out = _filter(map);\n\n    return { ...out };\n  },\n\n  isSelected: state => (resource) => {\n    return state.tableSelected.includes(resource);\n  },\n};\n\nexport const mutations = {\n  show(state, { resources, elem, event }) {\n    if ( !isArray(resources) ) {\n      resources = [resources];\n    }\n\n    state.resources = resources;\n    state.elem = elem;\n    state.event = event;\n    state.show = true;\n  },\n\n  hide(state) {\n    state.show = false;\n    state.resources = null;\n    state.elem = null;\n  },\n\n  togglePromptRemove(state, resources) {\n    if (!resources) {\n      state.showPromptRemove = false;\n      resources = [];\n    } else {\n      state.showPromptRemove = !state.showPromptRemove;\n      if (!isArray(resources)) {\n        resources = [resources];\n      }\n    }\n    state.toRemove = resources;\n  },\n\n  togglePromptMove(state, resources) {\n    if (!resources) {\n      state.showPromptMove = false;\n      resources = [];\n    } else {\n      state.showPromptMove = !state.showPromptMove;\n      state.toMove = Array.isArray(resources) ? resources : [resources];\n    }\n  },\n\n  togglePromptRestore(state, resources) {\n    if (!resources) {\n      state.showPromptRestore = false;\n      resources = [];\n    } else {\n      state.showPromptRestore = !state.showPromptRestore;\n      if (!isArray(resources)) {\n        resources = [resources];\n      }\n    }\n    state.toRestore = resources;\n  },\n\n  toggleAssignTo(state, resources) {\n    state.showAssignTo = !state.showAssignTo;\n\n    if (!isArray(resources)) {\n      resources = [resources];\n    }\n\n    state.toAssign = resources;\n  },\n\n  togglePromptUpdate(state, resources) {\n    if (!resources) {\n      // Clearing the resources also hides the prompt\n      state.showPromptUpdate = false;\n    } else {\n      state.showPromptUpdate = !state.showPromptUpdate;\n    }\n\n    if (!isArray(resources)) {\n      resources = [resources];\n    }\n\n    state.toUpdate = resources;\n  },\n\n  togglePromptModal(state, data) {\n    if (!data) {\n      // Clearing the resources also hides the prompt\n      state.showModal = false;\n    } else {\n      state.showModal = true;\n    }\n\n    state.modalData = data;\n  },\n};\n\nexport const actions = {\n  executeTable({ state }, { action, args }) {\n    return _execute(state.tableSelected, action, args);\n  },\n\n  execute({ state }, { action, args, opts }) {\n    return _execute(state.resources, action, args, opts);\n  },\n};\n\n// -----------------------------\n\nlet anon = 0;\n\nfunction _add(map, act, incrementCounts = true) {\n  let id = act.action;\n\n  if ( !id ) {\n    id = `anon${ anon }`;\n    anon++;\n  }\n\n  let obj = map[id];\n\n  if ( !obj ) {\n    obj = Object.assign({}, act);\n    map[id] = obj;\n    obj.allEnabled = false;\n  }\n\n  if ( act.enabled === false ) {\n    obj.allEnabled = false;\n  } else {\n    obj.anyEnabled = true;\n  }\n\n  if ( incrementCounts ) {\n    obj.available = (obj.available || 0) + (act.enabled === false ? 0 : 1 );\n    obj.total = (obj.total || 0) + 1;\n  }\n\n  return obj;\n}\n\nfunction _filter(map, disableAll = false) {\n  const out = filterBy(Object.values(map), 'anyEnabled', true);\n\n  for ( const act of out ) {\n    if ( disableAll ) {\n      act.enabled = false;\n    } else {\n      act.enabled = ( act.available >= act.total );\n    }\n  }\n\n  return out;\n}\n\nfunction _execute(resources, action, args, opts = {}) {\n  args = args || [];\n  if ( resources.length > 1 && action.bulkAction && !opts.alt ) {\n    const fn = resources[0][action.bulkAction];\n\n    if ( fn ) {\n      return fn.call(resources[0], resources, ...args);\n    }\n  }\n\n  const promises = [];\n\n  for ( const resource of resources ) {\n    let fn;\n\n    if (opts.alt && action.altAction) {\n      fn = resource[action.altAction];\n    } else {\n      fn = resource[action.action];\n    }\n\n    if ( fn ) {\n      promises.push(fn.apply(resource, args));\n    }\n  }\n\n  return Promise.all(promises);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/store/applicationSettings.ts",
    "content": "import _ from 'lodash';\nimport { GetterTree, MutationTree } from 'vuex';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport { defaultSettings } from '@pkg/config/settings';\nimport type { PathManagementStrategy } from '@pkg/integrations/pathManager';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\n/**\n * State is the type of the state we are maintaining in this store.\n */\ninterface State {\n  pathManagementStrategy: PathManagementStrategy;\n}\n\nconst cfg = _.cloneDeep(defaultSettings);\n\nexport const state: () => State = () => {\n  // While we load the settings from disk here, we only otherwise interact with\n  // the settings only via ipcRenderer.\n  return { pathManagementStrategy: cfg.application.pathManagementStrategy };\n};\n\nexport const mutations = {\n  SET_PATH_MANAGEMENT_STRATEGY(state: State, strategy: PathManagementStrategy) {\n    state.pathManagementStrategy = strategy;\n  },\n} satisfies Partial<MutationsType<State>> & MutationTree<State>;\n\nexport const actions = {\n  setPathManagementStrategy({ commit }, strategy: PathManagementStrategy) {\n    commit('SET_PATH_MANAGEMENT_STRATEGY', strategy);\n  },\n  async commitPathManagementStrategy({ commit }, strategy: PathManagementStrategy) {\n    commit('SET_PATH_MANAGEMENT_STRATEGY', strategy);\n    cfg.application.pathManagementStrategy = strategy;\n    await ipcRenderer.invoke('settings-write', { application: { pathManagementStrategy: strategy } });\n  },\n} satisfies ActionTree<State, any, typeof mutations, typeof getters>;\n\nexport const getters = {\n  pathManagementStrategy({ pathManagementStrategy }: State) {\n    return pathManagementStrategy;\n  },\n} satisfies GetterTree<State, any>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/container-engine.ts",
    "content": "import merge from 'lodash/merge';\nimport { MutationTree, Dispatch, GetterTree } from 'vuex';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport { ContainerEngine } from '@pkg/config/settings';\nimport type { RDXClient } from '@pkg/preload/extensions';\n\ntype ValidContainerEngine = Exclude<ContainerEngine, ContainerEngine.NONE>;\ntype SubscriberType = 'containers' | 'volumes';\ntype ErrorSource = 'containers' | 'volumes' | 'namespaces';\n\n/**\n * Shared extension API container list result parts\n */\ninterface ApiContainer {\n  Id:      string;\n  Command: string;\n  Created: number;\n  Started: number;\n  Image:   string;\n  ImageID: string;\n  Status:  string;\n  Mounts:       {\n    Type:        string;\n    Name?:       string;\n    Source:      string;\n    Destination: string;\n    Mode:        string;\n    RW:          boolean;\n    Propagation: string;\n  }[];\n  SizeRootFs: number;\n  SizeRw:     number;\n  Ports:      Record</* port/proto */string, { HostIp: string, HostPort: string }[] | null>;\n  Labels:     Record<string, string>;\n  State:      string;\n  Names:      string[];\n}\n\n/**\n * The container API response from moby.\n */\ninterface MobyContainer extends ApiContainer {\n  ImageID: string; // sha256:...\n  Mounts:  (ApiContainer['Mounts'][number] & { Driver: string })[];\n  State:   'created' | 'running' | 'paused' | 'restarting' | 'removing' | 'exited' | 'dead';\n}\n\n/**\n * The container API response from nerdctl.\n * @see github.com/containerd/nerdctl/v2/pkg/cmd/container#ListItem\n */\ninterface NerdctlContainer extends ApiContainer {\n  ImageID: string; // same as Image, registry/image:tags\n  Status:  'Created' | 'Paused' | 'Pausing' | 'Unknown' | 'Up' | string;\n  State:   'created' | 'running' | 'paused' | 'pausing' | 'unknown' | 'exited' | 'restarting' | '';\n}\n\n/**\n * Each container is an object to be described in the UI.\n */\nexport interface Container {\n  id:            string;\n  containerName: string;\n  imageName:     string;\n  state:         MobyContainer['State'] | NerdctlContainer['State'];\n  started:       Date | undefined;\n  projectGroup:  string;\n  labels:        Record<string, string>;\n  ports:         Record<string, { HostIp: string, HostPort: string }[] | null>;\n}\n\nexport interface Volume {\n  Name:       string;\n  Driver:     string;\n  Mountpoint: string;\n  Labels:     Record<string, string>;\n  Scope:      'local' | 'global';\n  Options:    Record<string, any>;\n  UsageData: {\n    Size?:     number;\n    RefCount?: number;\n  };\n  CreatedAt: string;\n  Created:   number;\n}\n\nclass Subscriber {\n  subscription?: { unsubscribe(): void };\n  destroy() {\n    this.subscription?.unsubscribe();\n  }\n}\n\ntype SubscriberConstructor =\n  new (client: RDXClient, dispatch: Dispatch, namespace?: string) => Subscriber;\n\nclass MobyContainerSubscriber extends Subscriber {\n  constructor(client: RDXClient, dispatch: Dispatch) {\n    super();\n    this.subscription = client.docker.rdSubscribeToEvents(\n      () => dispatch('fetchContainers'),\n      {\n        filters: {\n          type:  ['container'],\n          event: ['create', 'start', 'stop', 'die', 'kill', 'pause', 'unpause', 'rename', 'update', 'destroy', 'remove'],\n        },\n      });\n  }\n}\n\nclass MobyVolumeSubscriber extends Subscriber {\n  constructor(client: RDXClient, dispatch: Dispatch) {\n    super();\n    this.subscription = client.docker.rdSubscribeToEvents(\n      () => dispatch('fetchVolumes'),\n      {\n        filters: {\n          type:  ['volume'],\n          event: ['create', 'destroy', 'mount', 'unmount'],\n        },\n      });\n  }\n}\n\nclass NerdctlContainerSubscriber extends Subscriber {\n  constructor(client: RDXClient, dispatch: Dispatch, namespace?: string) {\n    super();\n    // Nerdctl does not support the filtering we need\n    this.subscription = client.docker.rdSubscribeToEvents(\n      (event) => {\n        const topic: string = (event as any).Topic;\n        if (topic.startsWith('/containers/')) {\n          dispatch('fetchContainers');\n        } else if (topic.startsWith('/namespaces/')) {\n          dispatch('fetchNamespaces');\n        }\n      },\n      { namespace },\n    );\n  }\n}\n\nclass NerdctlVolumeSubscriber extends Subscriber {\n  interval: ReturnType<typeof setInterval>;\n  constructor(client: RDXClient, dispatch: Dispatch, namespace?: string) {\n    super();\n    // Nerdctl does not support volume events; set up polling instead.\n    this.interval = setInterval(() => dispatch('fetchVolumes'), 2_000);\n    // But we still want a filter for namespaces\n    this.subscription = client.docker.rdSubscribeToEvents(\n      (event) => {\n        const topic: string = (event as any).Topic;\n        if (topic.startsWith('/namespaces/')) {\n          dispatch('fetchNamespaces');\n        }\n      },\n      // Use an invalid namespace to filter out most events; underscore is not a\n      // valid character in this context.\n      { namespace: '_invalid_' },\n    );\n  }\n\n  override destroy() {\n    clearInterval(this.interval);\n    super.destroy();\n  }\n}\n\nfunction subscriberConstructor(backend: ValidContainerEngine, type: SubscriberType): SubscriberConstructor {\n  return {\n    'moby:containers':       MobyContainerSubscriber,\n    'moby:volumes':          MobyVolumeSubscriber,\n    'containerd:containers': NerdctlContainerSubscriber,\n    'containerd:volumes':    NerdctlVolumeSubscriber,\n  }[`${ backend }:${ type }` as const];\n}\n\nexport interface ContainersState {\n  /** The backend in use; this may not match the committed preferences. */\n  backend:    ContainerEngine;\n  /** The type of object to monitor. */\n  type:       SubscriberType;\n  client:     RDXClient | null;\n  namespaces: string[] | null;\n  namespace:  string | undefined;\n  subscriber: Subscriber | null;\n  containers: Record<string, Container> | null;\n  volumes:    Record<string, Volume> | null;\n  /** The last error encountered, plus which fetch caused it. */\n  error:      { source: ErrorSource, error: Error } | null;\n}\n\nexport const state: () => ContainersState = () => ({\n  backend:    ContainerEngine.NONE,\n  type:       'containers',\n  client:     null,\n  namespaces: null,\n  namespace:  undefined,\n  subscriber: null,\n  containers: null,\n  volumes:    null,\n  error:      null,\n});\n\ntype BulkParams = Pick<ContainersState, 'backend' | 'type' | 'client' | 'namespace'>;\n\nexport const mutations = {\n  SET_SUBSCRIBER(state, subscriber) {\n    state.subscriber?.destroy();\n    state.subscriber = subscriber;\n  },\n  SET_NAMESPACES(state, namespaces) {\n    state.namespaces = Array.isArray(namespaces) ? namespaces.sort() : namespaces;\n  },\n  SET_CONTAINERS(state, containers) {\n    state.containers = containers;\n  },\n  SET_VOLUMES(state, volumes) {\n    state.volumes = volumes;\n  },\n  SET_ERROR(state, error) {\n    state.error = error;\n  },\n  SET_PARAMS(state, params: BulkParams) {\n    let clearData = false;\n    switch (true) {\n    case params.backend !== state.backend:\n    case params.type !== state.type:\n    case params.client === null:\n      clearData = true;\n    }\n    if (clearData) {\n      state.namespaces = null;\n    }\n    if (clearData || params.namespace !== state.namespace) {\n      state.subscriber?.destroy();\n    }\n    Object.assign(state, params);\n    if (clearData || params.namespace !== state.namespace) {\n      state.subscriber = null;\n      state.containers = null;\n      state.volumes = null;\n    }\n  },\n} satisfies Partial<MutationsType<ContainersState>> & MutationTree<ContainersState>;\n\ntype SubscribeParams = Omit<BulkParams, 'backend' | 'namespace'> & Partial<Pick<BulkParams, 'namespace'>>;\n\nexport const actions = {\n  async subscribe({ commit, state, dispatch, getters }, params: SubscribeParams) {\n    state.subscriber?.destroy();\n    commit('SET_PARAMS', { ...params, backend: getters.backend, namespace: params.namespace ?? getters.namespace });\n    if (state.backend === ContainerEngine.NONE) {\n      return;\n    }\n    const constructor = subscriberConstructor(state.backend, state.type);\n    if (state.client) {\n      commit('SET_SUBSCRIBER', new constructor(state.client, dispatch, state.namespace));\n      const type = state.type.replace(/^(.)/, c => c.toUpperCase()) as Capitalize<SubscriberType>;\n      const tasks = [dispatch(`fetch${ type }`)];\n\n      if (getters.supportsNamespaces) {\n        tasks.push(dispatch('fetchNamespaces'));\n      }\n      await Promise.all(tasks);\n    } else {\n      commit('SET_SUBSCRIBER', null);\n    }\n  },\n  unsubscribe({ commit, state }) {\n    state.subscriber?.destroy();\n    state.subscriber = null;\n    // Clear the state; this is needed if the backend becomes unready.\n    state.containers = null;\n    state.volumes = null;\n  },\n  async fetchNamespaces({ commit, state, getters }) {\n    try {\n      const { client } = state;\n\n      if (!getters.supportsNamespaces) {\n        commit('SET_NAMESPACES', null);\n        return;\n      }\n\n      commit('SET_NAMESPACES', await client?.docker.listNamespaces() ?? null);\n      if (state.error?.source === 'namespaces') {\n        commit('SET_ERROR', null);\n      }\n    } catch (error: any) {\n      commit('SET_ERROR', { source: 'namespaces', error });\n    }\n  },\n  async fetchContainers({ commit, getters, state }) {\n    try {\n      const { client, namespace } = state;\n      const containers = state.containers ?? {};\n      const options = { all: true, namespace: getters.supportsNamespaces ? namespace : undefined };\n      const apiContainers = await client?.docker.listContainers(options) ?? [];\n      const ids = new Set<string>();\n\n      // Update containers in-place to maintain any UI state\n      for (const container of apiContainers as (NerdctlContainer | MobyContainer)[]) {\n        const k8sPodName = container.Labels?.['io.kubernetes.pod.name'];\n        const k8sNamespace = container.Labels?.['io.kubernetes.pod.namespace'];\n        const composeProject = container.Labels?.['com.docker.compose.project'];\n        let state = container.State;\n        let projectGroup = 'Standalone Containers';\n\n        if (k8sPodName && k8sNamespace) {\n          projectGroup = `${ k8sNamespace }/${ k8sPodName }`;\n        } else if (composeProject) {\n          projectGroup = composeProject;\n        }\n\n        if (!state) {\n          // For containerd, stopped containers may have no state; try status.\n          state = container.Status.split(/\\s+/)[0].toLowerCase() as any || 'exited';\n        }\n\n        const info: Container = {\n          id:            container.Id,\n          containerName: container.Names[0].replace(/_[a-z0-9-]{36}_[0-9]+/, ''),\n          imageName:     container.Image,\n          state,\n          started:       undefined,\n          labels:        container.Labels ?? {},\n          ports:         container.Ports,\n          projectGroup,\n        };\n\n        if (container.State === 'running' && container.Started) {\n          info.started = new Date(container.Started);\n        }\n        containers[container.Id] = merge(containers[container.Id] ?? {}, info);\n        ids.add(container.Id);\n      }\n      // Remove containers that no longer exist\n      for (const id of Object.keys(containers)) {\n        if (!ids.has(id)) {\n          delete containers[id];\n        }\n      }\n      commit('SET_CONTAINERS', containers);\n      if (state.error?.source === 'containers') {\n        commit('SET_ERROR', null);\n      }\n    } catch (error: any) {\n      commit('SET_ERROR', { source: 'containers', error });\n    }\n  },\n  async fetchVolumes({ commit, getters, state }) {\n    try {\n      const { client, namespace } = state;\n      const volumes = state.volumes ?? {};\n      const names = new Set<string>();\n      const options = { namespace: getters.supportsNamespaces ? namespace : undefined };\n\n      // Update volumes in-place to maintain any UI state.\n      for (const volume of await client?.docker.rdListVolumes(options) ?? []) {\n        volumes[volume.Name] = Object.assign(volumes[volume.Name] ?? {}, volume);\n        names.add(volume.Name);\n      }\n      // Remove volumes that no longer exist\n      for (const name of Object.keys(volumes)) {\n        if (!names.has(name)) {\n          delete volumes[name];\n        }\n      }\n      commit('SET_VOLUMES', volumes);\n      if (state.error?.source === 'volumes') {\n        commit('SET_ERROR', null);\n      }\n    } catch (error: any) {\n      commit('SET_ERROR', { source: 'volumes', error });\n    }\n  },\n} satisfies ActionTree<ContainersState, any, typeof mutations, typeof getters>;\n\nexport const getters = {\n  backend(_state, _getters, rootState): ContainerEngine {\n    return rootState.preferences.initialPreferences?.containerEngine?.name ?? ContainerEngine.NONE;\n  },\n  supportsNamespaces(state) {\n    return state.backend === ContainerEngine.CONTAINERD;\n  },\n  namespace(_state, _getters, rootState): string | undefined {\n    return rootState.preferences.initialPreferences?.containers?.namespace;\n  },\n  error(state) {\n    return state.error;\n  },\n} satisfies GetterTree<ContainersState, any>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/credentials.ts",
    "content": "import { MutationTree } from 'vuex/types';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport Latch from '@pkg/utils/latch';\n\nexport type Credentials = Omit<ServerState, 'pid'>;\n\ninterface CredentialsState {\n  credentials: Credentials;\n}\n\nconst hasCredentials = Latch();\n\nexport async function fetchAPI(api: string, rootState: any, init?: RequestInit) {\n  // Any fetches will block until we have credentials.\n  await hasCredentials;\n\n  const { port, user, password } = rootState.credentials.credentials as Credentials;\n  const url = new URL(api, `http://localhost:${ port }/`);\n  const headers = new Headers(init?.headers);\n\n  headers.set('Authorization', `Basic ${ window.btoa(`${ user }:${ password }`) }`);\n  if (!headers.has('Content-Type')) {\n    headers.set('Content-Type', 'application/x-www-form-urlencoded');\n  }\n\n  init ??= {};\n  init.headers = headers;\n\n  return fetch(url.toString(), init);\n}\n\nexport const state: () => CredentialsState = () => (\n  {\n    credentials: {\n      password: '',\n      port:     0,\n      user:     '',\n    },\n  }\n);\n\nexport const mutations = {\n  SET_CREDENTIALS(state, credentials) {\n    state.credentials = credentials;\n    hasCredentials.resolve();\n  },\n} satisfies Partial<MutationsType<CredentialsState>> & MutationTree<CredentialsState>;\n\nexport const actions = {\n  async fetchCredentials({ commit }): Promise<Credentials> {\n    const result = await ipcRenderer.invoke('api-get-credentials');\n\n    commit('SET_CREDENTIALS', result);\n\n    return result;\n  },\n} satisfies ActionTree<CredentialsState, any, typeof mutations>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/diagnostics.ts",
    "content": "import DOMPurify from 'dompurify';\nimport _ from 'lodash';\nimport { marked } from 'marked';\nimport { Plugin } from 'vuex';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport { CURRENT_SETTINGS_VERSION } from '@pkg/config/settings';\nimport type { DiagnosticsResult, DiagnosticsResultCollection } from '@pkg/main/diagnostics/diagnostics';\nimport ipcRenderer from '@pkg/utils/ipcRenderer';\n\ninterface DiagnosticsState {\n  diagnostics: DiagnosticsResult[],\n  timeLastRun: Date;\n  inError:     boolean;\n}\n\nconst uri = (port: number, pathRemainder: string) => `http://localhost:${ port }/v1/${ pathRemainder }`;\n\n/**\n * Updates the muted property for diagnostic results.\n * @param checks A collection of diagnostic results that require muting.\n * @param mutedChecks A collection of key, value pairs that contains a key of\n * the ID for the diagnostic and a boolean value for muting the result.\n * @returns A collection of diagnostic results with an updated muted property.\n */\nfunction mapMutedDiagnostics(checks: DiagnosticsResult[], mutedChecks: Record<string, boolean>) {\n  return checks.map(check => ({ ...check, mute: !!mutedChecks[check.id] }));\n};\n\n/**\n * Maps over an array of diagnostic results, applying a markdown transformation\n * to the 'description' property of each object.\n * @param diagnostics The array of diagnostic results to map over.\n * @returns A promise that resolves to the array of diagnostic results with the\n * 'description' property transformed to markdown.\n */\nasync function mapMarkdownToDiagnostics(diagnostics: DiagnosticsResult[]) {\n  return await Promise.all(\n    diagnostics.map(async(x) => {\n      return {\n        ...x,\n        description: await markdown(x.description),\n      };\n    }),\n  );\n};\n\n/**\n * Processes a raw markdown string by first parsing it with `marked.parseInline`\n * and then sanitizing the result using `DOMPurify`.\n * @param raw The raw markdown string to be processed.\n * @returns A promise that resolves to a sanitized HTML string generated\n * from the provided markdown.\n */\nasync function markdown(raw: string) {\n  const markedString = await marked.parseInline(raw);\n\n  return DOMPurify.sanitize(markedString, { USE_PROFILES: { html: true } });\n};\n\nexport const state: () => DiagnosticsState = () => (\n  {\n    diagnostics: [],\n    timeLastRun: new Date(),\n    inError:     false,\n  }\n);\n\nexport const mutations = {\n  SET_DIAGNOSTICS(state: DiagnosticsState, diagnostics: DiagnosticsResult[]) {\n    state.diagnostics = diagnostics.filter(result => !result.passed);\n    state.inError = false;\n  },\n  SET_TIME_LAST_RUN(state: DiagnosticsState, currentDate: Date) {\n    state.timeLastRun = currentDate;\n  },\n  SET_IN_ERROR(state: DiagnosticsState, status: boolean) {\n    state.inError = status;\n  },\n} satisfies MutationsType<DiagnosticsState>;\n\nexport const actions = {\n  async fetchDiagnostics({ commit, rootState }) {\n    try {\n      const { port, user, password } = rootState.credentials.credentials;\n      const response = await fetch(\n        uri(port, 'diagnostic_checks'),\n        {\n          headers: new Headers({\n            Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n            'Content-Type': 'application/x-www-form-urlencoded',\n          }),\n        });\n\n      if (!response.ok) {\n        console.log(`fetchDiagnostics: failed: status: ${ response.status }:${ response.statusText }`);\n        commit('SET_IN_ERROR', true);\n\n        return;\n      }\n      const result: DiagnosticsResultCollection = await response.json();\n\n      const mutedChecks = rootState.preferences.preferences.diagnostics.mutedChecks;\n      const checks = mapMutedDiagnostics(result.checks, mutedChecks);\n\n      commit('SET_DIAGNOSTICS', await mapMarkdownToDiagnostics(checks));\n      commit('SET_TIME_LAST_RUN', new Date(result.last_update));\n      commit('SET_IN_ERROR', false);\n    } catch (ex) {\n      console.error(`fetchDiagnostics failed:`, ex);\n      commit('SET_IN_ERROR', true);\n    }\n  },\n  async runDiagnostics({ commit, rootState }) {\n    const { port, user, password } = rootState.credentials.credentials;\n    const response = await fetch(\n      uri(port, 'diagnostic_checks'),\n      {\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n        method: 'POST',\n      });\n\n    if (!response.ok) {\n      console.log(`runDiagnostics: failed: status: ${ response.status }:${ response.statusText }`);\n      commit('SET_IN_ERROR', true);\n\n      return;\n    }\n    const result: DiagnosticsResultCollection = await response.json();\n\n    const mutedChecks = rootState.preferences.preferences.diagnostics.mutedChecks;\n    const checks = mapMutedDiagnostics(result.checks, mutedChecks);\n\n    commit('SET_DIAGNOSTICS', await mapMarkdownToDiagnostics(checks));\n    commit('SET_TIME_LAST_RUN', new Date(result.last_update));\n  },\n  async updateDiagnostic({\n    commit, state, dispatch,\n  }, { isMuted, row }: { isMuted: boolean, row: DiagnosticsResult }) {\n    const diagnostics = _.cloneDeep(state.diagnostics);\n    const rowToUpdate = diagnostics.find(x => x.id === row.id);\n\n    if (rowToUpdate === undefined) {\n      return;\n    }\n\n    rowToUpdate.mute = isMuted;\n\n    await dispatch(\n      'preferences/commitPreferences',\n      {\n        payload: {\n          version:     CURRENT_SETTINGS_VERSION,\n          diagnostics: { mutedChecks: { [rowToUpdate.id]: isMuted } },\n        },\n      },\n      { root: true },\n    );\n\n    commit('SET_DIAGNOSTICS', await mapMarkdownToDiagnostics(diagnostics));\n  },\n} satisfies ActionTree<DiagnosticsState, any, typeof mutations>;\n\nexport const plugins: Plugin<DiagnosticsState>[] = [\n  // Vuex plugin used to refresh diagnostics on command from the backend.\n  function(store) {\n    ipcRenderer.on('diagnostics/update', () => {\n      store.dispatch('diagnostics/fetchDiagnostics');\n    });\n  },\n];\n"
  },
  {
    "path": "pkg/rancher-desktop/store/extensions.ts",
    "content": "import semver from 'semver';\nimport { GetterTree, MutationTree } from 'vuex';\n\nimport { fetchAPI } from './credentials';\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport MARKETPLACE_DATA from '@pkg/assets/extension-data.yaml';\nimport type { ExtensionMetadata } from '@pkg/main/extensions/types';\n\n/**\n * BackendExtensionState describes the API response from the API backend.\n * The raw response is a record of slug (i.e. extension ID without version) to\n * this structure.\n */\ninterface BackendExtensionState {\n  /** The installed extension version. */\n  version:  string;\n  /** Information from the extension's metadata.json. */\n  metadata: ExtensionMetadata;\n  /** Labels on the extension image. */\n  labels:   Record<string, string>;\n}\n\n/**\n * ExtensionState describes the data this Vuex store exposes; this is the same\n * as the backend state with the addition of a version available in the catalog.\n */\nexport type ExtensionState = BackendExtensionState & {\n  /** The extension id, excluding the version (tag). Also known as \"slug\". */\n  id:                string;\n  /** The version available in the marketplace. */\n  availableVersion?: string;\n  /** Whether this extension can be upgraded (i.e. availableVersion > version). */\n  canUpgrade:        boolean;\n};\n\ninterface ExtensionsState {\n  extensions: Record<string, ExtensionState>;\n}\n\nexport interface MarketplaceData {\n  slug:                  string;\n  version:               string;\n  containerd_compatible: boolean;\n  labels:                Record<string, string>;\n  title:                 string;\n  logo:                  string;\n  publisher:             string;\n  short_description:     string;\n}\n\nexport const state: () => ExtensionsState = () => ({ extensions: {} });\n\nexport const mutations = {\n  SET_EXTENSIONS(state, extensions: Record<string, ExtensionState>) {\n    state.extensions = extensions;\n  },\n} satisfies Partial<MutationsType<ExtensionsState>> & MutationTree<ExtensionsState>;\n\nexport const actions = {\n  async fetch({ commit, rootState }) {\n    const response = await fetchAPI('/v1/extensions', rootState);\n\n    if (!response.ok) {\n      console.log(`fetchExtensions: failed: status: ${ response.status }:${ response.statusText }`);\n\n      return;\n    }\n    const backendState: Record<string, BackendExtensionState> = await response.json();\n    const result = Object.fromEntries(Object.entries(backendState).map(([id, data]) => {\n      const marketplaceEntry = (MARKETPLACE_DATA as MarketplaceData[]).find(ext => ext.slug === id);\n      const frontendState: ExtensionState = {\n        ...data, id, canUpgrade: false,\n      };\n\n      if (marketplaceEntry) {\n        frontendState.availableVersion = marketplaceEntry.version;\n        try {\n          frontendState.canUpgrade = semver.gt(marketplaceEntry.version, data.version);\n        } catch {\n          // Either existing version or catalog version is invalid; can't upgrade.\n        }\n      }\n\n      return [id, frontendState];\n    }));\n\n    commit('SET_EXTENSIONS', result);\n  },\n\n  /**\n   * Install an extension by id.\n   * @param id The extension id; this should include the tag.\n   * @returns Error message, or `true` if extension is installed.\n   */\n  async install({ rootState, dispatch }, { id }: { id: string }) {\n    const r = await fetchAPI(`/v1/extensions/install?id=${ id }`, rootState, { method: 'POST' });\n\n    await dispatch('fetch');\n\n    if (!r.ok) {\n      return r.statusText;\n    }\n\n    return r.status === 201;\n  },\n\n  /**\n   * Uninstall an extension by id.\n   * @param id The extension id; this should _not_ include the tag.\n   * @returns Error message, or `true` if extension is uninstall.\n   */\n  async uninstall({ rootState, dispatch }, { id }: { id: string }) {\n    const r = await fetchAPI(`/v1/extensions/uninstall?id=${ id }`, rootState, { method: 'POST' });\n\n    await dispatch('fetch');\n\n    if (!r.ok) {\n      return r.statusText;\n    }\n\n    return r.status === 201;\n  },\n} satisfies ActionTree<ExtensionsState, any, typeof mutations, typeof getters>;\n\nexport const getters = {\n  installedExtensions(state): ExtensionState[] {\n    return Object.values(state.extensions);\n  },\n  marketData(): MarketplaceData[] {\n    return MARKETPLACE_DATA;\n  },\n} satisfies GetterTree<ExtensionsState, any>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/i18n.js",
    "content": "import { IntlMessageFormat } from 'intl-messageformat';\n\nimport en from '@pkg/assets/translations/en-us.yaml';\nimport { LOCALE } from '@pkg/config/cookies';\nimport { getProduct, getVendor } from '@pkg/config/private-label';\nimport { get } from '@pkg/utils/object';\n\nconst translationContext = import.meta.webpackContext('@pkg/assets/translations', { recursive: true, regExp: /.*/ });\n\nconst NONE = 'none';\n\n// Formatters can't be serialized into state\nconst intlCache = {};\n\nexport const state = function() {\n  const available = translationContext.keys().map(path => path.replace(/^.*\\/([^\\/]+)\\.[^.]+$/, '$1'));\n\n  const out = {\n    default:      'en-us',\n    selected:     null,\n    previous:     null,\n    available,\n    translations: { 'en-us': en },\n  };\n\n  return out;\n};\n\nexport const getters = {\n  selectedLocaleLabel(state) {\n    const key = `locale.${ state.selected }`;\n\n    if ( state.selected === NONE ) {\n      return `%${ key }%`;\n    } else {\n      return get(state.translations[state.default], key);\n    }\n  },\n\n  availableLocales(state, getters) {\n    const out = {};\n\n    for ( const locale of state.available ) {\n      const key = `locale.${ locale }`;\n\n      if ( state.selected === NONE ) {\n        out[locale] = `%${ key }%`;\n      } else {\n        out[locale] = get(state.translations[state.default], key);\n      }\n    }\n\n    return out;\n  },\n\n  t: state => (key, args) => {\n    if (state.selected === NONE ) {\n      return `%${ key }%`;\n    }\n\n    const cacheKey = `${ state.selected }/${ key }`;\n    let formatter = intlCache[cacheKey];\n\n    if ( !formatter ) {\n      let msg = get(state.translations[state.selected], key);\n\n      if ( !msg ) {\n        msg = get(state.translations[state.default], key);\n      }\n\n      if ( !msg ) {\n        return undefined;\n      }\n\n      if ( typeof msg === 'object' ) {\n        console.error('Translation for', cacheKey, 'is an object');\n\n        return undefined;\n      }\n\n      if ( msg?.includes('{')) {\n        formatter = new IntlMessageFormat(msg, state.selected);\n      } else {\n        formatter = msg;\n      }\n\n      intlCache[cacheKey] = formatter;\n    }\n\n    if ( typeof formatter === 'string' ) {\n      return formatter;\n    } else if ( formatter && formatter.format ) {\n      // Inject things like appName so they're always available in any translation\n      const moreArgs = {\n        vendor:  getVendor(),\n        appName: getProduct(),\n        ...args,\n      };\n\n      return formatter.format(moreArgs);\n    } else {\n      return '?';\n    }\n  },\n\n  exists: state => (key) => {\n    const cacheKey = `${ state.selected }/${ key }`;\n\n    if ( intlCache[cacheKey] ) {\n      return true;\n    }\n\n    let msg = get(state.translations[state.default], key);\n\n    if ( !msg && state.selected && state.selected !== NONE ) {\n      msg = get(state.translations[state.selected], key);\n    }\n\n    if ( msg !== undefined ) {\n      return true;\n    }\n\n    return false;\n  },\n\n  current: state => () => {\n    return state.selected;\n  },\n\n  default: state => () => {\n    return state.default;\n  },\n\n  withFallback: (state, getters) => (key, args, fallback, fallbackIsKey = false) => {\n    // Support withFallback(key,fallback) when no args\n    if ( !fallback && typeof args === 'string' ) {\n      fallback = args;\n      args = {};\n    }\n\n    if ( getters.exists(key) ) {\n      return getters.t(key, args);\n    } else if ( fallbackIsKey ) {\n      return getters.t(fallback, args);\n    } else {\n      return fallback;\n    }\n  },\n};\n\nexport const mutations = {\n  loadTranslations(state, { locale, translations }) {\n    state.translations[locale] = translations;\n  },\n\n  setSelected(state, locale) {\n    state.selected = locale;\n  },\n};\n\nexport const actions = {\n  init({ state, commit, dispatch }) {\n    let selected = this.$cookies.get(LOCALE, { parseJSON: false });\n\n    if ( !selected ) {\n      selected = state.default;\n    }\n\n    return dispatch('switchTo', selected);\n  },\n\n  async load({ commit }, locale) {\n    const translations = await translationContext(`./${ locale }.yaml`);\n\n    commit('loadTranslations', { locale, translations });\n\n    return true;\n  },\n\n  async switchTo({ state, commit, dispatch }, locale) {\n    if ( locale === NONE ) {\n      commit('setSelected', locale);\n\n      // Don't remember into cookie\n      return;\n    }\n\n    if ( !state.translations[locale] ) {\n      try {\n        await dispatch('load', locale);\n      } catch (e) {\n        if ( locale !== 'en-us' ) {\n          // Try to show something...\n\n          commit('setSelected', 'en-us');\n\n          return;\n        }\n      }\n    }\n\n    commit('setSelected', locale);\n    this.$cookies.set(LOCALE, locale, {\n      encode: x => x,\n      maxAge: 86400 * 365,\n      secure: true,\n      path:   '/',\n    });\n  },\n\n  toggleNone({ state, dispatch }) {\n    if ( state.selected === NONE ) {\n      return dispatch('switchTo', state.previous || state.default);\n    } else {\n      return dispatch('switchTo', NONE);\n    }\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/store/imageManager.ts",
    "content": "import { GetterTree, MutationTree } from 'vuex';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\ninterface ImageManagerState {\n  imageManagerState: boolean;\n}\n\nexport const state: () => ImageManagerState = () => ({ imageManagerState: false });\n\nexport const mutations = {\n  SET_IMAGE_MANAGER_STATE(state: ImageManagerState, imageManagerState: boolean) {\n    state.imageManagerState = imageManagerState;\n  },\n} satisfies Partial<MutationsType<ImageManagerState>> & MutationTree<ImageManagerState>;\n\nexport const actions = {\n  setImageManagerState({ commit }, imageManagerState: boolean) {\n    commit('SET_IMAGE_MANAGER_STATE', imageManagerState);\n  },\n} satisfies ActionTree<ImageManagerState, any, typeof mutations>;\n\nexport const getters = {\n  getImageManagerState({ imageManagerState }: ImageManagerState) {\n    return imageManagerState;\n  },\n} satisfies GetterTree<ImageManagerState, any>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/k8sManager.js",
    "content": "import { State as EngineStates } from '@pkg/backend/k8s';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\n\nexport const state = () => ({ k8sState: ipcRenderer.sendSync('k8s-state') });\n\nexport const mutations = {\n  SET_K8S_STATE(state, k8sState) {\n    state.k8sState = k8sState;\n  },\n};\n\nexport const actions = {\n  setK8sState({ commit }, k8sState) {\n    commit('SET_K8S_STATE', k8sState);\n  },\n};\n\nexport const getters = {\n  getK8sState({ k8sState }) {\n    return k8sState;\n  },\n  isReady({ k8sState }) {\n    return [EngineStates.STARTED, EngineStates.DISABLED].includes(k8sState);\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/store/page.ts",
    "content": "import { MutationTree } from 'vuex';\n\nimport { ActionTree, MutationsType } from './ts-helpers';\n\ninterface PageState {\n  title:       string;\n  description: string;\n  action:      string;\n  icon:        string;\n}\n\nexport const state: () => PageState = () => ({\n  title:       '',\n  description: '',\n  action:      '',\n  icon:        '',\n});\n\nexport const mutations = {\n  SET_TITLE(state, title) {\n    state.title = title;\n  },\n  SET_DESCRIPTION(state, description) {\n    state.description = description;\n  },\n  SET_ACTION(state, action) {\n    state.action = action;\n  },\n  SET_ICON(state, icon) {\n    state.icon = icon;\n  },\n} satisfies Partial<MutationsType<PageState>> & MutationTree<PageState>;\n\nexport const actions = {\n  setHeader({ commit }, args: { title: string, description?: string, action?: string, icon?: string }) {\n    const {\n      title, description, action, icon,\n    } = args;\n\n    commit('SET_TITLE', title);\n    commit('SET_DESCRIPTION', description ?? '');\n    commit('SET_ACTION', action ?? '');\n    commit('SET_ICON', icon ?? '');\n  },\n  setAction({ commit }, args: { action: string }) {\n    const { action } = args;\n\n    commit('SET_ACTION', action);\n  },\n} satisfies ActionTree<PageState, any, typeof mutations>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/preferences.ts",
    "content": "import _ from 'lodash';\n\nimport { ActionContext, ActionTree, MutationsType } from './ts-helpers';\n\nimport { CURRENT_SETTINGS_VERSION, defaultSettings, Settings, LockedSettingsType } from '@pkg/config/settings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { ipcRenderer } from '@pkg/utils/ipcRenderer';\nimport { RecursiveKeys, RecursivePartial, RecursiveTypes } from '@pkg/utils/typeUtils';\n\nimport type { GetterTree, MutationTree } from 'vuex';\n\ninterface Severities {\n  reset:   boolean;\n  restart: boolean;\n  error:   boolean;\n}\n\ninterface PreferencesState {\n  initialPreferences: Settings;\n  preferences:        Settings;\n  lockedPreferences:  LockedSettingsType;\n  wslIntegrations:    Record<string, string | boolean>;\n  isPlatformWindows:  boolean;\n  hasError:           boolean;\n  severities:         Severities;\n  preferencesError:   string;\n  canApply:           boolean;\n}\n\ntype Credentials = Omit<ServerState, 'pid'>;\n\ninterface CommitArgs {\n  payload?: RecursivePartial<Settings>;\n}\n\nconst uri = (port: number, path: string) => `http://localhost:${ port }/v1/${ path }`;\n\nconst proposedSettings = (port: number) => uri(port, 'propose_settings');\n\nconst settingsUri = (port: number) => uri(port, 'settings');\n\nconst lockedUri = (port: number) => uri(port, 'settings/locked');\n\n/**\n * Normalize WSL integrations configuration.\n * @param integrations The source collection, containing all WSL integrations.\n * @param mode How normalization should take place.\n *    'diff': Normalize for comparing to see if changes need to be applied.\n *    'submit': Normalize for submitting preferences.\n * @returns Returns a new object with normalized WSL configuration.\n */\nconst normalizeWslIntegrations = (integrations: Record<string, boolean>, mode: 'diff' | 'submit') => {\n  const normalizeFn = {\n    diff:   (entries: [string, boolean][]) => entries.filter(([, v]) => v),\n    submit: (entries: [string, boolean][]) => entries.map(([k, v]) => [k, v || null] as const),\n  }[mode];\n\n  return Object.fromEntries(normalizeFn(Object.entries(integrations)));\n};\n\n/**\n * Normalizes preferences for consistent usage between API and UI\n * @param preferences The preferences object to normalize.\n * @param mode How the preferences should be normalized.\n * @returns Returns a new object, containing normalized preferences data.\n */\nconst normalizePreferences = (preferences: Settings, mode: 'diff' | 'submit') => {\n  return {\n    ...preferences,\n    WSL: {\n      ...preferences.WSL,\n      integrations: normalizeWslIntegrations(preferences.WSL.integrations, mode),\n    },\n  };\n};\n\nexport const state: () => PreferencesState = () => (\n  {\n    initialPreferences: _.cloneDeep(defaultSettings),\n    preferences:        _.cloneDeep(defaultSettings),\n    lockedPreferences:  { },\n    wslIntegrations:    { },\n    isPlatformWindows:  false,\n    hasError:           false,\n    severities:         {\n      reset: false, restart: false, error: false,\n    },\n    preferencesError: '',\n    canApply:         false,\n  }\n);\n\nexport const mutations = {\n  SET_PREFERENCES(state, preferences) {\n    state.preferences = preferences;\n    state.canApply = false;\n  },\n  SET_INITIAL_PREFERENCES(state, preferences) {\n    state.initialPreferences = preferences;\n  },\n  SET_LOCKED_PREFERENCES(state, preferences) {\n    state.lockedPreferences = preferences;\n  },\n  SET_WSL_INTEGRATIONS(state, integrations) {\n    state.wslIntegrations = integrations;\n  },\n  SET_IS_PLATFORM_WINDOWS(state, isPlatformWindows) {\n    state.isPlatformWindows = isPlatformWindows;\n  },\n  SET_HAS_ERROR(state, hasError) {\n    state.hasError = hasError;\n  },\n  SET_SEVERITIES(state, severities) {\n    state.severities = severities;\n  },\n  SET_PREFERENCES_ERROR(state, error) {\n    state.preferencesError = error;\n  },\n  SET_CAN_APPLY(state, canApply) {\n    state.canApply = canApply;\n  },\n} satisfies Partial<MutationsType<PreferencesState>> & MutationTree<PreferencesState>;\n\ntype PrefActionContext = ActionContext<PreferencesState, typeof mutations>;\ninterface ProposePreferencesPayload { preferences?: Settings }\n\nexport const actions = {\n  setPreferences({ commit }, preferences: Settings) {\n    commit('SET_PREFERENCES', _.cloneDeep(preferences));\n  },\n  initializePreferences({ commit }, preferences: Settings) {\n    commit('SET_PREFERENCES', _.cloneDeep(preferences));\n    commit('SET_INITIAL_PREFERENCES', _.cloneDeep(preferences));\n  },\n  async fetchPreferences({ dispatch, commit, rootState }) {\n    const { port, user, password } = rootState.credentials.credentials;\n\n    const response = await fetch(\n      settingsUri(port),\n      {\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n      });\n\n    if (!response.ok) {\n      commit('SET_HAS_ERROR', true);\n\n      return;\n    }\n\n    const settings: Settings = await response.json();\n\n    dispatch('preferences/initializePreferences', settings, { root: true });\n  },\n  async fetchLocked({ commit, rootState }) {\n    const { port, user, password } = rootState.credentials.credentials;\n\n    const response = await fetch(\n      lockedUri(port),\n      {\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n      });\n\n    if (!response.ok) {\n      commit('SET_HAS_ERROR', true);\n\n      return;\n    }\n\n    const settings: Settings = await response.json();\n\n    commit('SET_LOCKED_PREFERENCES', settings);\n  },\n  async commitPreferences({ dispatch, getters, rootState }, args: CommitArgs = {}) {\n    const { port, user, password } = rootState.credentials.credentials;\n    const { payload } = args;\n\n    await fetch(\n      settingsUri(port),\n      {\n        method:  'PUT',\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n        body: JSON.stringify(payload ?? normalizePreferences(getters.getPreferences, 'submit')),\n      });\n\n    await dispatch('preferences/fetchPreferences', {}, { root: true });\n  },\n\n  /**\n   * Update a given property for preferences. Propose the new preferences after\n   * each update to check if kubernetes requires a reset or restart.\n   * @param context The vuex context object\n   * @param args Key, value pair that corresponds to a property and its value\n   * in the preferences object\n   */\n  async updatePreferencesData<P extends RecursiveKeys<Settings>>({\n    commit, dispatch, state, rootState,\n  }: PrefActionContext, args: { property: P, value: RecursiveTypes<Settings>[P] }): Promise<void> {\n    const { property, value } = args;\n\n    commit('SET_PREFERENCES', _.set(_.cloneDeep(state.preferences), property, value));\n\n    await dispatch(\n      'preferences/proposePreferences',\n      { ...rootState.credentials.credentials as Credentials },\n      { root: true },\n    );\n  },\n  setWslIntegrations({ commit, state }, integrations: Record<string, string | boolean>) {\n    /**\n     * Merge integrations if they exist during initialization.\n     *\n     * Issue #3232: First-time render of tabs causes the entire DOM tree to\n     * refresh, causing Preferences to initialize more than once.\n     */\n    const updatedIntegrations = _.merge({}, integrations, state.wslIntegrations);\n\n    commit('SET_WSL_INTEGRATIONS', updatedIntegrations);\n  },\n  updateWslIntegrations({ commit, state }, args: { distribution: string, value: boolean }) {\n    const { distribution, value } = args;\n\n    const integrations = _.set(_.cloneDeep(state.wslIntegrations), distribution, value);\n\n    commit('SET_WSL_INTEGRATIONS', integrations);\n  },\n  setPlatformWindows({ commit }, isPlatformWindows: boolean) {\n    commit('SET_IS_PLATFORM_WINDOWS', isPlatformWindows);\n  },\n  /**\n   * Validates the provided preferences object. Commits SET_SEVERITIES and\n   * SET_PREFERENCES_ERROR based on the validation response.\n   * @param context The vuex context object\n   * @param payload Contains credentials and an\n   * optional preferences object. Defaults to preferences stored in state if\n   * preferences are not provided.\n   * @returns A collection of severities to indicate any errors or side-effects\n   * associated with the preferences.\n   */\n  async proposePreferences(\n    { commit, state, getters, rootState },\n    { preferences }: ProposePreferencesPayload = {},\n  ): Promise<Severities> {\n    const proposal = preferences ?? normalizePreferences(getters.getPreferences, 'submit');\n    const { user, password, port } = rootState.credentials.credentials;\n\n    const result = await fetch(\n      proposedSettings(port),\n      {\n        method:  'PUT',\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n        body: JSON.stringify(proposal),\n      });\n\n    if (!result.ok) {\n      const severities = { ...state.severities, error: true };\n\n      commit('SET_SEVERITIES', severities);\n      commit('SET_PREFERENCES_ERROR', await result.text());\n\n      return severities;\n    }\n\n    const changes: Record<string, { severity: 'reset' | 'restart' }> = await result.json();\n    const values = Object.values(changes).map(v => v.severity);\n    const severities: Severities = {\n      reset:   values.includes('reset'),\n      restart: values.includes('restart'),\n      error:   false,\n    };\n\n    commit('SET_SEVERITIES', severities);\n    commit('SET_PREFERENCES_ERROR', '');\n\n    return severities;\n  },\n  async setShowMuted({ dispatch }, isMuted: boolean) {\n    await dispatch(\n      'preferences/commitPreferences',\n      {\n        payload: {\n          version:     CURRENT_SETTINGS_VERSION,\n          diagnostics: { showMuted: isMuted },\n        },\n      },\n      { root: true },\n    );\n  },\n  setCanApply({ commit }, canApply: boolean) {\n    commit('SET_CAN_APPLY', canApply);\n  },\n} satisfies ActionTree<PreferencesState, any, typeof mutations, typeof getters>;\n\nexport const getters = {\n  getPreferences(state) {\n    return state.preferences;\n  },\n  isPreferencesDirty(state) {\n    const isDirty = !_.isEqual(\n      normalizePreferences(state.initialPreferences, 'diff'),\n      normalizePreferences(state.preferences, 'diff'),\n    );\n\n    ipcRenderer.send('preferences-set-dirty', isDirty);\n\n    return isDirty;\n  },\n  getWslIntegrations(state) {\n    return state.wslIntegrations;\n  },\n  isPlatformWindows(state) {\n    return state.isPlatformWindows;\n  },\n  hasError(state) {\n    return state.hasError;\n  },\n  canApply(state, getters) {\n    return (getters.isPreferencesDirty && state.preferencesError.length === 0) || state.canApply;\n  },\n  showMuted(state) {\n    return state.preferences.diagnostics.showMuted;\n  },\n  isPreferenceLocked: (state) => (value: string) => {\n    return _.get(state.lockedPreferences, value);\n  },\n} satisfies GetterTree<PreferencesState, any>;\n"
  },
  {
    "path": "pkg/rancher-desktop/store/prefs.js",
    "content": "import { STEVE } from '@pkg/config/types';\nimport { clone } from '@pkg/utils/object';\n\nconst definitions = {};\n\nexport const create = function(name, def, opt = {}) {\n  const parseJSON = opt.parseJSON === true;\n  const asCookie = opt.asCookie === true;\n  const asUserPreference = opt.asUserPreference !== false;\n  const options = opt.options;\n\n  definitions[name] = {\n    def,\n    options,\n    parseJSON,\n    asCookie,\n    asUserPreference,\n    mangleRead:  opt.mangleRead, // Alter the value read from the API (to match old Rancher expectations)\n    mangleWrite: opt.mangleWrite, // Alter the value written back to the API (ditto)\n  };\n\n  return name;\n};\n\nexport const mapPref = function(name) {\n  return {\n    get() {\n      return this.$store.getters['prefs/get'](name);\n    },\n\n    set(value) {\n      this.$store.dispatch('prefs/set', { key: name, value });\n    },\n  };\n};\n\n// --------------------\nconst parseJSON = true; // Shortcut for setting it below\nconst asCookie = true; // Store as a cookie so that it's available before auth + on server-side\n\n// Keys must be lowercase and valid dns label (a-z 0-9 -)\nexport const CLUSTER = create('cluster', '');\nexport const LAST_NAMESPACE = create('last-namespace', '');\nexport const NAMESPACE_FILTERS = create('ns', ['all://user'], { parseJSON });\nexport const WORKSPACE = create('workspace', '');\nexport const EXPANDED_GROUPS = create('open-groups', ['cluster', 'rbac', 'serviceDiscovery', 'storage', 'workload'], { parseJSON });\nexport const FAVORITE_TYPES = create('fav-type', [], { parseJSON });\nexport const GROUP_RESOURCES = create('group-by', 'namespace');\nexport const DIFF = create('diff', 'unified', { options: ['unified', 'split'] });\nexport const THEME = create('theme', 'auto', {\n  options:     ['light', 'auto', 'dark'],\n  asCookie,\n  parseJSON,\n  mangleRead:  x => x.replace(/^ui-/, ''),\n  mangleWrite: x => `ui-${ x }`,\n});\nexport const PREFERS_SCHEME = create('pcs', '', { asCookie, asUserPreference: false });\nexport const LOCALE = create('locale', 'en-us', { asCookie });\nexport const KEYMAP = create('keymap', 'sublime', { options: ['sublime', 'emacs', 'vim'] });\nexport const ROWS_PER_PAGE = create('per-page', 100, { options: [10, 25, 50, 100, 250, 500, 1000], parseJSON });\nexport const LOGS_WRAP = create('logs-wrap', true, { parseJSON });\nexport const LOGS_TIME = create('logs-time', true, { parseJSON });\nexport const LOGS_RANGE = create('logs-range', '30 minutes', { parseJSON });\nexport const HIDE_REPOS = create('hide-repos', [], { parseJSON });\nexport const HIDE_DESC = create('hide-desc', [], { parseJSON });\nexport const HIDE_SENSITIVE = create('hide-sensitive', true, { options: [true, false], parseJSON });\nexport const SHOW_PRE_RELEASE = create('show-pre-release', false, { options: [false, true], parseJSON });\n\nexport const DATE_FORMAT = create('date-format', 'ddd, MMM D YYYY', {\n  options: [\n    'ddd, MMM D YYYY',\n    'ddd, D MMM YYYY',\n    'D/M/YYYY',\n    'M/D/YYYY',\n    'YYYY-MM-DD',\n  ],\n});\n\nexport const TIME_FORMAT = create('time-format', 'h:mm:ss a', {\n  options: [\n    'h:mm:ss a',\n    'HH:mm:ss',\n  ],\n});\n\nexport const TIME_ZONE = create('time-zone', 'local');\nexport const DEV = create('dev', false, { parseJSON });\nexport const LAST_VISITED = create('last-visited', 'home', { parseJSON });\nexport const SEEN_WHATS_NEW = create('seen-whatsnew', '', { parseJSON });\nexport const READ_WHATS_NEW = create('read-whatsnew', '', { parseJSON });\nexport const AFTER_LOGIN_ROUTE = create('after-login-route', 'home', { parseJSON } );\nexport const HIDE_HOME_PAGE_CARDS = create('home-page-cards', {}, { parseJSON } );\n\nexport const _RKE1 = 'rke1';\nexport const _RKE2 = 'rke2';\nexport const PROVISIONER = create('provisioner', _RKE2, { options: [_RKE1, _RKE2] });\n\n// Promo for Cluster Tools feature on Cluster Dashboard page\nexport const CLUSTER_TOOLS_TIP = create('hide-cluster-tools-tip', false, { parseJSON });\n\n// Maximum number of clusters to show in the slide-in menu\nexport const MENU_MAX_CLUSTERS = create('menu-max-clusters', 4, { options: [2, 3, 4, 5, 6, 7, 8, 9, 10], parseJSON });\n\n// --------------------\n\nconst cookiePrefix = 'R_';\nconst cookieOptions = {\n  maxAge:   365 * 86400,\n  path:     '/',\n  sameSite: true,\n  secure:   true,\n};\n\nexport const state = function() {\n  return {\n    cookiesLoaded: false,\n    data:          {},\n  };\n};\n\nexport const getters = {\n  get: state => (key) => {\n    const definition = definitions[key];\n\n    if (!definition) {\n      throw new Error(`Unknown preference: ${ key }`);\n    }\n\n    const user = state.data[key];\n\n    if (user !== undefined) {\n      return clone(user);\n    }\n\n    const def = clone(definition.def);\n\n    return def;\n  },\n\n  defaultValue: state => (key) => {\n    const definition = definitions[key];\n\n    if (!definition) {\n      throw new Error(`Unknown preference: ${ key }`);\n    }\n\n    return clone(definition.def);\n  },\n\n  options: state => (key) => {\n    const definition = definitions[key];\n\n    if (!definition) {\n      throw new Error(`Unknown preference: ${ key }`);\n    }\n\n    if (!definition.options) {\n      throw new Error(`Preference does not have options: ${ key }`);\n    }\n\n    return definition.options.slice();\n  },\n\n  theme: (state, getters) => {\n    let theme = getters['get'](THEME);\n    const pcs = getters['get'](PREFERS_SCHEME);\n\n    // console.log('Get Theme', theme, pcs);\n\n    // Ember UI uses this prefix\n    if ( theme.startsWith('ui-') ) {\n      theme = theme.substr(3);\n    }\n\n    if ( theme === 'auto' ) {\n      if ( pcs === 'light' || pcs === 'dark' ) {\n        return pcs;\n      }\n\n      return 'dark';\n    }\n\n    return theme;\n  },\n\n  afterLoginRoute: (state, getters) => {\n    const afterLoginRoutePref = getters['get'](AFTER_LOGIN_ROUTE);\n\n    if (typeof afterLoginRoutePref !== 'string') {\n      return afterLoginRoutePref;\n    }\n\n    switch (true) {\n    case (afterLoginRoutePref === 'home'):\n      return { name: 'home' };\n    case (afterLoginRoutePref === 'last-visited'): {\n      const lastVisitedPref = getters['get'](LAST_VISITED);\n\n      if (lastVisitedPref) {\n        return lastVisitedPref;\n      }\n      const clusterPref = getters['get'](CLUSTER);\n\n      return { name: 'c-cluster-explorer', params: { product: 'explorer', cluster: clusterPref } };\n    }\n    case (!!afterLoginRoutePref.match(/.+-dashboard$/)):\n    {\n      const clusterId = afterLoginRoutePref.split('-dashboard')[0];\n\n      return { name: 'c-cluster-explorer', params: { product: 'explorer', cluster: clusterId } };\n    }\n    default:\n      return { name: afterLoginRoutePref };\n    }\n  },\n};\n\nexport const mutations = {\n  load(state, { key, value }) {\n    state.data[key] = value;\n  },\n\n  cookiesLoaded(state) {\n    state.cookiesLoaded = true;\n  },\n};\n\nexport const actions = {\n  async set({ dispatch, commit }, opt) {\n    let { key, value } = opt;\n    const definition = definitions[key];\n    let server;\n\n    if ( opt.val ) {\n      throw new Error('Use value, not val');\n    }\n\n    commit('load', { key, value });\n\n    if ( definition.asCookie ) {\n      const opt = {\n        ...cookieOptions,\n        parseJSON: definition.parseJSON === true,\n      };\n\n      this.$cookies.set(`${ cookiePrefix }${ key }`.toUpperCase(), value, opt);\n    }\n    if ( definition.asUserPreference ) {\n      try {\n        server = await dispatch('loadServer', key); // There's no watch on prefs, so get before set...\n\n        if ( server?.data ) {\n          if ( definition.mangleWrite ) {\n            value = definition.mangleWrite(value);\n          }\n\n          if ( definition.parseJSON ) {\n            server.data[key] = JSON.stringify(value);\n          } else {\n            server.data[key] = value;\n          }\n\n          await server.save({ redirectUnauthorized: false });\n        }\n      } catch (e) {\n        // Well it failed, but not much to do about it...\n      }\n    }\n  },\n\n  async setTheme({ dispatch }, val) {\n    await dispatch('set', { key: THEME, value: val });\n  },\n\n  loadCookies({ state, commit }) {\n    if ( state.cookiesLoaded ) {\n      return;\n    }\n\n    for (const key in definitions) {\n      const definition = definitions[key];\n\n      if ( !definition.asCookie ) {\n        continue;\n      }\n\n      const opt = { parseJSON: definition.parseJSON === true };\n      const value = this.$cookies.get(`${ cookiePrefix }${ key }`.toUpperCase(), opt);\n\n      if (value !== undefined) {\n        commit('load', { key, value });\n      }\n    }\n\n    commit('cookiesLoaded');\n  },\n\n  loadTheme({ state, dispatch }) {\n    if ( process.client ) {\n      const watchDark = window.matchMedia('(prefers-color-scheme: dark)');\n      const watchLight = window.matchMedia('(prefers-color-scheme: light)');\n      const watchNone = window.matchMedia('(prefers-color-scheme: no-preference)');\n\n      const interval = 30 * 60 * 1000;\n      const nextHalfHour = interval - Math.round(new Date().getTime()) % interval;\n\n      setTimeout(() => {\n        dispatch('loadTheme');\n      }, nextHalfHour);\n      // console.log('Update theme in', nextHalfHour, 'ms');\n\n      if ( watchDark.matches ) {\n        changed('dark');\n      } else if ( watchLight.matches ) {\n        changed('light');\n      } else {\n        changed(fromClock());\n      }\n\n      watchDark.addListener((e) => {\n        if ( e.matches ) {\n          changed('dark');\n        }\n      });\n\n      watchLight.addListener((e) => {\n        if ( e.matches ) {\n          changed('light');\n        }\n      });\n\n      watchNone.addListener((e) => {\n        if ( e.matches ) {\n          changed(fromClock());\n        }\n      });\n    }\n\n    function changed(value) {\n      // console.log('Prefers Theme:', value);\n      dispatch('set', { key: PREFERS_SCHEME, value });\n    }\n\n    function fromClock() {\n      const hour = new Date().getHours();\n\n      if ( hour < 7 || hour >= 18 ) {\n        return 'dark';\n      }\n\n      return 'light';\n    }\n  },\n\n  async loadServer({ state, dispatch, commit }, ignoreKey) {\n    let server = { data: {} };\n\n    try {\n      const all = await dispatch('management/findAll', {\n        type: STEVE.PREFERENCE,\n        opt:  {\n          url:                  'userpreferences',\n          force:                true,\n          watch:                false,\n          redirectUnauthorized: false,\n        },\n      }, { root: true });\n\n      server = all?.[0];\n    } catch (e) {\n      console.error('Error loading preferences', e);\n    }\n\n    if ( !server?.data ) {\n      return;\n    }\n\n    for (const key in definitions) {\n      const definition = definitions[key];\n      let value = clone(server.data[key]);\n\n      if ( value === undefined || key === ignoreKey) {\n        continue;\n      }\n\n      if ( definition.parseJSON ) {\n        try {\n          value = JSON.parse(value);\n        } catch (err) {\n          console.error('Error parsing server pref', key, value, err);\n          continue;\n        }\n      }\n\n      if ( definition.mangleRead ) {\n        value = definition.mangleRead(value);\n      }\n\n      commit('load', { key, value });\n    }\n\n    return server;\n  },\n\n  setLastVisited({ state, dispatch }, route) {\n    if (!route) {\n      return;\n    }\n\n    const toSave = getLoginRoute(route);\n\n    return dispatch('set', { key: LAST_VISITED, value: toSave });\n  },\n\n  toggleTheme({ getters, dispatch }) {\n    const value = getters[THEME] === 'light' ? 'dark' : 'light';\n\n    return dispatch('set', { key: THEME, value });\n  },\n};\n\nfunction getLoginRoute(route) {\n  let parts = route.name?.split('-') || [];\n  const params = {};\n  const routeParams = route.params || {};\n\n  // Find the 'resource' part of the route, if it is there\n  const index = parts.findIndex(p => p === 'resource');\n\n  if (index >= 0) {\n    parts = parts.slice(0, index);\n  }\n\n  // Just keep the params that are needed\n  parts.forEach((param) => {\n    if (routeParams[param]) {\n      params[param] = routeParams[param];\n    }\n  });\n\n  return {\n    name: parts.join('-'),\n    params,\n  };\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/store/resource-fetch.js",
    "content": "export const state = function() {\n  return {\n    refreshFlag:                null,\n    isTooManyItemsToAutoUpdate: false,\n    manualRefreshIsLoading:     false,\n  };\n};\n\nexport const getters = {\n  isTooManyItemsToAutoUpdate: (state) => state.isTooManyItemsToAutoUpdate,\n  refreshFlag:                (state) => state.refreshFlag,\n  manualRefreshIsLoading:     (state) => state.manualRefreshIsLoading,\n};\n\nexport const mutations = {\n  updateIsTooManyItems(state, data) {\n    state.isTooManyItemsToAutoUpdate = data;\n  },\n  updateRefreshFlag(state, data) {\n    state.refreshFlag = data;\n  },\n  updateManualRefreshIsLoading(state, data) {\n    state.manualRefreshIsLoading = data;\n  },\n};\n\nexport const actions = {\n  clearData({ commit, state }) {\n    commit('updateIsTooManyItems', false);\n    commit('updateRefreshFlag', null);\n  },\n  updateIsTooManyItems({ commit }, data) {\n    commit('updateIsTooManyItems', data);\n  },\n  updateManualRefreshIsLoading({ commit }, data) {\n    commit('updateManualRefreshIsLoading', data);\n  },\n  doManualRefresh({ commit, dispatch, state }) {\n    // simple change to trigger request on the resource-fetch mixin....\n    const finalData = new Date().getTime();\n\n    commit('updateRefreshFlag', finalData);\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/store/snapshots.ts",
    "content": "import { GetterTree, MutationTree } from 'vuex';\n\nimport { fetchAPI } from './credentials';\nimport { ActionTree, MutationsType } from './ts-helpers';\n\nimport { Snapshot } from '@pkg/main/snapshots/types';\n\ninterface SnapshotsState {\n  snapshots: Snapshot[]\n}\n\nexport const state: () => SnapshotsState = () => ({ snapshots: [] });\n\nexport const mutations = {\n  SET_SNAPSHOTS(state: SnapshotsState, snapshots: Snapshot[]) {\n    state.snapshots = snapshots;\n  },\n} satisfies Partial<MutationsType<SnapshotsState>> & MutationTree<SnapshotsState>;\n\nexport const actions = {\n  async fetch({ commit, rootState }) {\n    const response = await fetchAPI('/v1/snapshots', rootState);\n\n    if (!response.ok) {\n      console.log(`fetchSnapshots: failed: status: ${ response.status }:${ response.statusText }`);\n\n      const error = await response.text();\n\n      return error;\n    }\n    const snapshots: Snapshot[] = await response.json();\n\n    commit('SET_SNAPSHOTS', snapshots.sort((a, b) => b.created.localeCompare(a.created)));\n  },\n\n  async create({ rootState, dispatch }, snapshot: Snapshot) {\n    const body = JSON.stringify(snapshot ?? {});\n\n    const response = await fetchAPI('/v1/snapshots', rootState, { method: 'POST', body });\n\n    if (!response.ok) {\n      console.log(`createSnapshot: failed: status: ${ response.status }:${ response.statusText }`);\n\n      const error = await response.text();\n\n      return error;\n    }\n\n    await dispatch('fetch');\n  },\n\n  async delete({ rootState, dispatch }, name: string) {\n    const response = await fetchAPI(`/v1/snapshots?name=${ encodeURIComponent(name) }`, rootState, { method: 'DELETE' });\n\n    if (!response.ok) {\n      console.log(`deleteSnapshot: failed: status: ${ response.status }:${ response.statusText }`);\n\n      const error = await response.text();\n\n      return error;\n    }\n\n    await dispatch('fetch');\n  },\n\n  async restore({ rootState }, name: string) {\n    const response = await fetchAPI(`/v1/snapshot/restore?name=${ encodeURIComponent(name) }`, rootState, { method: 'POST' });\n\n    if (!response.ok) {\n      console.log(`restoreSnapshot: failed: status: ${ response.status }:${ response.statusText }`);\n\n      const error = await response.text();\n\n      return error;\n    }\n  },\n} satisfies ActionTree<SnapshotsState, any, typeof mutations, typeof getters>;\n\nexport const getters: GetterTree<SnapshotsState, SnapshotsState> = {\n  list(state: SnapshotsState) {\n    return state.snapshots;\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/store/transientSettings.ts",
    "content": "import _ from 'lodash';\nimport semver from 'semver';\n\nimport { ActionContext, MutationsType } from './ts-helpers';\n\nimport { defaultTransientSettings, NavItemName, TransientSettings } from '@pkg/config/transientSettings';\nimport type { ServerState } from '@pkg/main/commandServer/httpCommandServer';\nimport { RecursivePartial } from '@pkg/utils/typeUtils';\n\nimport type { ActionTree, GetterTree } from 'vuex';\n\ntype Preferences = typeof defaultTransientSettings.preferences;\n\ninterface CommitArgs {\n  payload?: RecursivePartial<TransientSettings>;\n}\n\ninterface NavigatePrefsDialogArgs extends ServerState {\n  navItem: NavItemName;\n  tab?:    string;\n}\n\ntype ExtendedTransientSettings = TransientSettings & {\n  macOsVersion?: semver.SemVer;\n  isArm?:        boolean;\n};\n\nconst uri = (port: number) => `http://localhost:${ port }/v1/transient_settings`;\n\nexport const state: () => ExtendedTransientSettings = () => _.cloneDeep(defaultTransientSettings);\n\nexport const mutations: MutationsType<ExtendedTransientSettings> = {\n  SET_PREFERENCES(state, preferences) {\n    state.preferences = preferences;\n  },\n  SET_NO_MODAL_DIALOGS(state, noModalDialogs) {\n    state.noModalDialogs = noModalDialogs;\n  },\n  SET_MAC_OS_VERSION(state, macOsVersion) {\n    state.macOsVersion = macOsVersion;\n  },\n  SET_IS_ARM(state, isArm) {\n    state.isArm = isArm;\n  },\n};\n\nexport const actions = {\n  setPreferences({ commit }, preferences: Preferences) {\n    commit('SET_PREFERENCES', _.cloneDeep(preferences));\n  },\n  async fetchTransientSettings({ commit, rootState }) {\n    const { port, user, password } = rootState.credentials.credentials;\n\n    const response = await fetch(\n      uri(port),\n      {\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n      });\n    const transientSettings: TransientSettings = await response.json();\n\n    commit('SET_PREFERENCES', _.cloneDeep(transientSettings.preferences));\n  },\n  async commitPreferences({ state, dispatch, rootState }, args: CommitArgs) {\n    const { port, user, password } = rootState.credentials.credentials;\n    const { payload } = args;\n\n    await fetch(\n      uri(port),\n      {\n        method:  'PUT',\n        headers: new Headers({\n          Authorization:  `Basic ${ window.btoa(`${ user }:${ password }`) }`,\n          'Content-Type': 'application/x-www-form-urlencoded',\n        }),\n        body: JSON.stringify(payload ?? state.preferences),\n      });\n\n    await dispatch('fetchTransientSettings', args);\n  },\n  async navigatePrefDialog(context, args: NavigatePrefsDialogArgs) {\n    const commitArgs = _.omit(args, 'navItem', 'tab');\n    const { navItem, tab } = args;\n    const preferences = { navItem: { current: navItem, currentTabs: { [navItem]: tab } } };\n\n    await context.dispatch('commitPreferences', { ...commitArgs, payload: { preferences } });\n  },\n  setMacOsVersion({ commit }, macOsVersion: semver.SemVer) {\n    commit('SET_MAC_OS_VERSION', macOsVersion);\n  },\n  setIsArm({ commit }, isArm: boolean) {\n    commit('SET_IS_ARM', isArm);\n  },\n} satisfies ActionTree<TransientSettings, any>;\n\nexport const getters: GetterTree<TransientSettings, TransientSettings> = {\n  getPreferences(state: TransientSettings) {\n    return state.preferences;\n  },\n  getNoModalDialogs(state: TransientSettings) {\n    return state.noModalDialogs;\n  },\n  getCurrentNavItem(state: TransientSettings) {\n    return state.preferences?.navItem?.current;\n  },\n  getActiveTab(state: TransientSettings) {\n    const currentNavItem = state.preferences?.navItem.current;\n\n    return state.preferences?.navItem?.currentTabs[currentNavItem];\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/store/ts-helpers.ts",
    "content": "import type { UpperSnakeCase } from '@pkg/utils/typeUtils';\n\nimport type { CommitOptions, Dispatch, GetterTree, MutationTree, Store } from 'vuex';\n\n/**\n * MutationsType is used to describe the type that `mutations` should have.\n * This has a `SET_` method per property in `State`, that takes a payload of the\n * correct type.  Note that we may have additional mutations available; typically\n * this is used as `const mutations = { ... } satisfies MutationsType<State>`.\n */\nexport type MutationsType<T> = {\n  [key in keyof T as `SET_${ UpperSnakeCase<key> }`]?: (state: T, payload: T[key]) => any;\n};\n\n/**\n * MutationsPayloadType converts from a MutationsType to a type with the same\n * keys but just the payload as the value.\n */\ntype MutationsPayloadType<M> = {\n  [key in keyof M]: M[key] extends (...args: any) => any ? Parameters<M[key]>[1] : never;\n};\n\n/**\n * ActionContext is the first argument for an action.  We only declare the\n * subset we currently need.  We're not using the types from Vuex as that does\n * not provide typing to match the mutations.\n */\nexport interface ActionContext<S, M = MutationsType<S>, G = GetterTree<S, any>> {\n  commit<mutationType extends keyof M>(\n    type: mutationType,\n    payload: MutationsPayloadType<M>[mutationType],\n    commitOptions?: CommitOptions): void;\n  dispatch:  Dispatch;\n  state:     S;\n  rootState: any;\n  getters:   { [key in keyof G]: G[key] extends (...args: any) => any ? ReturnType<G[key]> : never };\n}\n\n// Copies from the vuex definition, but using our override ActionContext above.\ntype ActionHandler<S, R, M, G> = (this: Store<R>, context: ActionContext<S, M, G>, payload?: any) => any;\nexport interface ActionObject<S, R, M, G> {\n  root?:   boolean;\n  handler: ActionHandler<S, R, M, G>;\n}\ntype Action<S, R, M, G> = ActionHandler<S, R, M, G> | ActionObject<S, R, M, G>;\n\nexport type ActionTree<\n  S,\n  R,\n  M extends MutationsType<S> & MutationTree<S> = MutationsType<S> & MutationTree<S>,\n  G extends GetterTree<S, any> = GetterTree<S, any>,\n> = Record<string, Action<S, R, M, G>>;\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/CHANGELOG.md",
    "content": "# Rancher Desktop related changes\n\nThis module has been imported from https://github.com/jorangreef/sudo-prompt/tree/v9.2.1 (commit c3cc31a) and modified for Rancher Desktop:\n\nThe `applet.app` used to be included as a base64 encoded ZIP file inside `index.js` and extracted at runtime into a temp directory. The extracted app was renamed to match the `name` and `icns` specified by the caller, and the commands were written into `applet.app/Content/MacOS/sudo-prompt-command`.\n\nThe bundled applet did not include support for `aarch64` machines, so needed Rosetta2 installed to run. It was also not signed.\n\n## Changes\n\nThe applet source code has been moved to `<repo>/src/sudo-prompt` and is built from source using `osacompile`, so `applet` will be an up-to-date universal binary supporting `x86_64` and `aarch64`.\n\nThe applet is placed into `<repo>/resources/darwin/internal/Rancher Desktop.app`. The app name is displayed as part of the dialog: \"Rancher Desktop wants to make changes\".\n\nThe `Contents/Info.plist` file has the `CFBundleName` set to \"Rancher Desktop Password Prompt\".\n\nA `.icns` format icon has been created (the old `.png` file doesn't seem to work with the new applet) and is stored into `Contents/Resources/applet.icns`.\n\nThe `sudo-prompt-script` has been moved from `Contents/MacOS` to `Contents/Resources/Scripts` because it cannot be code-signed.\n\nWhen the `RD_SUDO_PROMPT_OSASCRIPT` environment variable is set then the `Contents/Resources/Scripts/main.scpt` file (the compiled version of `sudo-prompt.applescript`) is executed via `osascript` instead of the applet. This will show an approval prompt that supports the Apple watch, or a touch id keyboard, but will not use the `Rancher Desktop` name or icon in the dialog.\n\nThe `sudo-prompt.applescript` has been modified to locate the `sudo-prompt-script` inside the applet because the working directory will no longer be inside the app.\n\nAll this means that the app can now be code-signed and notarized and will not be modified at runtime.\n\nThe app is being build by `yarn` during the `postinstall` phase with a custom dependency script.\n\nThe `index.js` code to modify the app at runtime has been removed and the logic simplified. `name` and `icns` options are ignored in the macOS `sudo` function.\n<hr>\n\n# Original CHANGELOG below\n\n## [9.2.0] 2020-04-29\n\n### Fixed\n\n- Update TypeScript types to accommodate recent changes, see\n[#117](https://github.com/jorangreef/sudo-prompt/issues/117).\n\n## [9.1.0] 2019-11-13\n\n### Added\n\n- Add TypeScript types.\n\n## [9.0.0] 2019-06-03\n\n### Changed\n\n- Make cross-platform `stdout`, `stderr` behavior consistent, see\n[#89](https://github.com/jorangreef/sudo-prompt/issues/89).\n\n- Preserve current working directory on all platforms.\n\n- Improve kdesudo dialog appearance.\n\n### Added\n\n- Add `options.env` to set environment variables on all platforms, see\n[#91](https://github.com/jorangreef/sudo-prompt/issues/91).\n\n### Fixed\n\n- Always return PERMISSION_DENIED as an Error object.\n\n- Support multiple commands separated by semicolons on Linux, see\n[#39](https://github.com/jorangreef/sudo-prompt/issues/39).\n\n- Distinguish between elevation errors and command errors on Linux, see\n[#88](https://github.com/jorangreef/sudo-prompt/issues/88).\n\n- Fix Windows to return `PERMISSION_DENIED` Error even when Windows' error\nmessages are internationalized, see\n[#96](https://github.com/jorangreef/sudo-prompt/issues/96).\n\n## [8.2.5] 2018-12-12\n\n### Fixed\n\n- Whitelist package.json files.\n\n## [8.2.4] 2018-12-12\n\n### Added\n\n- A CHANGELOG.md file, see\n[#78](https://github.com/jorangreef/sudo-prompt/issues/78).\n\n## [8.2.3] 2018-09-11\n\n### Fixed\n\n- README: Link to concurrency discussion.\n\n## [8.2.2] 2018-09-11\n\n### Fixed\n\n- README: Details on concurrency.\n\n## [8.2.1] 2018-09-11\n\n### Fixed\n\n- A rare idempotency edge case where a command might have been run more than\nonce, given a very specific OS environment setup.\n\n## [8.2.0] 2018-03-22\n\n### Added\n\n- Windows: Fix `cd` when `cwd` is on another drive, see\n[#70](https://github.com/jorangreef/sudo-prompt/issues/70).\n\n## [8.1.0] 2018-01-10\n\n### Added\n\n- Linux: Increase `maxBuffer` limit to 128 MiB, see\n[#66](https://github.com/jorangreef/sudo-prompt/issues/66).\n\n## [8.0.0] 2018-11-02\n\n### Changed\n\n- Windows: Set code page of command batch script to UTF-8.\n\n## [7.1.1] 2017-07-18\n\n### Fixed\n\n- README: Explicitly mention that no child process is returned.\n\n## [7.0.0] 2017-03-15\n\n### Changed\n\n- Add status code to errors on Windows and macOS.\n\n## [6.2.1] 2016-12-16\n\n### Fixed\n\n- README: Syntax highlighting.\n\n## [6.2.0] 2016-08-17\n\n### Fixed\n\n- README: Rename OS X to macOS.\n\n## [6.1.0] 2016-08-02\n\n### Added\n\n- Yield an error if no polkit authentication agent is found, see\n[#29](https://github.com/jorangreef/sudo-prompt/issues/29).\n\n## [6.0.2] 2016-07-21\n\n### Fixed\n\n- README: Update explanation of Linux behavior.\n\n## [6.0.1] 2016-07-15\n\n### Fixed\n\n- Update keywords in package.json.\n\n## [6.0.0] 2016-07-15\n\n### Changed\n\n- Add support for Windows.\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Joran Dirk Greef\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/README.md",
    "content": "# Rancher Desktop sudo-prompt\n\nThis module has been imported from [jorangreef/sudo-prompt: Run a command using sudo, prompting the user with an OS dialog if necessary.](https://github.com/jorangreef/sudo-prompt).\n\nIt is no longer a reusable module, but has been modified specifically for Rancher Desktop usage; see details in the [changelog](CHANGELOG.md).\n\n<hr>\n\n# Original README below\n\n# sudo-prompt\n\nRun a non-graphical terminal command using `sudo`, prompting the user with a graphical OS dialog if necessary. Useful for background Node.js applications or native Electron apps that need `sudo`.\n\n## Cross-Platform\n`sudo-prompt` provides a native OS dialog prompt on **macOS**, **Linux** and **Windows**.\n\n![macOS](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/macos.png)\n\n![Linux](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/linux.png)\n\n![Windows](https://raw.githubusercontent.com/jorangreef/sudo-prompt/master/windows.png)\n\n## Installation\n`sudo-prompt` has no external dependencies and does not require any native bindings.\n```\nnpm install sudo-prompt\n```\n\n## Usage\nNote: Your command should not start with the `sudo` prefix.\n```javascript\nvar sudo = require('sudo-prompt');\nvar options = {\n  name: 'Electron',\n  icns: '/Applications/Electron.app/Contents/Resources/Electron.icns', // (optional)\n};\nsudo.exec('echo hello', options,\n  function(error, stdout, stderr) {\n    if (error) throw error;\n    console.log('stdout: ' + stdout);\n  }\n);\n```\n\n`sudo-prompt` will use `process.title` as `options.name` if `options.name` is not provided. `options.name` must be alphanumeric only (spaces are supported) and at most 70 characters.\n\n`sudo-prompt` will preserve the current working directory on all platforms. Environment variables can be set explicitly using `options.env`.\n\n**`sudo-prompt.exec()` is different to `child-process.exec()` in that no child process is returned (due to platform and permissions constraints).**\n\n## Behavior\nOn macOS, `sudo-prompt` should behave just like the `sudo` command in the shell. If your command does not work with the `sudo` command in the shell (perhaps because it uses `>` redirection to a restricted file), then it may not work with `sudo-prompt`. However, it is still possible to use sudo-prompt to get a privileged shell, [see this closed issue for more information](https://github.com/jorangreef/sudo-prompt/issues/1).\n\nOn Linux, `sudo-prompt` will use either `pkexec` or `kdesudo` to show the password prompt and run your command. Where possible, `sudo-prompt` will try and get these to mimic `sudo`. Depending on which binary is used, and due to the limitations of some binaries, the name of your program or the command itself may be displayed to your user. `sudo-prompt` will not use `gksudo` since `gksudo` does not support concurrent prompts. Passing `options.icns` is currently not supported by `sudo-prompt` on Linux. Patches are welcome to add support for icons based on `polkit`.\n\nOn Windows, `sudo-prompt` will elevate your command using User Account Control (UAC). Passing `options.name` or `options.icns` is currently not supported by `sudo-prompt` on Windows.\n\n## Non-graphical terminal commands only\nJust as you should never use `sudo` to launch any graphical applications, you should never use `sudo-prompt` to launch any graphical applications. Doing so could cause files in your home directory to become owned by root. `sudo-prompt` is explicitly designed to launch non-graphical terminal commands. For more information, [read this post](http://www.psychocats.net/ubuntu/graphicalsudo).\n\n## Concurrency\nOn systems where the user has opted to have `tty-tickets` enabled (most systems), each call to `exec()` will result in a separate password prompt. Where `tty-tickets` are disabled, subsequent calls to `exec()` will still require a password prompt, even where the user's `sudo` timestamp file remains valid, due to edge cases with `sudo` itself, [see this discussion for more information](https://github.com/jorangreef/sudo-prompt/pull/76).\n\nYou should never rely on `sudo-prompt` to execute your calls in order. If you need to enforce ordering of calls, then you should explicitly order your calls in your application. Where your commands are short-lived, you should always queue your calls to `exec()` to make sure your user is not overloaded with password prompts.\n\n## Invalidating the timestamp\nOn macOS and Linux, you can invalidate the user's `sudo` timestamp file to force the prompt to appear by running the following command in your terminal:\n\n```sh\n$ sudo -k\n```\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/index.d.ts",
    "content": "export function exec(cmd: string,\n  options?: ((error?: Error, stdout?: string | Buffer, stderr?: string | Buffer) => void)\n                | { name?: string, icns?: string, env?: Record<string, string> },\n  callback?: (error?: Error, stdout?: string | Buffer, stderr?: string | Buffer) => void): void;\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/index.js",
    "content": "const Node = {\n  child:    require('child_process'),\n  crypto:   require('crypto'),\n  electron: require('electron'),\n  fs:       require('fs'),\n  os:       require('os'),\n  path:     require('path'),\n  process,\n  util:     require('util'),\n};\n\nfunction Attempt(instance, end) {\n  const platform = Node.process.platform;\n\n  if (platform === 'darwin') {\n    return Mac(instance, end);\n  }\n  if (platform === 'linux') {\n    return Linux(instance, end);\n  }\n  if (platform === 'win32') {\n    return Windows(instance, end);\n  }\n  end(new Error('Platform not yet supported.'));\n}\n\nfunction EscapeDoubleQuotes(string) {\n  if (typeof string !== 'string') {\n    throw new TypeError('Expected a string.');\n  }\n\n  return string.replace(/\"/g, '\\\\\"');\n}\n\nfunction Exec() {\n  if (arguments.length < 1 || arguments.length > 3) {\n    throw new Error('Wrong number of arguments.');\n  }\n  const command = arguments[0];\n  let options = {};\n  let end = function() {};\n\n  if (typeof command !== 'string') {\n    throw new TypeError('Command should be a string.');\n  }\n  if (arguments.length === 2) {\n    if (arguments[1] !== null && typeof arguments[1] === 'object') {\n      options = arguments[1];\n    } else if (typeof arguments[1] === 'function') {\n      end = arguments[1];\n    } else {\n      throw new TypeError('Expected options or callback.');\n    }\n  } else if (arguments.length === 3) {\n    if (arguments[1] !== null && typeof arguments[1] === 'object') {\n      options = arguments[1];\n    } else {\n      throw new TypeError('Expected options to be an object.');\n    }\n    if (typeof arguments[2] === 'function') {\n      end = arguments[2];\n    } else {\n      throw new TypeError('Expected callback to be a function.');\n    }\n  }\n  if (/^sudo/i.test(command)) {\n    return end(new Error('Command should not be prefixed with \"sudo\".'));\n  }\n  if (typeof options.name === 'undefined') {\n    const title = Node.process.title;\n\n    if (ValidName(title)) {\n      options.name = title;\n    } else {\n      return end(new Error('process.title cannot be used as a valid name.'));\n    }\n  } else if (!ValidName(options.name)) {\n    let error = '';\n\n    error += 'options.name must be alphanumeric only ';\n    error += '(spaces are allowed) and <= 70 characters.';\n\n    return end(new Error(error));\n  }\n  if (typeof options.icns !== 'undefined') {\n    if (typeof options.icns !== 'string') {\n      return end(new Error('options.icns must be a string if provided.'));\n    } else if (options.icns.trim().length === 0) {\n      return end(new Error('options.icns must not be empty if provided.'));\n    }\n  }\n  if (typeof options.env !== 'undefined') {\n    if (typeof options.env !== 'object') {\n      return end(new Error('options.env must be an object if provided.'));\n    } else if (Object.keys(options.env).length === 0) {\n      return end(new Error('options.env must not be empty if provided.'));\n    } else {\n      for (const key in options.env) {\n        const value = options.env[key];\n\n        if (typeof key !== 'string' || typeof value !== 'string') {\n          return end(\n            new Error('options.env environment variables must be strings.'),\n          );\n        }\n        // \"Environment variable names used by the utilities in the Shell and\n        // Utilities volume of IEEE Std 1003.1-2001 consist solely of uppercase\n        // letters, digits, and the '_' (underscore) from the characters defined\n        // in Portable Character Set and do not begin with a digit. Other\n        // characters may be permitted by an implementation; applications shall\n        // tolerate the presence of such names.\"\n        if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {\n          return end(\n            new Error(\n              `options.env has an invalid environment variable name: ${\n                JSON.stringify(key) }`,\n            ),\n          );\n        }\n        if (/[\\r\\n]/.test(value)) {\n          return end(\n            new Error(\n              `options.env has an invalid environment variable value: ${\n                JSON.stringify(value) }`,\n            ),\n          );\n        }\n      }\n    }\n  }\n  const platform = Node.process.platform;\n\n  if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32') {\n    return end(new Error('Platform not yet supported.'));\n  }\n  const instance = {\n    command,\n    options,\n    uuid: undefined,\n    path: undefined,\n  };\n\n  Attempt(instance, end);\n}\n\nfunction Linux(instance, end) {\n  LinuxBinary(instance,\n    (error, binary) => {\n      if (error) {\n        return end(error);\n      }\n      let command = [];\n\n      // Preserve current working directory:\n      command.push(`cd \"${ EscapeDoubleQuotes(Node.process.cwd()) }\";`);\n      // Export environment variables:\n      for (const key in instance.options.env) {\n        const value = instance.options.env[key];\n\n        command.push(`export ${ key }=\"${ EscapeDoubleQuotes(value) }\";`);\n      }\n      command.push(`\"${ EscapeDoubleQuotes(binary) }\"`);\n      if (/kdesudo/i.test(binary)) {\n        command.push(\n          '--comment',\n          `\"${ instance.options.name } wants to make changes. ` +\n          `Enter your password to allow this.\"`,\n        );\n        command.push('-d'); // Do not show the command to be run in the dialog.\n        command.push('--');\n      } else if (/pkexec/i.test(binary)) {\n        command.push('--disable-internal-agent');\n      }\n      const magic = 'SUDOPROMPT\\n';\n\n      command.push(\n        `/bin/bash -c \"echo ${ EscapeDoubleQuotes(magic.trim()) }; ${\n          EscapeDoubleQuotes(instance.command)\n        }\"`,\n      );\n      command = command.join(' ');\n      Node.child.exec(command, { encoding: 'utf-8', maxBuffer: MAX_BUFFER },\n        (error, stdout, stderr) => {\n          // ISSUE 88:\n          // We must distinguish between elevation errors and command errors.\n          //\n          // KDESUDO:\n          // kdesudo provides no way to do this. We add a magic marker to know\n          // if elevation succeeded. Any error thereafter is a command error.\n          //\n          // PKEXEC:\n          // \"Upon successful completion, the return value is the return value of\n          // PROGRAM. If the calling process is not authorized or an\n          // authorization could not be obtained through authentication or an\n          // error occured, pkexec exits with a return value of 127. If the\n          // authorization could not be obtained because the user dismissed the\n          // authentication dialog, pkexec exits with a return value of 126.\"\n          //\n          // However, we do not rely on pkexec's return of 127 since our magic\n          // marker is more reliable, and we already use it for kdesudo.\n          const elevated = stdout && stdout.slice(0, magic.length) === magic;\n\n          if (elevated) {\n            stdout = stdout.slice(magic.length);\n          }\n          // Only normalize the error if it is definitely not a command error:\n          // In other words, if we know that the command was never elevated.\n          // We do not inspect error messages beyond NO_POLKIT_AGENT.\n          // We cannot rely on English errors because of internationalization.\n          if (error && !elevated) {\n            if (/No authentication agent found/.test(stderr)) {\n              error.message = NO_POLKIT_AGENT;\n            } else {\n              error.message = PERMISSION_DENIED;\n            }\n          }\n          end(error, stdout, stderr);\n        },\n      );\n    },\n  );\n}\n\nfunction LinuxBinary(_, end) {\n  let index = 0;\n  // We used to prefer gksudo over pkexec since it enabled a better prompt.\n  // However, gksudo cannot run multiple commands concurrently.\n  const paths = ['/usr/bin/kdesudo', '/usr/bin/pkexec'];\n\n  function test() {\n    if (index === paths.length) {\n      return end(new Error('Unable to find pkexec or kdesudo.'));\n    }\n    const path = paths[index++];\n\n    Node.fs.stat(path,\n      (error) => {\n        if (error) {\n          if (error.code === 'ENOTDIR') {\n            return test();\n          }\n          if (error.code === 'ENOENT') {\n            return test();\n          }\n          end(error);\n        } else {\n          end(undefined, path);\n        }\n      },\n    );\n  }\n  test();\n}\n\nfunction Mac(instance, callback) {\n  const temp = Node.os.tmpdir();\n\n  if (!temp) {\n    return callback(new Error('os.tmpdir() not defined.'));\n  }\n  const user = Node.process.env.USER; // Applet shell scripts require $USER.\n\n  if (!user) {\n    return callback(new Error('env[\\'USER\\'] not defined.'));\n  }\n  UUID(instance,\n    (error, uuid) => {\n      if (error) {\n        return callback(error);\n      }\n      instance.uuid = uuid;\n      instance.path = Node.path.join(\n        temp,\n        instance.uuid,\n      );\n      Node.fs.mkdir(instance.path, 0o700,\n        (error) => {\n          if (error) {\n            return callback(error);\n          }\n          function end(error, stdout, stderr) {\n            Remove(instance.path,\n              (errorRemove) => {\n                if (error) {\n                  return callback(error);\n                }\n                if (errorRemove) {\n                  return callback(errorRemove);\n                }\n                callback(undefined, stdout, stderr);\n              },\n            );\n          }\n          MacCommand(instance,\n            (error) => {\n              if (error) {\n                return end(error);\n              }\n              MacOpen(instance,\n                (error, stdout, stderr) => {\n                  if (error) {\n                    return end(error, stdout, stderr);\n                  }\n                  MacResult(instance, end);\n                },\n              );\n            },\n          );\n        },\n      );\n    },\n  );\n}\n\nfunction MacCommand(instance, end) {\n  const path = Node.path.join(instance.path, 'sudo-prompt-command');\n  let script = [];\n\n  // Preserve current working directory:\n  // We do this for commands that rely on relative paths.\n  // This runs in a subshell and will not change the cwd of sudo-prompt-script.\n  script.push(`cd \"${ EscapeDoubleQuotes(Node.process.cwd()) }\"`);\n  // Export environment variables:\n  for (const key in instance.options.env) {\n    const value = instance.options.env[key];\n\n    script.push(`export ${ key }=\"${ EscapeDoubleQuotes(value) }\"`);\n  }\n  script.push(instance.command);\n  script = script.join('\\n');\n  Node.fs.writeFile(path, script, 'utf-8', end);\n}\n\nfunction MacOpen(instance, end) {\n  let basePath;\n\n  if (Node.electron.app?.isPackaged) {\n    basePath = process.resourcesPath;\n  } else {\n    basePath = process.cwd();\n  }\n\n  // We must set the cwd so that the AppleScript can find the sudo-prompt-command script.\n  const options = {\n    cwd:      instance.path,\n    encoding: 'utf-8',\n  };\n\n  if (Node.process.env.RD_SUDO_PROMPT_OSASCRIPT) {\n    const script = Node.path.join(basePath, 'resources', 'darwin', 'internal', 'Rancher Desktop.app', 'Contents', 'Resources', 'Scripts', 'main.scpt');\n\n    Node.child.exec(`/usr/bin/osascript \"${ EscapeDoubleQuotes(Node.path.normalize(script)) }\"`, options, end);\n  } else {\n    // We must run the binary directly so that the cwd will apply.\n    const binary = Node.path.join(basePath, 'resources', 'darwin', 'internal', 'Rancher Desktop.app', 'Contents', 'MacOS', 'applet');\n\n    Node.child.exec(`\"${ EscapeDoubleQuotes(Node.path.normalize(binary)) }\"`, options, end);\n  }\n}\n\nfunction MacResult(instance, end) {\n  Node.fs.readFile(Node.path.join(instance.path, 'code'), 'utf-8',\n    (error, code) => {\n      if (error) {\n        if (error.code === 'ENOENT') {\n          return end(new Error(PERMISSION_DENIED));\n        }\n        end(error);\n      } else {\n        Node.fs.readFile(Node.path.join(instance.path, 'stdout'), 'utf-8',\n          (error, stdout) => {\n            if (error) {\n              return end(error);\n            }\n            Node.fs.readFile(Node.path.join(instance.path, 'stderr'), 'utf-8',\n              (error, stderr) => {\n                if (error) {\n                  return end(error);\n                }\n                code = parseInt(code.trim(), 10); // Includes trailing newline.\n                if (code === 0) {\n                  end(undefined, stdout, stderr);\n                } else {\n                  error = new Error(\n                    `Command failed: ${ instance.command }\\n${ stderr }`,\n                  );\n                  error.code = String(code);\n                  end(error, stdout, stderr);\n                }\n              },\n            );\n          },\n        );\n      }\n    },\n  );\n}\n\nfunction Remove(path, end) {\n  if (typeof path !== 'string' || !path.trim()) {\n    return end(new Error('Argument path not defined.'));\n  }\n  let command = [];\n\n  if (Node.process.platform === 'win32') {\n    if (/\"/.test(path)) {\n      return end(new Error('Argument path cannot contain double-quotes.'));\n    }\n    command.push(`rmdir /s /q \"${ path }\"`);\n  } else {\n    command.push('/bin/rm');\n    command.push('-rf');\n    command.push(`\"${ EscapeDoubleQuotes(Node.path.normalize(path)) }\"`);\n  }\n  command = command.join(' ');\n  Node.child.exec(command, { encoding: 'utf-8' }, end);\n}\n\nfunction UUID(instance, end) {\n  Node.crypto.randomBytes(256,\n    (error, random) => {\n      if (error) {\n        random = `${ Date.now() }${ Math.random() }`;\n      }\n      const hash = Node.crypto.createHash('SHA256');\n\n      hash.update('sudo-prompt-3');\n      hash.update(instance.options.name);\n      hash.update(instance.command);\n      hash.update(random);\n      const uuid = hash.digest('hex').slice(-32);\n\n      if (!uuid || typeof uuid !== 'string' || uuid.length !== 32) {\n        // This is critical to ensure we don't remove the wrong temp directory.\n        return end(new Error('Expected a valid UUID.'));\n      }\n      end(undefined, uuid);\n    },\n  );\n}\n\nfunction ValidName(string) {\n  // We use 70 characters as a limit to side-step any issues with Unicode\n  // normalization form causing a 255 character string to exceed the fs limit.\n  if (!/^[a-z0-9 ]+$/i.test(string)) {\n    return false;\n  }\n  if (string.trim().length === 0) {\n    return false;\n  }\n\n  return string.length <= 70;\n}\n\nfunction Windows(instance, callback) {\n  const temp = Node.os.tmpdir();\n\n  if (!temp) {\n    return callback(new Error('os.tmpdir() not defined.'));\n  }\n  UUID(instance,\n    (error, uuid) => {\n      if (error) {\n        return callback(error);\n      }\n      instance.uuid = uuid;\n      instance.path = Node.path.join(temp, instance.uuid);\n      if (/\"/.test(instance.path)) {\n        // We expect double quotes to be reserved on Windows.\n        // Even so, we test for this and abort if they are present.\n        return callback(\n          new Error('instance.path cannot contain double-quotes.'),\n        );\n      }\n      instance.pathElevate = Node.path.join(instance.path, 'elevate.vbs');\n      instance.pathExecute = Node.path.join(instance.path, 'execute.bat');\n      instance.pathCommand = Node.path.join(instance.path, 'command.bat');\n      instance.pathStdout = Node.path.join(instance.path, 'stdout');\n      instance.pathStderr = Node.path.join(instance.path, 'stderr');\n      instance.pathStatus = Node.path.join(instance.path, 'status');\n      Node.fs.mkdir(instance.path,\n        (error) => {\n          if (error) {\n            return callback(error);\n          }\n          function end(error, stdout, stderr) {\n            Remove(instance.path,\n              (errorRemove) => {\n                if (error) {\n                  return callback(error);\n                }\n                if (errorRemove) {\n                  return callback(errorRemove);\n                }\n                callback(undefined, stdout, stderr);\n              },\n            );\n          }\n          WindowsWriteExecuteScript(instance,\n            (error) => {\n              if (error) {\n                return end(error);\n              }\n              WindowsWriteCommandScript(instance,\n                (error) => {\n                  if (error) {\n                    return end(error);\n                  }\n                  WindowsElevate(instance,\n                    (error, stdout, stderr) => {\n                      if (error) {\n                        return end(error, stdout, stderr);\n                      }\n                      WindowsWaitForStatus(instance,\n                        (error) => {\n                          if (error) {\n                            return end(error);\n                          }\n                          WindowsResult(instance, end);\n                        },\n                      );\n                    },\n                  );\n                },\n              );\n            },\n          );\n        },\n      );\n    },\n  );\n}\n\nfunction WindowsElevate(instance, end) {\n  // We used to use this for executing elevate.vbs:\n  // var command = 'cscript.exe //NoLogo \"' + instance.pathElevate + '\"';\n  let command = [];\n\n  command.push('powershell.exe');\n  command.push('Start-Process');\n  command.push('-FilePath');\n  // Escape characters for cmd using double quotes:\n  // Escape characters for PowerShell using single quotes:\n  // Escape single quotes for PowerShell using backtick:\n  // See: https://ss64.com/ps/syntax-esc.html\n  command.push(`\"'${ instance.pathExecute.replace(/'/g, \"`'\") }'\"`);\n  command.push('-WindowStyle hidden');\n  command.push('-Verb runAs');\n  command = command.join(' ');\n  const child = Node.child.exec(command, { encoding: 'utf-8' },\n    (error, stdout, stderr) => {\n      // We used to return PERMISSION_DENIED only for error messages containing\n      // the string 'canceled by the user'. However, Windows internationalizes\n      // error messages (issue 96) so now we must assume all errors here are\n      // permission errors. This seems reasonable, given that we already run the\n      // user's command in a subshell.\n      if (error) {\n        return end(new Error(PERMISSION_DENIED), stdout, stderr);\n      }\n      end();\n    },\n  );\n\n  child.stdin.end(); // Otherwise PowerShell waits indefinitely on Windows 7.\n}\n\nfunction WindowsResult(instance, end) {\n  Node.fs.readFile(instance.pathStatus, 'utf-8',\n    (error, code) => {\n      if (error) {\n        return end(error);\n      }\n      Node.fs.readFile(instance.pathStdout, 'utf-8',\n        (error, stdout) => {\n          if (error) {\n            return end(error);\n          }\n          Node.fs.readFile(instance.pathStderr, 'utf-8',\n            (error, stderr) => {\n              if (error) {\n                return end(error);\n              }\n              code = parseInt(code.trim(), 10);\n              if (code === 0) {\n                end(undefined, stdout, stderr);\n              } else {\n                error = new Error(\n                  `Command failed: ${ instance.command }\\r\\n${ stderr }`,\n                );\n                error.code = String(code);\n                end(error, stdout, stderr);\n              }\n            },\n          );\n        },\n      );\n    },\n  );\n}\n\nfunction WindowsWaitForStatus(instance, end) {\n  // VBScript cannot wait for the elevated process to finish so we have to poll.\n  // VBScript cannot return error code if user does not grant permission.\n  // PowerShell can be used to elevate and wait on Windows 10.\n  // PowerShell can be used to elevate on Windows 7 but it cannot wait.\n  // powershell.exe Start-Process cmd.exe -Verb runAs -Wait\n  Node.fs.stat(instance.pathStatus,\n    (error, stats) => {\n      if ((error && error.code === 'ENOENT') || stats.size < 2) {\n        // Retry if file does not exist or is not finished writing.\n        // We expect a file size of 2. That should cover at least \"0\\r\".\n        // We use a 1 second timeout to keep a light footprint for long-lived\n        // sudo-prompt processes.\n        setTimeout(\n          () => {\n            // If administrator has no password and user clicks Yes, then\n            // PowerShell returns no error and execute (and command) never runs.\n            // We check that command output has been redirected to stdout file:\n            Node.fs.stat(instance.pathStdout,\n              (error) => {\n                if (error) {\n                  return end(new Error(PERMISSION_DENIED));\n                }\n                WindowsWaitForStatus(instance, end);\n              },\n            );\n          },\n          1000,\n        );\n      } else if (error) {\n        end(error);\n      } else {\n        end();\n      }\n    },\n  );\n}\n\nfunction WindowsWriteCommandScript(instance, end) {\n  const cwd = Node.process.cwd();\n\n  if (/\"/.test(cwd)) {\n    // We expect double quotes to be reserved on Windows.\n    // Even so, we test for this and abort if they are present.\n    return end(new Error('process.cwd() cannot contain double-quotes.'));\n  }\n  let script = [];\n\n  script.push('@echo off');\n  // Set code page to UTF-8:\n  script.push('chcp 65001>nul');\n  // Preserve current working directory:\n  // We pass /d as an option in case the cwd is on another drive (issue 70).\n  script.push(`cd /d \"${ cwd }\"`);\n  // Export environment variables:\n  for (const key in instance.options.env) {\n    // \"The characters <, >, |, &, ^ are special command shell characters, and\n    // they must be preceded by the escape character (^) or enclosed in\n    // quotation marks. If you use quotation marks to enclose a string that\n    // contains one of the special characters, the quotation marks are set as\n    // part of the environment variable value.\"\n    // In other words, Windows assigns everything that follows the equals sign\n    // to the value of the variable, whereas Unix systems ignore double quotes.\n    const value = instance.options.env[key];\n\n    script.push(`set ${ key }=${ value.replace(/([<>\\\\|&^])/g, '^$1') }`);\n  }\n  script.push(instance.command);\n  script = script.join('\\r\\n');\n  Node.fs.writeFile(instance.pathCommand, script, 'utf-8', end);\n}\n\nfunction WindowsWriteExecuteScript(instance, end) {\n  let script = [];\n\n  script.push('@echo off');\n  script.push(\n    `call \"${ instance.pathCommand }\"` +\n    ` > \"${ instance.pathStdout }\" 2> \"${ instance.pathStderr }\"`,\n  );\n  script.push(`(echo %ERRORLEVEL%) > \"${ instance.pathStatus }\"`);\n  script = script.join('\\r\\n');\n  Node.fs.writeFile(instance.pathExecute, script, 'utf-8', end);\n}\n\nexport const exec = Exec;\n\nconst PERMISSION_DENIED = 'User did not grant permission.';\nconst NO_POLKIT_AGENT = 'No polkit authentication agent found.';\n\n// See issue 66:\nconst MAX_BUFFER = 134217728;\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/package.json",
    "content": "{\n  \"name\": \"sudo-prompt\",\n  \"version\": \"9.2.1\",\n  \"description\": \"Run a command using sudo, prompting the user with an OS dialog if necessary\",\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"files\": [\n    \"LICENSE\",\n    \"README.md\",\n    \"index.d.ts\",\n    \"index.js\",\n    \"package.json\",\n    \"test.js\",\n    \"test-concurrent.js\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/jorangreef/sudo-prompt.git\"\n  },\n  \"keywords\": [\n    \"sudo\",\n    \"os\",\n    \"dialog\",\n    \"prompt\",\n    \"command\",\n    \"exec\",\n    \"user access control\",\n    \"UAC\",\n    \"privileges\",\n    \"administrative\",\n    \"elevate\",\n    \"run as administrator\"\n  ],\n  \"author\": \"Joran Dirk Greef\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/jorangreef/sudo-prompt/issues\"\n  },\n  \"homepage\": \"https://github.com/jorangreef/sudo-prompt#readme\",\n  \"scripts\": {}\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/test-concurrent.js",
    "content": "import { exec } from 'child_process';\n\nimport { exec as sudo } from './';\n\nfunction kill(end) {\n  if (process.platform === 'win32') {\n    return end();\n  }\n  exec('sudo -k', end);\n}\n\nkill(\n  () => {\n    const options = { name: 'Sudo Prompt' };\n\n    let sleep;\n\n    if (process.platform === 'win32') {\n      sleep = 'timeout /t 10\\r\\necho world';\n    } else {\n      sleep = 'sleep 10 && echo world';\n    }\n    sudo(sleep, options,\n      (error, stdout, stderr) => {\n        console.log(error, stdout, stderr);\n      },\n    );\n    sudo('echo hello', options,\n      (error, stdout, stderr) => {\n        console.log(error, stdout, stderr);\n      },\n    );\n  },\n);\n"
  },
  {
    "path": "pkg/rancher-desktop/sudo-prompt/test.js",
    "content": "import assert from 'assert';\nimport { exec } from 'child_process';\nimport { statSync } from 'fs';\n\nimport { exec as sudo } from './';\n\nfunction kill(end) {\n  if (process.platform === 'win32') {\n    return end();\n  }\n  exec('sudo -k', end);\n}\n\nfunction icns() {\n  if (process.platform !== 'darwin') {\n    return undefined;\n  }\n  const path = '/Applications/Electron.app/Contents/Resources/Electron.icns';\n\n  try {\n    statSync(path);\n\n    return path;\n  } catch (error) {\n  }\n\n  return undefined;\n}\n\nkill(\n  () => {\n    const options = {\n      env:  { SUDO_PROMPT_TEST_ENV: 'hello world' },\n      icns: icns(),\n      name: 'Electron',\n    };\n\n    let command;\n    let expected;\n\n    if (process.platform === 'win32') {\n      command = 'echo %SUDO_PROMPT_TEST_ENV%';\n      expected = 'hello world\\r\\n';\n    } else {\n      // We use double quotes to tell echo to preserve internal space:\n      command = 'echo \"$SUDO_PROMPT_TEST_ENV\"';\n      expected = 'hello world\\n';\n    }\n    console.log(\n      `sudo.exec(${\n        JSON.stringify(command) }, ${\n        JSON.stringify(options)\n      })`,\n    );\n    sudo(command, options,\n      (error, stdout, stderr) => {\n        console.log('error:', error);\n        console.log(`stdout: ${ JSON.stringify(stdout) }`);\n        console.log(`stderr: ${ JSON.stringify(stderr) }`);\n        assert(error === undefined || typeof error === 'object');\n        assert(stdout === undefined || typeof stdout === 'string');\n        assert(stderr === undefined || typeof stderr === 'string');\n        kill(\n          () => {\n            if (error) {\n              throw error;\n            }\n            if (stdout !== expected) {\n              throw new Error(`stdout != ${ JSON.stringify(expected) }`);\n            }\n            if (stderr !== '') {\n              throw new Error('stderr != \"\"');\n            }\n            console.log('OK');\n          },\n        );\n      },\n    );\n  },\n);\n"
  },
  {
    "path": "pkg/rancher-desktop/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2018\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"lib\": [\n      \"ESNext\",\n      \"ESNext.AsyncIterable\",\n      \"DOM\",\n      \"DOM.Iterable\",\n    ],\n    \"esModuleInterop\": true,\n    \"allowJs\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"noEmit\": false,\n    \"experimentalDecorators\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@pkg/*\": [\n        \"./*\"\n      ]\n    },\n    \"typeRoots\": [\n      \"../../node_modules\",\n      \"../../node_modules/@types\",\n    ],\n    \"types\": [\n      \"@types/node\",\n      \"@types/jest\"\n    ]\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\",\n    \"babel.config.js\"\n  ],\n  \"include\": [\n    \"**/*\",\n    \"**/*.ts\",\n    \"**/*.d.ts\",\n    \"**/*.tsx\",\n    \"**/*.vue\"\n  ]\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/types/components/labeledSelect.ts",
    "content": "export const LABEL_SELECT_KINDS = {\n  GROUP:   'group',\n  DIVIDER: 'divider',\n  NONE:    'none',\n};\n\nexport const LABEL_SELECT_NOT_OPTION_KINDS = [\n  LABEL_SELECT_KINDS.GROUP,\n  LABEL_SELECT_KINDS.DIVIDER,\n];\n\n/**\n * Options used When LabelSelect requests a new page\n */\nexport interface LabelSelectPaginateFnOptions<T = any> {\n  /**\n   * Current page\n   */\n  pageContent: T[],\n  /**\n   * page number to fetch\n   */\n  page:        number,\n  /**\n   * number of items in the page to fetch\n   */\n  pageSize:    number,\n  /**\n   * filter pagination filter. this is just a text string associated with user entered text\n   */\n  filter:      string,\n  /**\n   * true if the result should only contain the fetched page, false if the result should be added to the pageContent\n   */\n  resetPage:   boolean,\n}\n\n/**\n * Response that LabelSelect needs when it's requested a new page\n */\nexport interface LabelSelectPaginateFnResponse<T = any> {\n  page:  T[],\n  pages: number,\n  total: number\n}\n\n/**\n * Function called when LabelSelect needs a new page\n */\nexport type LabelSelectPaginateFn<T = any> = (opts: LabelSelectPaginateFnOptions<T>) => Promise<LabelSelectPaginateFnResponse<T>>;\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/assets.d.ts",
    "content": "declare module '@pkg/assets/*.yaml' {\n  const content: any;\n  export default content;\n}\n\ndeclare module '@pkg/assets/scripts/*' {\n  const content: string;\n  export default content;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/electron-ipc.d.ts",
    "content": "/**\n * Custom declarations for Electron IPC topics.\n */\n\nimport Electron from 'electron';\nimport semver from 'semver';\n\nimport type { ServiceEntry } from '@pkg/backend/k8s';\nimport { SnapshotDialog, SnapshotEvent } from '@pkg/main/snapshots/types';\nimport type { Direction, RecursivePartial } from '@pkg/utils/typeUtils';\n/**\n * IpcMainEvents describes events the renderer can send to the main process,\n * i.e. ipcRenderer.send() -> ipcMain.on().\n */\nexport interface IpcMainEvents {\n  'backend-state-check':   () => string;\n  'k8s-restart':           () => void;\n  'settings-read':         () => void;\n  'k8s-versions':          () => void;\n  'k8s-reset':             (mode: 'fast' | 'wipe') => void;\n  'k8s-state':             () => void;\n  'k8s-current-engine':    () => void;\n  'k8s-current-port':      () => void;\n  'k8s-progress':          () => void;\n  'k8s-integrations':      () => void;\n  'k8s-integration-set':   (name: string, newState: boolean) => void;\n  'factory-reset':         (keepSystemImages: boolean) => void;\n  'get-app-version':       () => void;\n  'update-network-status': (status: boolean) => void;\n\n  // #region main/update\n  'update-state': () => void;\n  // Quit and apply the update.\n  'update-apply': () => void;\n  // #endregion\n\n  // #region main/containerEvents\n  'do-containers-exec':        (command: string, containerId: string[]) => void;\n  'containers-process-output': (data: string, isStdErr: boolean) => void;\n  // #endregion\n\n  // #region main/containerExec\n  'container-exec/start':  (containerId: string, namespace?: string) => void;\n  'container-exec/input':  (containerId: string, data: string) => void;\n  'container-exec/kill':   (containerId: string) => void;\n  'container-exec/detach': (containerId: string) => void;\n  // #endregion\n\n  // #region main/imageEvents\n  'confirm-do-image-deletion': (imageName: string, imageID: string) => void;\n  'do-image-build':            (taggedImageName: string) => void;\n  'do-image-pull':             (imageName: string) => void;\n  'do-image-scan':             (imageName: string, namespace: string) => void;\n  'do-image-push':             (imageName: string, imageID: string, tag: string) => void;\n  'do-image-deletion':         (imageName: string, imageID: string) => void;\n  'do-image-deletion-batch':   (images: string[]) => void;\n  'images-namespaces-read':    () => void;\n  // #endregion\n\n  // #region dialog\n  'dialog/load':    () => void;\n  'dialog/ready':   () => void;\n  'dialog/mounted': () => void;\n  /** For message box only */\n  'dialog/error':   (args: Record<string, string>) => void;\n  'dialog/close':   (...args: any[]) => void;\n  // #endregion\n\n  // #region sudo-prompt\n  'sudo-prompt/closed': (suppress: boolean) => void;\n  // #endregion\n\n  // #region kubernetes-errors\n  'kubernetes-errors/ready': () => void;\n  // #endregion\n\n  // #region Preferences\n  'preferences-open':       () => void;\n  'preferences-close':      () => void;\n  'preferences-set-dirty':  (isDirty: boolean) => void;\n  'get-debugging-statuses': () => void;\n  // #endregion\n\n  'show-logs': () => void;\n\n  'dashboard-open':  () => void;\n  'dashboard-close': () => void;\n\n  'diagnostics/run': () => void;\n\n  /** Only for the preferences window */\n  'preferences/load': () => void;\n\n  'help/preferences/open-url': () => void;\n\n  // #region Extensions\n  'extensions/open':            (id: string, path: string) => void;\n  'extensions/close':           () => void;\n  'extensions/open-external':   (url: string) => void;\n  'extensions/spawn/kill':      (execId: string) => void;\n  /** Execute the given command, streaming results back via events. */\n  'extensions/spawn/streaming': (\n    options: import('@pkg/main/extensions/types').SpawnOptions\n  ) => void;\n  /** Show a notification */\n  'extensions/ui/toast': (\n    level: 'success' | 'warning' | 'error',\n    message: string\n  ) => void;\n  'ok:extensions/getContentArea': (payload: { top: number, right: number, bottom: number, left: number }) => void;\n  // #endregion\n\n  // #region Snapshots\n  snapshot:          (event: SnapshotEvent | null) => void;\n  'snapshot/cancel': () => void;\n  // #endregion\n}\n\n/**\n * IpcMainInvokeEvents describes handlers describes RPC calls the renderer can\n * invoke on the main process, i.e. ipcRenderer.invoke() -> ipcMain.handle()\n */\nexport interface IpcMainInvokeEvents {\n  'get-locked-fields':         () => import('@pkg/config/settings').LockedSettingsType;\n  'settings-write':            (arg: RecursivePartial<import('@pkg/config/settings').Settings>) => void;\n  'transient-settings-fetch':  () => import('@pkg/config/transientSettings').TransientSettings;\n  'transient-settings-update': (arg: RecursivePartial<import('@pkg/config/transientSettings').TransientSettings>) => void;\n  'service-fetch':             (namespace?: string) => import('@pkg/backend/k8s').ServiceEntry[];\n  'service-forward':           (service: ServiceEntry, state: boolean) => void;\n  'get-app-version':           () => string;\n  'show-message-box':          (options: Electron.MessageBoxOptions) => Electron.MessageBoxReturnValue;\n  'show-message-box-rd':       (options: Electron.MessageBoxOptions, modal?: boolean) => any;\n  'api-get-credentials':       () => { user: string, password: string, port: number };\n  'k8s-progress':              () => Readonly<{ current: number, max: number, description?: string, transitionTime?: Date }>;\n\n  // #region main/imageEvents\n  'images-mounted':     (mounted: boolean) => (import('@pkg/backend/images/imageProcessor').ImageType)[];\n  'images-check-state': () => boolean;\n  // #endregion\n\n  // #region extensions\n  /** Execute the given command and return the results. */\n  'extensions/spawn/blocking': (options: import('@pkg/main/extensions/types').SpawnOptions) => import('@pkg/main/extensions/types').SpawnResult;\n  'extensions/ui/show-open':   (options: import('electron').OpenDialogOptions) => import('electron').OpenDialogReturnValue;\n  /* Fetch data from the backend, or arbitrary host ignoring CORS. */\n  'extensions/vm/http-fetch':  (config: import('@docker/extension-api-client-types').v1.RequestConfig) => import('@docker/extension-api-client-types').v1.ServiceError;\n  // #endregion\n\n  // #region Versions\n  'versions/macOs': () => semver.SemVer;\n  // #endregion\n\n  // #region Host\n  'host/isArm': () => boolean;\n  // #endregion\n\n  // #region Snapshots\n  'show-snapshots-confirm-dialog':  (options: { window: Partial<Electron.MessageBoxOptions>, format: SnapshotDialog }) => any;\n  'show-snapshots-blocking-dialog': (options: { window: Partial<Electron.MessageBoxOptions>, format: SnapshotDialog }) => any;\n  // #endregion\n}\n\n/**\n * IpcRendererEvents describes events that the main process may send to the renderer\n * process, i.e. webContents.send() -> ipcRenderer.on().\n */\nexport interface IpcRendererEvents {\n  'backend-locked':   (action?: string) => void;\n  'backend-unlocked': () => void;\n  'settings-update': (\n    settings: import('@pkg/config/settings').Settings\n  ) => void;\n  'settings-read':    (settings: import('@pkg/config/settings').Settings) => void;\n  'get-app-version':  (version: string) => void;\n  'update-state':     (state: import('@pkg/main/update').UpdateState) => void;\n  'always-debugging': (status: boolean) => void;\n  'is-debugging':     (status: boolean) => void;\n  'k8s-progress': (\n    progress: Readonly<{\n      current:         number;\n      max:             number;\n      description?:    string;\n      transitionTime?: Date;\n    }>\n  ) => void;\n  'k8s-check-state':    (state: import('@pkg/backend/k8s').State) => void;\n  'k8s-current-engine': (\n    engine: import('@pkg/config/settings').ContainerEngine\n  ) => void;\n  'k8s-current-port': (port: number) => void;\n  'k8s-versions': (\n    versions: import('@pkg/utils/kubeVersions').VersionEntry[],\n    cachedOnly: boolean\n  ) => void;\n  'k8s-integrations':          (integrations: Record<string, boolean | string>) => void;\n  'service-changed':           (services: ServiceEntry[]) => void;\n  'service-error':             (service: ServiceEntry, errorMessage: string) => void;\n  'kubernetes-errors-details': (\n    titlePart: string,\n    mainMessage: string,\n    failureDetails: import('@pkg/backend/k8s').FailureDetails\n  ) => void;\n  'update-network-status': (status: boolean) => void;\n  'diagnostics/update':    () => void;\n\n  // #region Images\n  'images-process-cancelled': () => void;\n  'images-process-ended':     (exitCode: number) => void;\n  'images-process-output':    (data: string, isStdErr: boolean) => void;\n  'ok:images-process-output': (data: string) => void;\n  'images-changed': (\n    images: (import('@pkg/backend/images/imageProcessor').ImageType)[]\n  ) => void;\n  'images-check-state':       (state: boolean) => void;\n  'images-namespaces':        (namespaces: string[]) => void;\n  'container-process-output': (data: string, isStdErr: boolean) => void;\n  // #endregion\n\n  // #region main/containerExec\n  'container-exec/output':      (execId: string, data: string) => void;\n  'container-exec/exit':        (execId: string, code: number) => void;\n  'container-exec/ready':       (execId: string, history: string) => void;\n  'container-exec/unsupported': () => void;\n  // #endregion\n\n  // #region dialog\n  'dialog/mounted':  () => void;\n  'dialog/populate': (...args: any) => void;\n  'dialog/size':     (size: { width: number; height: number }) => void;\n  'dialog/options':  (...args: any) => void;\n  'dialog/close':    (...args: any) => void;\n  'dialog/error':    (args: any) => void;\n  'dialog/info':     (args: Record<string, string>) => void;\n  'dashboard-open':  () => void;\n  // #endregion\n\n  // #region tab navigation\n  route: (route: {\n    name?:      string;\n    path?:      string;\n    direction?: Direction;\n  }) => void;\n  // #endregion\n\n  // #region extensions\n  // The list of installed extensions may have changed.\n  'extensions/changed':        () => void;\n  'extensions/getContentArea': () => void;\n  'extensions/open':           (id: string, path: string) => void;\n  'err:extensions/open':       () => void;\n  'extensions/close':          () => void;\n  'extensions/spawn/close':    (id: string, code: number) => void;\n  'extensions/spawn/error':    (id: string, error: Error | NodeJS.Signals) => void;\n  'extensions/spawn/output': (\n    id: string,\n    data: { stdout: string } | { stderr: string }\n  ) => void;\n  'ok:extensions/uninstall': (id: string) => void;\n  // #endregion\n\n  // #region window\n  'window/blur': (state: boolean) => void;\n  // #endregion\n\n  // #region preferences\n  'preferences/changed': () => void;\n  // #endregion\n\n  // #region Versions\n  'versions/macOs': (macOsVersion: semver.SemVer) => void;\n  // #endregion\n\n  // #region Host\n  'host/isArm': (isArm: boolean) => void;\n  // #endregion\n\n  // #region Snapshots\n  snapshot:          (event: SnapshotEvent | null) => void;\n  'snapshot/cancel': () => void;\n  // #endregion\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/linux-ca.d.ts",
    "content": "declare module 'linux-ca' {\n  import * as forge from 'node-forge';\n\n  export function getAllCerts(readSync?: boolean): Promise<string[][]>;\n  export function getFilteredCerts(filterAttribute: string, filterMethod?: (cert: forge.pki.Certificate, attribute: string) => boolean): Promise<string[]>;\n  export function pemToCert(pem: string): forge.pki.Certificate;\n  export function certToPem(cert: forge.pki.Certificate): string;\n  export function defaultFilter(cert: forge.pki.Certificate, subject: string): boolean;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/rdx.d.ts",
    "content": "import type { RDXClient } from '@pkg/preload/extensions';\n\ndeclare global {\n  interface Window {\n    ddClient: RDXClient;\n  }\n}\n\ndeclare module '@docker/extension-api-client-types/dist/v1' {\n  interface ExecOptions {\n    stream?: never; // Ensure that if `stream` is set it takes the other overload.\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/shell.d.ts",
    "content": "declare module '@shell/core/types' {\n  class IPlugin {\n    metadata:   any;\n    addProduct: any;\n    addRoutes:  any;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/shims-vue.d.ts",
    "content": "declare module '*.vue' {\n  import type { DefineComponent } from 'vue';\n  const component: DefineComponent<object, object, any>;\n  export default component;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/store.d.ts",
    "content": "import { Store } from 'vuex/types';\n\nimport type { Modules } from '@pkg/entry/store';\n\ntype Actions<\n  module extends string,\n  actions extends Record<string, (context: any, args: any) => any>,\n> = {\n  [action in keyof actions as `${ module }/${ action & string }`]:\n  (arg: Parameters<actions[action]>[1]) => ReturnType<actions[action]>;\n};\n\ntype Keys<T> = T extends Record<infer K, any> ? K : never;\ntype Values<T> = T extends Record<any, infer V> ? V : never;\ntype Intersect<U extends object> = {\n  [K in Keys<U>]: U extends Record<K, infer T> ? T : never;\n};\n\ntype storeActions = Intersect<Values<{\n  [module in keyof Modules]:\n  Modules[module] extends { actions: any } ?\n    Actions<module, Modules[module]['actions']> : never;\n}>>;\n\ntype Mutations<\n  module extends string,\n  mutations extends Record<string, (state: any, payload?: any) => any>,\n> = {\n  [mutation in keyof mutations as `${ module }/${ mutation & string }`]:\n  (payload: Parameters<mutations[mutation]>[1]) => ReturnType<mutations[mutation]>;\n};\n\ntype storeMutations = Intersect<Values<{\n  [module in keyof Modules]:\n  Modules[module] extends { mutations: any } ?\n    Mutations<module, Modules[module]['mutations']> : never;\n}>>;\n\ndeclare module 'vuex/types' {\n  export interface Dispatch {\n    <action extends keyof storeActions>\n    (\n      type: action,\n      payload: Parameters<storeActions[action]>[0],\n      options?: DispatchOptions\n    ): Promise<Awaited<ReturnType<storeActions[action]>>>;\n\n    <action extends keyof storeActions>\n    (\n      type: action,\n    ): Promise<Awaited<ReturnType<storeActions[action]>>>;\n  }\n\n  export interface Commit {\n    <mutation extends keyof storeMutations>\n    (\n      type: mutation,\n      payload?: Parameters<storeMutations[mutation]>[0],\n      options?: CommitOptions\n    ): void;\n    <\n      mutation extends keyof storeMutations,\n      P extends { type: mutation } & Parameters<storeMutations[mutation]>[0],\n    >(payloadWithType: P, options?: CommitOptions): void;\n  }\n\n  export function useStore(): Store<{\n    [key in keyof Modules]: ReturnType<Modules[key]['state']>;\n  }>;\n}\n\ndeclare module 'vue' {\n  // provide typings for `this.$store`\n  interface ComponentCustomProperties {\n    $store: Store<{\n      [key in keyof Modules]: ReturnType<Modules[key]['state']>;\n    }>;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/unix.interface.ts",
    "content": "export interface UnixError {\n  stdout:  string;\n  stderr:  string;\n  code:    string;\n  message: string;\n}\n\nexport const isUnixError = (val: any): val is UnixError => {\n  return 'stdout' in val &&\n    'stderr' in val &&\n    'code' in val &&\n    'message' in val;\n};\n\nexport interface NodeError {\n  syscall: string;\n  path:    string;\n  code:    string;\n  message: string;\n  errno:   number;\n}\n\nexport const isNodeError = (val: any): val is NodeError => {\n  return 'syscall' in val &&\n    'path' in val &&\n    'code' in val &&\n    'message' in val;\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/typings/vue-i18n.ts",
    "content": "// Ensure this augments the package, instead of overwriting it.\n// See https://vuejs.org/guide/typescript/options-api.html#type-augmentation-placement\nexport {};\n\ndeclare module 'vue' {\n  interface ComponentCustomProperties {\n    /**\n       * Lookup a given string with the given arguments\n       * @param raw if set, do not do HTML escaping.\n       */\n    t: (key: string, args?: Record<string, any>, raw?: boolean) => string,\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/DownloadProgressListener.ts",
    "content": "import stream from 'stream';\n\n/**\n * DownloadProgressListener observes a stream pipe to monitor progress.\n */\nexport default class DownloadProgressListener extends stream.Transform {\n  protected status: { current: number };\n\n  /**\n   * Construct a new DownloadProgressListener, which will update the passed-in\n   * object on progress.  No events will be emitted to avoid redrawing the UI\n   * too often; a timer/interval should be used instead.\n   * @param status A object that will be modified when download progress occurs.\n   * @param options Options to pass to {stream.Transform}.\n   */\n  constructor(status: { current: number }, options: stream.TransformOptions = {}) {\n    super(options);\n    this.status = status;\n  }\n\n  _transform(chunk: any, encoding: string, callback: stream.TransformCallback): void {\n    if (encoding === 'buffer') {\n      this.status.current += (chunk as Buffer).length;\n    } else {\n      this.status.current += (chunk as string).length;\n    }\n    callback(null, chunk);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/childProcess.spec.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport * as childProcess from '../childProcess';\n\nimport { Log } from '@pkg/utils/logging';\n\ndescribe(childProcess.spawnFile, () => {\n  function makeArg(fn: () => void) {\n    return `--eval=(${ fn.toString() })();`;\n  }\n\n  test('returns output', async() => {\n    const args = ['--version'];\n    const result = await childProcess.spawnFile(process.execPath, args, { stdio: ['ignore', 'pipe', 'ignore'] });\n\n    expect(result.stdout.trim()).toEqual(process.version);\n    expect(result).not.toHaveProperty('stderr');\n  });\n\n  test('returns output under stress', async() => {\n    const args = ['--version'];\n\n    await Promise.all(Array.from(Array(1000).keys()).map(async() => {\n      const result = await childProcess.spawnFile(process.execPath, args, { stdio: ['ignore', 'pipe', 'ignore'] });\n\n      expect(result.stdout.trim()).toEqual(process.version);\n      expect(result).not.toHaveProperty('stderr');\n    }));\n  }, 180_000);\n\n  test('returns error', async() => {\n    const args = [makeArg(() => console.error('hello'))];\n    const result = await childProcess.spawnFile(process.execPath, args, { stdio: 'pipe' });\n\n    expect(result.stdout).toEqual('');\n    expect(result.stderr.trim()).toEqual('hello');\n  });\n\n  test('throws on failure', async() => {\n    const args = [makeArg(() => {\n      console.log('stdout');\n      console.error('stderr');\n      process.exit(1);\n    })];\n    const result = childProcess.spawnFile(process.execPath, args, { stdio: 'pipe' });\n\n    await expect(result).rejects.toThrow('exited with code 1');\n    await expect(result).rejects.toHaveProperty('stdout', 'stdout\\n');\n    await expect(result).rejects.toHaveProperty('stderr', 'stderr\\n');\n  });\n\n  test('converts encodings on stdout', async() => {\n    const args = [makeArg(() => console.log(Buffer.from('hello', 'utf16le').toString()))];\n    const result = await childProcess.spawnFile(process.execPath, args, { stdio: 'pipe', encoding: 'utf16le' });\n\n    expect(result.stdout.trim()).toEqual('hello');\n  });\n\n  test('converts encodings on stderr', async() => {\n    const args = [makeArg(() => console.error(Buffer.from('hello', 'utf16le').toString()))];\n    const result = await childProcess.spawnFile(process.execPath, args, { stdio: 'pipe', encoding: 'utf16le' });\n\n    expect(result.stderr.trim()).toEqual('hello');\n  });\n\n  test('output to log', async() => {\n    const workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-test-childprocess-'));\n    let log: Log | undefined;\n\n    try {\n      log = new Log('childprocess-test', workdir);\n      const args = [makeArg(() => {\n        console.log('stdout'); console.error('stderr');\n      })];\n      const result = await childProcess.spawnFile(process.execPath, args, { stdio: log });\n\n      expect(result).not.toHaveProperty('stdout');\n      expect(result).not.toHaveProperty('stderr');\n\n      const output = await fs.promises.readFile(log.path, 'utf-8');\n\n      expect(output).toContain('stdout');\n      expect(output).toContain('stderr');\n    } finally {\n      log?.stream?.close();\n      await fs.promises.rm(workdir, { recursive: true, maxRetries: 3 });\n    }\n  });\n\n  test('prints stderr', async() => {\n    const script = `\n      console.log('Output on std!!out');\n      console.error('Output on std!!err');\n      process.exitCode = 42;\n    `;\n\n    try {\n      await childProcess.spawnFile(process.execPath, ['-e', script], { stdio: 'pipe' });\n    } catch (ex: any) {\n      expect(ex).toBeInstanceOf(Error);\n      // Check that the Error toString() is used\n      expect(ex.toString()).toContain(`Error: ${ process.execPath }`);\n      // Check that we have the exit code logged\n      expect(ex.toString()).toContain('exited with code 42');\n      // Check that we have stdout in the output\n      expect(ex.toString()).toContain('std!!out');\n      // CHeck that we have stderr in the output\n      expect(ex.toString()).toContain('std!!err');\n    }\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/dockerDirManager.spec.ts",
    "content": "import fs from 'fs';\nimport net from 'net';\nimport os from 'os';\nimport path from 'path';\n\nimport { jest } from '@jest/globals';\n\nimport mockModules from '../testUtils/mockModules';\n\nimport * as childProcess from '@pkg/utils/childProcess';\nimport paths from '@pkg/utils/paths';\n\nconst spawnFile = childProcess.spawnFile;\nconst modules = mockModules({\n  '@pkg/utils/childProcess': {\n    ...childProcess,\n    spawnFile: jest.fn<(command: string, args: string[], options: any) => Promise<unknown>>(),\n  },\n  '@pkg/utils/logging': {\n    background: {\n      debug: jest.fn(),\n      error: jest.fn(),\n      /** Mocked console.log() to check messages. */\n      log:   jest.fn(),\n    },\n  },\n  '@pkg/utils/paths': {\n    ...paths,\n    resources: paths.resources,\n  },\n});\n\nconst itUnix = os.platform() === 'win32' ? it.skip : it;\nconst itDarwin = os.platform() === 'darwin' ? it : it.skip;\nconst itLinux = os.platform() === 'linux' ? it : it.skip;\nconst describeUnix = os.platform() === 'win32' ? describe.skip : describe;\nconst { DockerDirManager } = await import('@pkg/utils/dockerDirManager');\n\ndescribe('DockerDirManager', () => {\n  /** The instance of LimaBackend under test. */\n  let subj: InstanceType<typeof DockerDirManager>;\n  /** A directory we can use for scratch files during the test. */\n  let workdir: string;\n\n  beforeEach(async() => {\n    modules['@pkg/utils/childProcess'].spawnFile.mockImplementation(spawnFile);\n    await expect((async() => {\n      workdir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rancher-desktop-lima-test-'));\n      subj = new DockerDirManager(path.join(workdir, '.docker'));\n    })()).resolves.toBeUndefined();\n  });\n  afterEach(async() => {\n    modules['@pkg/utils/logging'].background.log.mockReset();\n    await fs.promises.rm(workdir, { recursive: true });\n  });\n\n  describe('getDesiredDockerContext', () => {\n    it('should clear context when we own the default socket', async() => {\n      await expect(subj['getDesiredDockerContext'](true, undefined)).resolves.toBeUndefined();\n      await expect(subj['getDesiredDockerContext'](true, 'pikachu')).resolves.toBeUndefined();\n    });\n\n    itUnix('should return rancher-desktop when no config and no control over socket', async() => {\n      await expect(subj['getDesiredDockerContext'](false, undefined)).resolves.toEqual('rancher-desktop');\n    });\n\n    itUnix('should do nothing if context is already set to rancher-desktop', async() => {\n      await expect(subj['getDesiredDockerContext'](false, 'rancher-desktop')).resolves.toEqual('rancher-desktop');\n    });\n\n    itUnix('should return current context when that context is tcp', async() => {\n      const getCurrentDockerSocketMock = jest.spyOn(subj as any, 'getCurrentDockerSocket')\n        .mockResolvedValue('some-url');\n\n      try {\n        const currentContext = 'pikachu';\n\n        await expect(subj['getDesiredDockerContext'](false, currentContext)).resolves.toEqual(currentContext);\n      } finally {\n        getCurrentDockerSocketMock.mockRestore();\n      }\n    });\n\n    itUnix('should return current context when that context is unix socket', async() => {\n      const unixSocketPath = path.join(workdir, 'test-socket');\n      const unixSocketPathWithUnix = `unix://${ unixSocketPath }`;\n      const unixSocketServer = net.createServer();\n\n      unixSocketServer.listen(unixSocketPath);\n      const getCurrentDockerSocketMock = jest.spyOn(subj as any, 'getCurrentDockerSocket')\n        .mockResolvedValue(unixSocketPathWithUnix);\n\n      try {\n        const currentContext = 'pikachu';\n\n        await expect(subj['getDesiredDockerContext'](false, currentContext)).resolves.toEqual(currentContext);\n      } finally {\n        getCurrentDockerSocketMock.mockRestore();\n        await new Promise((resolve) => {\n          unixSocketServer.close(() => resolve(null));\n        });\n      }\n    });\n  });\n\n  describeUnix('ensureDockerContextFile', () => {\n    /** Path to the docker context metadata file (in workdir). */\n    let metaPath: string;\n    /** Path to the docker socket Rancher Desktop is providing. */\n    let sockPath: string;\n\n    beforeEach(() => {\n      metaPath = path.join(workdir, '.docker', 'contexts', 'meta',\n        'b547d66a5de60e5f0843aba28283a8875c2ad72e99ba076060ef9ec7c09917c8',\n        'meta.json');\n      sockPath = path.join(workdir, 'docker.sock');\n    });\n\n    it('should create additional docker context if none exists', async() => {\n      await expect(subj['ensureDockerContextFile'](sockPath)).resolves.toBeUndefined();\n      const result = JSON.parse(await fs.promises.readFile(metaPath, 'utf-8'));\n\n      expect(result).toEqual({\n        Endpoints: {\n          docker: {\n            Host:          `unix://${ sockPath }`,\n            SkipTLSVerify: false,\n          },\n        },\n        Metadata: { Description: 'Rancher Desktop moby context' },\n        Name:     'rancher-desktop',\n      });\n      expect(modules['@pkg/utils/logging'].background.log).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('ensureDockerContextConfigured', () => {\n    /** Path to the docker config file (in workdir). */\n    let configPath: string;\n    /** Path to a secondary docker context metadata file, for existing contexts. */\n    let altMetaPath: string;\n    /** Path to the docker socket Rancher Desktop is providing. */\n    let sockPath: string;\n    /** Path to a secondary docker socket, for existing contexts. */\n    let altSockPath: string;\n\n    beforeEach(() => {\n      configPath = path.join(workdir, '.docker', 'config.json');\n      altMetaPath = path.join(workdir, '.docker', 'contexts', 'meta',\n        '43999461d22f67840fcd9b8824293eaa4f18146e57b2c651bcd925e3b3e4e429',\n        'meta.json');\n      sockPath = path.join(workdir, 'docker.sock');\n      altSockPath = path.join(workdir, 'pikachu.sock');\n    });\n\n    itUnix('should not touch working unix socket', async() => {\n      const server = net.createServer();\n\n      try {\n        await new Promise<void>(resolve => server.listen(altSockPath, resolve));\n        await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n        await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'pikachu' }));\n        await fs.promises.mkdir(path.dirname(altMetaPath), { recursive: true });\n        await fs.promises.writeFile(altMetaPath, JSON.stringify({\n          Name:      'pikachu',\n          Endpoints: { docker: { Host: `unix://${ altSockPath }` } },\n        }));\n        await expect(subj.ensureDockerContextConfigured(false, sockPath)).resolves.toBeUndefined();\n\n        expect(JSON.parse(await fs.promises.readFile(configPath, 'utf-8'))).toHaveProperty('currentContext', 'pikachu');\n      } finally {\n        server.close();\n      }\n    });\n\n    itUnix('should change context when current points to nonexistent socket', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'pikachu' }));\n      await fs.promises.mkdir(path.dirname(altMetaPath), { recursive: true });\n      await fs.promises.writeFile(altMetaPath, JSON.stringify({\n        Name:      'pikachu',\n        Endpoints: { docker: { Host: `unix://${ altSockPath }` } },\n      }));\n\n      await expect(subj.ensureDockerContextConfigured(false, sockPath)).resolves.toBeUndefined();\n\n      expect(modules['@pkg/utils/logging'].background.log.mock.calls).toContainEqual([\n        expect.stringMatching(`Could not read existing docker socket.*${ workdir }.*pikachu.*ENOENT`),\n      ]);\n\n      expect(JSON.parse(await fs.promises.readFile(configPath, 'utf-8'))).toHaveProperty('currentContext', 'rancher-desktop');\n    });\n\n    itUnix('should change context when existing is invalid', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'pikachu' }));\n      await fs.promises.mkdir(path.dirname(altMetaPath), { recursive: true });\n      await fs.promises.writeFile(altMetaPath, JSON.stringify({\n        Name:      'pikachu',\n        Endpoints: { docker: { Host: `unix://${ altSockPath }` } },\n      }));\n      await fs.promises.writeFile(altSockPath, '');\n\n      await expect(subj.ensureDockerContextConfigured(false, sockPath)).resolves.toBeUndefined();\n\n      expect(modules['@pkg/utils/logging'].background.log.mock.calls).toContainEqual([\n        expect.stringMatching(`Invalid existing context.*pikachu.*${ workdir }`),\n      ]);\n\n      expect(JSON.parse(await fs.promises.readFile(configPath, 'utf-8'))).toHaveProperty('currentContext', 'rancher-desktop');\n    });\n\n    itUnix('should not change context if existing is tcp socket', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'pikachu' }));\n      await fs.promises.mkdir(path.dirname(altMetaPath), { recursive: true });\n      await fs.promises.writeFile(altMetaPath, JSON.stringify({\n        Name:      'pikachu',\n        Endpoints: { docker: { Host: `tcp://server.test:1234` } },\n      }));\n      await expect(subj.ensureDockerContextConfigured(false, sockPath)).resolves.toBeUndefined();\n\n      expect(JSON.parse(await fs.promises.readFile(configPath, 'utf-8'))).toHaveProperty('currentContext', 'pikachu');\n    });\n\n    itUnix('should allow for existing invalid context configuration', async() => {\n      const metaDir = path.join(workdir, '.docker', 'contexts', 'meta');\n      const statMock = jest.spyOn(fs.promises, 'stat')\n        .mockImplementation((pathLike: fs.PathLike, opts?: fs.StatOptions ) => {\n          expect(pathLike).toEqual('/var/run/docker.sock');\n\n          throw new Error(`ENOENT: no such file or directory, stat ${ pathLike }`);\n        });\n\n      try {\n        await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n        await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'pikachu' }));\n        await fs.promises.mkdir(path.join(metaDir, 'invalid-context', 'meta.json'), { recursive: true });\n        await fs.promises.writeFile(path.join(metaDir, 'invalid-context-two'), '');\n        await expect(subj.ensureDockerContextConfigured(false, sockPath)).resolves.toBeUndefined();\n\n        expect(modules['@pkg/utils/logging'].background.log.mock.calls).toContainEqual([\n          expect.stringMatching(`Failed to read context.*invalid-context.*EISDIR`),\n        ]);\n        expect(modules['@pkg/utils/logging'].background.log.mock.calls).toContainEqual([\n          expect.stringMatching(`Failed to read context.*invalid-context-two.*ENOTDIR`),\n        ]);\n        expect(modules['@pkg/utils/logging'].background.log.mock.calls).toContainEqual([\n          expect.stringMatching(`Could not read existing docker socket.*ENOENT`),\n        ]);\n      } finally {\n        statMock.mockRestore();\n      }\n    });\n  });\n\n  describe('ensureCredHelperConfigured', () => {\n    /** Path to the docker config file (in workdir). */\n    let configPath: string;\n\n    beforeEach(() => {\n      configPath = path.join(workdir, '.docker', 'config.json');\n    });\n\n    it('should throw errors reading config.json', async() => {\n      await fs.promises.mkdir(configPath, { recursive: true });\n      await expect(subj.ensureCredHelperConfigured()).rejects.toThrow('EISDIR');\n      expect(modules['@pkg/utils/logging'].background.log).not.toHaveBeenCalled();\n    });\n\n    it('should set credsStore to default when undefined', async() => {\n      await subj.ensureCredHelperConfigured();\n      const rawConfig = await fs.promises.readFile(configPath, 'utf-8');\n      const newConfig = JSON.parse(rawConfig);\n\n      expect(newConfig.credsStore).toEqual(await subj['getCredsStoreFor'](undefined));\n    });\n\n    it('should set credsStore to platform default when it is \"desktop\"', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ credsStore: 'desktop' }));\n      await subj.ensureCredHelperConfigured();\n      const rawConfig = await fs.promises.readFile(configPath, 'utf-8');\n      const newConfig = JSON.parse(rawConfig);\n\n      expect(newConfig.credsStore).toEqual(await subj['getCredsStoreFor']('desktop'));\n    });\n\n    it('should not change any irrelevant keys in config.json', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ otherKey: 'otherValue' }));\n      await subj.ensureCredHelperConfigured();\n      const newConfig = JSON.parse(await fs.promises.readFile(configPath, 'utf-8'));\n\n      expect(newConfig).toHaveProperty('otherKey', 'otherValue');\n    });\n  });\n\n  describe('clearDockerContext', () => {\n    /** Path to the docker config file (in workdir). */\n    let configPath: string;\n    /** Path to the docker context metadata file (in workdir). */\n    let metaPath: string;\n\n    beforeEach(() => {\n      configPath = path.join(workdir, '.docker', 'config.json');\n      metaPath = path.join(workdir, '.docker', 'contexts', 'meta',\n        'b547d66a5de60e5f0843aba28283a8875c2ad72e99ba076060ef9ec7c09917c8',\n        'meta.json');\n    });\n\n    it('should remove the docker context directory', async() => {\n      await fs.promises.mkdir(path.dirname(metaPath), { recursive: true });\n      await fs.promises.writeFile(metaPath, 'irrelevant');\n\n      await expect(subj['clearDockerContext']()).resolves.toBeUndefined();\n      await expect(fs.promises.lstat(path.dirname(metaPath))).rejects.toThrow('ENOENT');\n    });\n\n    it('should unset docker context as needed', async() => {\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: 'rancher-desktop' }));\n      await expect(subj['clearDockerContext']()).resolves.toBeUndefined();\n\n      const contents = JSON.parse(await fs.promises.readFile(configPath, 'utf-8')) ?? {};\n\n      expect(contents).not.toHaveProperty('currentContext');\n    });\n\n    it('should not unset unrelated docker context', async() => {\n      const context = 'unrelated-context';\n\n      await fs.promises.mkdir(path.dirname(configPath), { recursive: true });\n      await fs.promises.writeFile(configPath, JSON.stringify({ currentContext: context }));\n      await expect(subj['clearDockerContext']()).resolves.toBeUndefined();\n\n      const contents = JSON.parse(await fs.promises.readFile(configPath, 'utf-8')) ?? {};\n\n      expect(contents).toHaveProperty('currentContext', context);\n    });\n\n    it('should not fail if docker config is missing', async() => {\n      await fs.promises.mkdir(path.dirname(metaPath), { recursive: true });\n      await fs.promises.writeFile(metaPath, 'irrelevant');\n\n      await expect(subj['clearDockerContext']()).resolves.toBeUndefined();\n    });\n  });\n\n  describe('credHelperWorking', () => {\n    const commonCredHelperExpectations: (...args: Parameters<typeof childProcess.spawnFile>) => void = (command, args, options) => {\n      expect(command).toEqual('docker-credential-mockhelper');\n      expect(args[0]).toEqual('list');\n      expect(options.stdio[0]).toBe('ignore');\n      expect(options.stdio[1]).toBe('ignore');\n      expect(options.stdio[2]).toBe(modules['@pkg/utils/logging'].background);\n    };\n\n    beforeEach(() => {\n      modules['@pkg/utils/paths'].resources = 'RESOURCES';\n    });\n    afterEach(() => {\n      modules['@pkg/utils/childProcess'].spawnFile.mockRestore();\n      modules['@pkg/utils/paths'].resources = paths.resources;\n    });\n\n    it('should return false when cred helper is not working', async() => {\n      modules['@pkg/utils/childProcess'].spawnFile\n        .mockImplementation((command, args, options) => {\n          commonCredHelperExpectations(command, args, options);\n\n          return Promise.reject(new Error('not a valid cred-helper'));\n        });\n      await expect(subj['credHelperWorking']('mockhelper')).resolves.toBeFalsy();\n    });\n\n    it('should return true when cred helper is working', async() => {\n      modules['@pkg/utils/childProcess'].spawnFile\n        .mockImplementation((command, args, options) => {\n          commonCredHelperExpectations(command, args, options);\n\n          return Promise.resolve({});\n        });\n      await expect(subj['credHelperWorking']('mockhelper')).resolves.toBeTruthy();\n    });\n\n    it('should blacklist docker-credentials-desktop', async() => {\n      modules['@pkg/utils/childProcess'].spawnFile\n        .mockRejectedValue('not called');\n      await expect(subj['credHelperWorking']('desktop')).resolves.toBeFalsy();\n      expect(modules['@pkg/utils/childProcess'].spawnFile).not.toHaveBeenCalled();\n    });\n\n    it('should test cred helper with resources in path', async() => {\n      modules['@pkg/utils/childProcess'].spawnFile\n        .mockImplementation((command, args, options) => {\n          commonCredHelperExpectations(command, args, options);\n\n          expect((options.env?.PATH ?? '').split(path.delimiter)).toContain(path.join('RESOURCES', os.platform(), 'bin'));\n\n          return Promise.resolve({});\n        });\n      await expect(subj['credHelperWorking']('mockhelper')).resolves.toBeTruthy();\n    });\n\n    itDarwin('should test cred helper with /usr/local/bin in path', async() => {\n      modules['@pkg/utils/childProcess'].spawnFile\n        .mockImplementation((command, args, options) => {\n          commonCredHelperExpectations(command, args, options);\n\n          expect((options.env?.PATH ?? '').split(path.delimiter)).toContain('/usr/local/bin');\n\n          return Promise.resolve({});\n        });\n      await expect(subj['credHelperWorking']('mockhelper')).resolves.toBeTruthy();\n    });\n  });\n\n  describe('getCredsStoreFor', () => {\n    const platformDefaultHelper = ({\n      linux:  'pass',\n      darwin: 'osxkeychain',\n      win32:  'wincred',\n    } as Record<string, string>)[os.platform()];\n\n    afterEach(() => {\n      jest.spyOn(subj as any, 'credHelperWorking').mockRestore();\n    });\n\n    it('should return existing cred helper if it works', async() => {\n      const helperName = 'mock-helper';\n\n      jest.spyOn(subj as any, 'credHelperWorking').mockResolvedValue(true);\n      await expect(subj['getCredsStoreFor'](helperName)).resolves.toEqual(helperName);\n    });\n\n    it('should return the right cred helper for the right platform', async() => {\n      jest.spyOn(subj as any, 'credHelperWorking').mockReturnValue(true);\n      await expect(subj['getCredsStoreFor'](undefined)).resolves.toEqual(platformDefaultHelper);\n    });\n\n    it('should return the platform helper if the existing one does not work', async() => {\n      jest.spyOn<any, any, (_: string) => Promise<boolean>>(subj, 'credHelperWorking').mockImplementation((helperName) => {\n        return Promise.resolve(os.platform() === 'linux' && helperName === 'pass');\n      });\n      await expect(subj['getCredsStoreFor']('broken-helper')).resolves.toEqual(platformDefaultHelper);\n    });\n\n    itLinux('should default to pass when it works', async() => {\n      jest.spyOn<any, any, (_: string) => Promise<boolean>>(subj, 'credHelperWorking').mockImplementation((helperName) => {\n        expect(helperName).toEqual('pass');\n\n        return Promise.resolve(true);\n      });\n      await expect(subj['getCredsStoreFor'](undefined)).resolves.toEqual('pass');\n    });\n\n    itLinux('should default to pass when secretservice is broken', async() => {\n      jest.spyOn<any, any, (_: string) => Promise<boolean>>(subj, 'credHelperWorking').mockImplementation((helperName) => {\n        return Promise.resolve(helperName === 'pass');\n      });\n      await expect(subj['getCredsStoreFor']('secretservice')).resolves.toEqual('pass');\n    });\n\n    itLinux('should default to secretservice when pass does not work', async() => {\n      jest.spyOn<any, any, (_: string) => Promise<boolean>>(subj, 'credHelperWorking').mockImplementation((helperName) => {\n        expect(helperName).toEqual('pass');\n\n        return Promise.resolve(false);\n      });\n      await expect(subj['getCredsStoreFor'](undefined)).resolves.toEqual('secretservice');\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/dockerUtils.spec.ts",
    "content": "import { imageInfo, parseImageReference } from '../dockerUtils';\n\ndescribe('parseImageReference', () => {\n  const dockerHub = new URL('https://index.docker.io');\n  const testCases: Record<string, ReturnType<typeof parseImageReference>> = {\n    component:                          new imageInfo(dockerHub, 'library/component'),\n    'name:tag':                         new imageInfo(dockerHub, 'library/name', 'tag'),\n    'dir/name':                         new imageInfo(dockerHub, 'dir/name'),\n    'registry.test/thing':              new imageInfo(new URL('https://registry.test/'), 'thing' ),\n    'registry.test:5000/org/thing:tag': new imageInfo(new URL('https://registry.test:5000/'), 'org/thing', 'tag'),\n    _:                                  null,\n    ':10/tag':                          null,\n    [`xxx:${ Array(130).join('x') }`]:  null,\n    'name:':                            null,\n    'dir/':                             null,\n    '':                                 null,\n  };\n\n  test.each(Object.entries(testCases))('%s', (input, expected) => {\n    expect(parseImageReference(input)).toEqual(expected);\n  });\n\n  describe('when parsing for prefix', () => {\n    const testCases: Record<string, ReturnType<typeof parseImageReference>> = {\n      component:            new imageInfo(dockerHub, 'library/component'),\n      'name:tag':           new imageInfo(dockerHub, 'library/name', 'tag'),\n      'dir/':               new imageInfo(dockerHub, 'dir/'),\n      'registry.test/':     new imageInfo(new URL('https://registry.test/'), ''),\n      'registry.test/dir/': new imageInfo(new URL('https://registry.test/'), 'dir/'),\n      '':                   null,\n    };\n\n    test.each(Object.entries(testCases))('%s', (input, expected) => {\n      expect(parseImageReference(input, true)).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/iterator.spec.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport AsyncCallbackIterator from '../iterator';\n\ndescribe('AsyncCallbackIterator', () => {\n  test('can iterate items', async() => {\n    const subject = new AsyncCallbackIterator<number>();\n    const results: number[] = [];\n\n    setTimeout(() => subject.emit(1), 100);\n    setTimeout(() => subject.emit(2), 200);\n    setTimeout(() => subject.end(), 300);\n\n    for await (const val of subject) {\n      results.push(val);\n    }\n\n    expect(results).toEqual([1, 2]);\n  }, 1_000);\n\n  test('can handle exceptions', async() => {\n    const subject = new AsyncCallbackIterator<number>();\n\n    setTimeout(() => subject.error('hello'), 100);\n\n    await expect(async() => {\n      for await (const val of subject) {\n        fail(`Got unexpected value ${ val }`);\n      }\n    }).rejects.toEqual('hello');\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/kubeVersions.spec.ts",
    "content": "import semver from 'semver';\n\nimport { highestStableVersion, minimumUpgradeVersion, SemanticVersionEntry, VersionEntry } from '@pkg/utils/kubeVersions';\n\ndescribe('highestStableVersion', () => {\n  it('should return the highest stable version', () => {\n    const versions: VersionEntry[] = [\n      { version: '2.0.0', channels: ['unstable'] },\n      { version: '1.1.0', channels: ['stable'] },\n      { version: '1.3.0', channels: ['stable'] },\n      { version: '1.2.0', channels: ['stable'] },\n    ];\n    const result = highestStableVersion(versions)?.version;\n\n    expect(result).toEqual('1.3.0');\n  });\n\n  it('should return highest version if no stable version is found', () => {\n    const versions: VersionEntry[] = [\n      { version: '1.0.0', channels: ['unstable'] },\n      { version: '1.2.0', channels: ['beta'] },\n      { version: '1.1.0', channels: ['beta'] },\n    ];\n    const result = highestStableVersion(versions)?.version;\n\n    expect(result).toEqual('1.2.0');\n  });\n\n  it('should return undefined if the list is empty', () => {\n    const result = highestStableVersion([]);\n\n    expect(result).toBeUndefined();\n  });\n});\n\ndescribe('minimumUpgradeVersion', () => {\n  it('should return the highest patch release of the lowest major.minor version', () => {\n    const versions: SemanticVersionEntry[] = [\n      new SemanticVersionEntry(new semver.SemVer('v1.2.1'), ['stable']),\n      new SemanticVersionEntry(new semver.SemVer('v1.0.0'), ['unstable']),\n      new SemanticVersionEntry(new semver.SemVer('v1.0.3'), ['unstable']),\n      new SemanticVersionEntry(new semver.SemVer('v1.0.2'), ['stable']),\n      new SemanticVersionEntry(new semver.SemVer('v1.2.2'), ['stable']),\n    ];\n    const result = minimumUpgradeVersion(versions)?.version.version;\n\n    expect(result).toEqual('1.0.3');\n  });\n\n  it('should return undefined if the list is empty', () => {\n    const result = minimumUpgradeVersion([]);\n\n    expect(result).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/paths.spec.ts",
    "content": "import os from 'os';\nimport path from 'path';\n\nimport mockModules from '../testUtils/mockModules';\n\nimport type { Paths } from '../paths';\n\nconst RESOURCES_PATH = path.join(process.cwd(), 'resources');\n\ntype Platform = 'darwin' | 'linux' | 'win32';\ntype expectedData = Record<Platform, string | Error>;\n\nmockModules({\n  electron: {\n    app: {\n      isPackaged: false,\n      getAppPath: () => process.cwd(),\n    },\n  },\n});\n\nconst { default: paths } = await import('../paths');\n\ndescribe('paths', () => {\n  const cases: Record<keyof Paths, expectedData> = {\n    appHome: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/',\n      linux:  '%HOME%/.local/share/rancher-desktop/',\n      darwin: '%HOME%/Library/Application Support/rancher-desktop/',\n    },\n    altAppHome: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/',\n      linux:  '%HOME%/.rd/',\n      darwin: '%HOME%/.rd/',\n    },\n    config: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/',\n      linux:  '%HOME%/.config/rancher-desktop/',\n      darwin: '%HOME%/Library/Preferences/rancher-desktop/',\n    },\n    logs: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/logs/',\n      linux:  '%HOME%/.local/share/rancher-desktop/logs/',\n      darwin: '%HOME%/Library/Logs/rancher-desktop/',\n    },\n    cache: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/cache/',\n      linux:  '%HOME%/.cache/rancher-desktop/',\n      darwin: '%HOME%/Library/Caches/rancher-desktop/',\n    },\n    wslDistro: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/distro/',\n      linux:  new Error('wslDistro'),\n      darwin: new Error('wslDistro'),\n    },\n    wslDistroData: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/distro-data/',\n      linux:  new Error('wslDistroData'),\n      darwin: new Error('wslDistroData'),\n    },\n    lima: {\n      win32:  new Error('lima'),\n      linux:  '%HOME%/.local/share/rancher-desktop/lima/',\n      darwin: '%HOME%/Library/Application Support/rancher-desktop/lima/',\n    },\n    integration: {\n      win32:  new Error('integration'),\n      linux:  '%HOME%/.rd/bin',\n      darwin: '%HOME%/.rd/bin',\n    },\n    resources: {\n      win32:  RESOURCES_PATH,\n      linux:  RESOURCES_PATH,\n      darwin: RESOURCES_PATH,\n    },\n    deploymentProfileSystem: {\n      win32:  new Error('Windows profiles will be read from Registry'),\n      linux:  '/etc/rancher-desktop',\n      darwin: '/Library/Managed Preferences',\n    },\n    altDeploymentProfileSystem: {\n      win32:  new Error('Windows profiles will be read from Registry'),\n      linux:  '/usr/etc/rancher-desktop',\n      darwin: '/Library/Preferences',\n    },\n    deploymentProfileUser: {\n      win32:  new Error('Windows profiles will be read from Registry'),\n      linux:  '%HOME%/.config',\n      darwin: '%HOME%/Library/Preferences',\n    },\n    extensionRoot: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/extensions/',\n      linux:  '%HOME%/.local/share/rancher-desktop/extensions/',\n      darwin: '%HOME%/Library/Application Support/rancher-desktop/extensions/',\n    },\n    snapshots: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/snapshots/',\n      linux:  '%HOME%/.local/share/rancher-desktop/snapshots/',\n      darwin: '%HOME%/Library/Application Support/rancher-desktop/snapshots/',\n    },\n    containerdShims: {\n      win32:  '%LOCALAPPDATA%/rancher-desktop/containerd-shims/',\n      linux:  '%HOME%/.local/share/rancher-desktop/containerd-shims/',\n      darwin: '%HOME%/Library/Application Support/rancher-desktop/containerd-shims/',\n    },\n  };\n\n  const table = Object.entries(cases).flatMap(\n    ([prop, data]) => Object.entries(data).map<[string, Platform, string | Error]>(\n      ([platform, expected]) => [prop, platform as Platform, expected],\n    ),\n  ).filter(([_, platform]) => platform === process.platform);\n\n  // Make a fake environment, because these would not be available on mac.\n  const env = Object.assign(process.env, {\n    APPDATA:      path.join(os.homedir(), 'AppData', 'Roaming'),\n    LOCALAPPDATA: path.join(os.homedir(), 'AppData', 'Local'),\n  });\n\n  test.each(table)('.%s (%s)', (prop, _, expected) => {\n    const propName = prop as keyof Paths;\n\n    if (expected instanceof Error) {\n      expect(() => paths[propName]).toThrow();\n    } else {\n      const replaceEnv = (_: string, name: string) => {\n        const result = env[name];\n\n        if (!result) {\n          throw new Error(`Missing environment variable ${ name }`);\n        }\n\n        return result;\n      };\n      const replaced = expected.replace(/%(.*?)%/g, replaceEnv);\n      const cleaned = path.normalize(path.resolve(replaced, '.'));\n      const actual = path.normalize(path.resolve(paths[propName]));\n\n      expect(actual).toEqual(cleaned);\n    }\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/__tests__/safeRename.spec.ts",
    "content": "import childProcess from 'child_process';\nimport fs from 'fs';\nimport { rename } from 'fs/promises';\nimport os from 'os';\nimport { join, resolve } from 'path';\n\nimport { jest } from '@jest/globals';\nimport { copy as extraCopy, remove as extraRemove } from 'fs-extra';\n\nimport mockModules from '../testUtils/mockModules';\n\nconst assetsDir = resolve('./pkg/rancher-desktop/utils/__tests__/assets/safeRename');\n\nconst modules = mockModules({\n  fs: {\n    ...fs,\n    promises: {\n      ...fs.promises,\n      copyFile: jest.spyOn(fs.promises, 'copyFile'),\n      rename:   jest.spyOn(fs.promises, 'rename'),\n      unlink:   jest.spyOn(fs.promises, 'unlink'),\n    },\n  },\n  'fs-extra': {\n    copy: jest.fn(extraCopy),\n  },\n});\n\n/* input tar file contents:\n * rename1.txt\n * a/\n *   a1.txt\n *   a2.txt\n *   b/\n *     b1.txt\n */\n\nfunction fileExists(path: string) {\n  try {\n    fs.accessSync(path, fs.constants.F_OK);\n\n    return true;\n  } catch (_) {\n    return false;\n  }\n}\n\nconst { default: safeRename } = await import('../safeRename');\n\ndescribe('safeRename', () => {\n  let tarDir: string;\n  let targetDir: string;\n\n  beforeEach(() => {\n    const tar = process.platform === 'win32' ? join(process.env.SystemRoot ?? '', 'system32', 'tar.exe') : 'tar';\n\n    tarDir = fs.mkdtempSync(join(os.tmpdir(), 'renameS-'));\n    childProcess.execFileSync(tar, ['xf', join(assetsDir, 'safeRename.tar'), '-C', tarDir], { cwd: assetsDir });\n    targetDir = fs.mkdtempSync(join(os.tmpdir(), 'renameD-'));\n\n    modules.fs.promises.rename.mockImplementation(rename);\n  });\n  afterEach(async() => {\n    // cleanup\n    for (const fullPath of [targetDir, tarDir]) {\n      try {\n        if (fileExists(fullPath)) {\n          await extraRemove(fullPath);\n        }\n      } catch (e) {\n        console.log(`Failed to delete ${ fullPath }: ${ e }`);\n      }\n    }\n  });\n\n  describe('rename fails', () => {\n    beforeEach(() => {\n      modules.fs.promises.rename.mockImplementation(() => {\n        throw new Error('EXDEV: cross-device link not permitted');\n      });\n    });\n    afterEach(() => {\n      modules.fs.promises.rename.mockReset();\n    });\n\n    test('can rename a file, specifying the full dest path', async() => {\n      const srcPath = join(tarDir, 'rename1.txt');\n      const destPath = join(targetDir, 'newname1.txt');\n\n      await safeRename(srcPath, destPath);\n      expect(modules.fs.promises.copyFile).toHaveBeenCalled();\n      expect(modules.fs.promises.unlink).toHaveBeenCalled();\n      expect(fileExists(destPath)).toBeTruthy();\n      expect(fileExists(srcPath)).toBeFalsy();\n    });\n\n    test('can rename a dir', async() => {\n      const srcPath = join(tarDir, 'a');\n      const destPath = join(targetDir, 'new_a');\n\n      await safeRename(srcPath, destPath);\n      expect(modules['fs-extra'].copy).toHaveBeenCalled();\n      expect(fileExists(destPath)).toBeTruthy();\n      expect(fileExists(srcPath)).toBeFalsy();\n      expect(fileExists(join(destPath, 'a1.txt'))).toBeTruthy();\n      expect(fileExists(join(destPath, 'a2.txt'))).toBeTruthy();\n      expect(fileExists(join(destPath, 'b/b1.txt'))).toBeTruthy();\n    });\n  });\n\n  describe('rename works', () => {\n    const nonExceptionMockFunc = () => {\n      throw new Error('Should not have failed when using standard rename');\n    };\n\n    beforeEach(() => {\n      modules.fs.promises.copyFile.mockImplementation(nonExceptionMockFunc);\n      modules.fs.promises.unlink.mockImplementation(nonExceptionMockFunc);\n    });\n    afterEach(() => {\n      modules.fs.promises.copyFile.mockRestore();\n      modules.fs.promises.unlink.mockRestore();\n    });\n\n    test('can rename a file, specifying the full dest path', async() => {\n      const srcPath = join(tarDir, 'rename1.txt');\n      const destPath = join(targetDir, 'newname1.txt');\n\n      await safeRename(srcPath, destPath);\n      expect(fileExists(destPath)).toBeTruthy();\n      expect(fileExists(srcPath)).toBeFalsy();\n    });\n\n    test('can rename a dir', async() => {\n      const srcPath = join(tarDir, 'a');\n      const destPath = join(targetDir, 'new_a');\n\n      await safeRename(srcPath, destPath);\n      expect(fileExists(destPath)).toBeTruthy();\n      expect(fileExists(srcPath)).toBeFalsy();\n      expect(fileExists(join(destPath, 'a1.txt'))).toBeTruthy();\n      expect(fileExists(join(destPath, 'a2.txt'))).toBeTruthy();\n      expect(fileExists(join(destPath, 'b/b1.txt'))).toBeTruthy();\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/array.ts",
    "content": "import xor from 'lodash/xor.js';\n\nimport { get, isEqual } from '@pkg/utils/object';\n\nexport function removeObject<T>(ary: T[], obj: T): T[] {\n  const idx = ary.indexOf(obj);\n\n  if ( idx >= 0 ) {\n    ary.splice(idx, 1);\n  }\n\n  return ary;\n}\n\nexport function removeObjects<T>(ary: T[], objs: T[]): T[] {\n  let i;\n  let indexes = [];\n\n  for ( i = 0; i < objs.length; i++ ) {\n    let idx = ary.indexOf(objs[i]);\n\n    // Find multiple copies of the same value\n    while ( idx !== -1 ) {\n      indexes.push(idx);\n      idx = ary.indexOf(objs[i], idx + 1);\n    }\n  }\n\n  if ( !indexes.length ) {\n    // That was easy...\n    return ary;\n  }\n\n  indexes = indexes.sort((a, b) => a - b);\n\n  const ranges = [];\n  let first: number;\n  let last: number;\n\n  // Group all the indexes into contiguous ranges\n  while ( indexes.length ) {\n    first = indexes.shift()!;\n    last = first;\n\n    while ( indexes.length && indexes[0] === last + 1 ) {\n      last = indexes.shift()!;\n    }\n\n    ranges.push({ start: first, end: last });\n  }\n\n  // Remove the items by range\n  for ( i = ranges.length - 1; i >= 0; i--) {\n    const { start, end } = ranges[i];\n\n    ary.splice(start, end - start + 1);\n  }\n\n  return ary;\n}\n\nexport function addObject<T>(ary: T[], obj: T): void {\n  const idx = ary.indexOf(obj);\n\n  if ( idx === -1 ) {\n    ary.push(obj);\n  }\n}\n\nexport function addObjects<T>(ary: T[], objs: T[]): void {\n  const unique: T[] = [];\n\n  for ( const obj of objs ) {\n    if ( !ary.includes(obj) && !unique.includes(obj) ) {\n      unique.push(obj);\n    }\n  }\n\n  ary.push(...unique);\n}\n\nexport function insertAt<T>(ary: T[], idx: number, ...objs: T[]): void {\n  ary.splice(idx, 0, ...objs);\n}\n\nexport function isArray<T>(thing: T[] | T): thing is T[] {\n  return Array.isArray(thing);\n}\n\nexport function toArray<T>(thing: T[] | T): T[] {\n  return isArray(thing) ? thing : [thing];\n}\n\nexport function removeAt<T>(ary: T[], idx: number, length = 1): T[] {\n  if ( idx < 0 ) {\n    throw new Error('Index too low');\n  }\n\n  if ( idx + length > ary.length ) {\n    throw new Error('Index + length too high');\n  }\n\n  ary.splice(idx, length);\n\n  return ary;\n}\n\nexport function clear<T>(ary: T[]): void {\n  ary.splice(0, ary.length);\n}\n\nexport function replaceWith<T>(ary: T[], ...values: T[]): void {\n  ary.splice(0, ary.length, ...values);\n}\n\nfunction findOrFilterBy<T, K, V>(\n  method: 'filter', ary: T[] | null, keyOrObj: string | K, val?: V\n): T[];\nfunction findOrFilterBy<T, K, V>(\n  method: 'find', ary: T[] | null, keyOrObj: string | K, val?: V\n): T;\nfunction findOrFilterBy<T, K, V>(\n  method: keyof T[], ary: T[] | null, keyOrObj: string | K, val?: V,\n): T[] {\n  ary = ary || [];\n\n  if ( typeof keyOrObj === 'object' ) {\n    return (ary[method] as Function)((item: T) => {\n      for ( const path in keyOrObj ) {\n        const want = keyOrObj[path];\n        const have = get(item, path);\n\n        if ( typeof want === 'undefined' ) {\n          if ( !have ) {\n            return false;\n          }\n        } else if ( have !== want ) {\n          return false;\n        }\n      }\n\n      return true;\n    });\n  } else if ( val === undefined ) {\n    return (ary[method] as Function)((item: T) => !!get(item, keyOrObj));\n  } else {\n    return (ary[method] as Function)((item: T) => get(item, keyOrObj) === val);\n  }\n}\n\nexport function filterBy<T, K, V>(\n  ary: T[] | null, keyOrObj: string | K, val?: V,\n): T[] {\n  return findOrFilterBy('filter', ary, keyOrObj, val);\n}\n\nexport function findBy<T, K, V>(\n  ary: T[] | null, keyOrObj: string | K, val?: V,\n): T {\n  return findOrFilterBy('find', ary, keyOrObj, val);\n}\n\nexport function findStringIndex(items: string[], item: string, trim = true): number {\n  return items.indexOf(trim ? item?.trim() : item);\n}\n\nexport function hasDuplicatedStrings(items: string[], caseSensitive = true): boolean {\n  const normalizedItems = items.map((i) => (caseSensitive ? i : i.toLowerCase()).trim());\n\n  for (let i = 0; i < items.length; i++) {\n    const index = findStringIndex(\n      normalizedItems,\n      (caseSensitive ? items[i] : items[i].toLowerCase()),\n    );\n\n    if (i !== index) {\n      return true;\n    }\n  }\n\n  return false;\n}\n\nexport function sameContents<T>(aryA: T[], aryB: T[]): boolean {\n  return xor(aryA, aryB).length === 0;\n}\n\nexport function sameArrayObjects<T>(aryA: T[], aryB: T[], positionAgnostic = false): boolean {\n  if (!aryA && !aryB) {\n    // catch calls from js (where props aren't type checked)\n    return false;\n  }\n  if (aryA?.length !== aryB?.length) {\n    // catch one null and not t'other, and different lengths\n    return false;\n  }\n\n  if (positionAgnostic) {\n    const consumedB: Record<number, boolean> = {};\n\n    aryB.forEach((_, index) => {\n      consumedB[index] = false;\n    });\n\n    for (let i = 0; i < aryA.length; i++) {\n      const a = aryA[i];\n\n      const validA = aryB.findIndex((arB, index) => isEqual(arB, a) && !consumedB[index] );\n\n      if (validA >= 0) {\n        consumedB[validA] = true;\n      } else {\n        return false;\n      }\n    }\n  } else {\n    for (let i = 0; i < aryA.length; i++) {\n      if (!isEqual(aryA[i], aryB[i])) {\n        return false;\n      }\n    }\n  }\n\n  return true;\n}\n\nexport function uniq<T>(ary: T[]): T[] {\n  const out: T[] = [];\n\n  addObjects(out, ary);\n\n  return out;\n}\n\nexport function concatStrings(a: string[], b: string[]): string[] {\n  return [...a.map((aa) => b.map((bb) => aa.concat(bb)))].reduce((acc, arr) => [...arr, ...acc], []);\n}\n\ninterface KubeResource { metadata: { labels: Record<string, string> } } // Migrate to central kube types resource when those are brought in\nexport function getUniqueLabelKeys<T extends KubeResource>(aryResources: T[]): string[] {\n  const uniqueObj = aryResources.reduce((res, r) => {\n    Object.keys(r.metadata.labels).forEach((l) => (res[l] = true));\n\n    return res;\n  }, {} as Record<string, boolean>);\n\n  return Object.keys(uniqueObj).sort();\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/backgroundProcess.ts",
    "content": "import timers from 'timers';\nimport util from 'util';\n\nimport * as childProcess from '@pkg/utils/childProcess';\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.background;\n\ninterface BackgroundProcessConstructorOptions {\n  /** A function to create the underlying child process. */\n  spawn:      () => Promise<childProcess.ChildProcess>;\n  /** Optional function to stop the underlying child process. */\n  destroy?:   (child: childProcess.ChildProcess | null) => Promise<void>;\n  /** Additional checks to see if the process should be started. */\n  shouldRun?: () => Promise<boolean>;\n}\n\n/**\n * This manages a given persistent background process that must be kept running\n * indefinitely (until stop is called).\n */\nexport default class BackgroundProcess {\n  /**\n   * The process being managed.\n   */\n  protected process: childProcess.ChildProcess | null = null;\n\n  /**\n   * A descriptive name of this process, for logging.\n   */\n  protected name: string;\n\n  /**\n   * A function which will spawn the process to be monitored.\n   */\n  protected spawn: BackgroundProcessConstructorOptions['spawn'];\n\n  /** A function which will terminate the process. */\n  protected destroy: Required<BackgroundProcessConstructorOptions>['destroy'];\n\n  /** A function that provides an additional check if this process should run. */\n  protected shouldRunCallback: Required<BackgroundProcessConstructorOptions>['shouldRun'];\n\n  /**\n   * Whether the process should be running.\n   */\n  protected started = false;\n\n  /**\n   * Timer used to restart the process;\n   */\n  protected timer: NodeJS.Timeout | undefined;\n\n  /**\n   * @param name A descriptive name of the process for logging.\n   */\n  constructor(name: string, options: BackgroundProcessConstructorOptions) {\n    this.name = name;\n    this.spawn = options.spawn;\n    this.destroy = options.destroy ?? ((process) => {\n      process?.kill('SIGTERM');\n\n      return Promise.resolve();\n    });\n    this.shouldRunCallback = options.shouldRun ?? function() {\n      return Promise.resolve(true);\n    };\n  }\n\n  /**\n   * Start the process asynchronously if it does not already exist, and attempt\n   * to keep it running indefinitely.\n   */\n  start() {\n    this.started = true;\n    this.restart();\n  }\n\n  /**\n   * Check if the process should be running at this point in time.\n   */\n  protected async shouldRun() {\n    return this.started && await this.shouldRunCallback();\n  }\n\n  /**\n   * Check if the monitored process is still running.\n   */\n  protected isRunning() {\n    return this.process?.exitCode === null && this.process.signalCode === null;\n  }\n\n  /**\n   * Attempt to start the process once.\n   */\n  protected async restart() {\n    if (!await this.shouldRun()) {\n      console.debug(`Not restarting ${ this.name } because shouldRun is ${ this.shouldRun }`);\n      await this.stop();\n\n      return;\n    }\n    timers.clearTimeout(this.timer);\n    this.timer = undefined;\n\n    if (this.process) {\n      if (this.isRunning()) {\n        console.debug(`Restarting ${ this.name } (pid ${ this.process.pid }): ignoring restart, already alive`);\n\n        return;\n      }\n      console.debug(`Stopping existing ${ this.name } process (pid ${ this.process.pid })`);\n      await this.destroy(this.process);\n\n      // Wait for the process to fully exit\n      while (this.isRunning()) {\n        await util.promisify(timers.setTimeout)(100);\n      }\n    }\n\n    console.log(`Launching background process ${ this.name }.`);\n    const process = await this.spawn();\n\n    this.process = process;\n    console.debug(`Launched background process ${ this.name } (pid ${ process.pid })`);\n    process.on('exit', (status, signal) => {\n      if ([0, null].includes(status) && ['SIGTERM', null].includes(signal)) {\n        console.log(`Background process ${ this.name } (pid ${ process.pid }) exited gracefully.`);\n      } else {\n        console.log(`Background process ${ this.name } (pid ${ process.pid }) exited with status ${ status } signal ${ signal }`);\n      }\n      if (!Object.is(process, this.process)) {\n        console.log(`Not current ${ this.name } process (pid ${ process.pid }, want ${ this.process?.pid }); nothing to be done.`);\n\n        return;\n      }\n      this.shouldRun().then((result) => {\n        if (result) {\n          this.timer = timers.setTimeout(() => {\n            this.restart().catch(ex => console.error(ex));\n          }, 1_000);\n          console.debug(`Background process ${ this.name } will restart (process ${ process.pid } exited)`);\n        }\n      }).catch(console.error);\n    });\n  }\n\n  /**\n   * Stop the process and do not restart it.\n   */\n  async stop() {\n    console.log(`Stopping background process ${ this.name } (pid ${ this.process?.pid ?? '<none>' }).`);\n    this.started = false;\n    timers.clearTimeout(this.timer);\n    this.timer = undefined;\n    await this.destroy(this.process);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/childProcess.ts",
    "content": "import {\n  spawn, CommonOptions, IOType, MessagingOptions, StdioOptions, StdioNull, StdioPipe,\n} from 'node:child_process';\nimport stream from 'node:stream';\n\nimport { Log } from '@pkg/utils/logging';\n\nexport { ChildProcess, exec, execFile, spawn } from 'node:child_process';\nexport type { CommonOptions, SpawnOptions } from 'node:child_process';\n\n/**\n * ErrorCommand is a symbol we attach to any exceptions thrown to describe the\n * command that failed.\n */\nexport const ErrorCommand = Symbol('child-process.command');\n\ninterface SpawnOptionsWithStdioLog<\n  Stdio extends IOType | Log,\n> extends SpawnOptionsLog {\n  stdio: Stdio\n}\n\ninterface SpawnOptionsEncoding {\n  /**\n   * The expected encoding of the executable.  If set, we will attempt to\n   * convert the output to strings.\n   */\n  encoding?: { stdout?: BufferEncoding, stderr?: BufferEncoding } | BufferEncoding\n}\n\nclass SpawnError extends Error {\n  constructor(command: string[], options: { code: number | null, signal: NodeJS.Signals | null, stdout?: string, stderr?: string }) {\n    const executable = command[0];\n    let message = `${ executable } exited with code ${ options.code }`;\n\n    if (options.code === null) {\n      message = `${ executable } exited with signal ${ options.signal }`;\n    }\n    super(message);\n\n    this[ErrorCommand] = command.join(' ');\n    this.command = command;\n    if (options.stdout !== undefined) {\n      this.stdout = options.stdout;\n    }\n    if (options.stderr !== undefined) {\n      this.stderr = options.stderr;\n    }\n    if (options.code !== null) {\n      this.code = options.code;\n    }\n    if (options.signal !== null) {\n      this.signal = options.signal;\n    }\n  }\n\n  toString() {\n    const lines = [Error.prototype.toString.call(this)];\n\n    if (process.env.NODE_ENV !== 'production') {\n      if (this.stdout !== undefined) {\n        lines.push('stdout:');\n        lines.push(...this.stdout.split('\\n').map(line => `  ${ line }`));\n      }\n      if (this.stderr !== undefined) {\n        lines.push('stderr:');\n        lines.push(...this.stderr.split('\\n').map(line => `  ${ line }`));\n      }\n    }\n\n    return lines.map(line => `${ line }\\n`).join('');\n  }\n\n  [ErrorCommand]: string;\n  command:        string[];\n  stdout?:        string;\n  stderr?:        string;\n  code?:          number;\n  signal?:        NodeJS.Signals;\n}\n\n/**\n * A StdioElementType is the type of a single element in a stdio 3-tuple.\n */\ntype StdioElementType = IOType | stream.Stream | Log | number | null | undefined;\n\ntype StdioOptionsLog = IOType | Log | StdioElementType[];\n\ninterface CommonSpawnOptionsLog extends CommonOptions, MessagingOptions {\n  argv0?:                    string;\n  stdio?:                    StdioOptionsLog;\n  shell?:                    boolean | string;\n  windowsVerbatimArguments?: boolean;\n}\n\ninterface SpawnOptionsLog extends CommonSpawnOptionsLog {\n  detached?: boolean;\n}\n\ntype StdioNullLog = StdioNull | Log;\n\ninterface SpawnOptionsWithStdioTuple<\n  Stdin extends StdioNull | StdioPipe,\n  Stdout extends StdioNullLog | StdioPipe,\n  Stderr extends StdioNullLog | StdioPipe,\n> extends SpawnOptionsLog {\n  stdio: [Stdin, Stdout, Stderr];\n}\n\nfunction isLog(it: StdioElementType): it is Log {\n  return (typeof it === 'object') && !!it && 'fdStream' in it;\n}\n\n/**\n * Wrapper around child_process.spawn() to promisify it.  On Windows, we never\n * spawn a new command prompt window.\n * @param command The executable to spawn.\n * @param args Any arguments to the executable.\n * @param options Options to child_process.spawn();\n * @throws {SpawnError} When the command returns a failure\n */\n\nexport async function spawnFile(\n  command: string,\n): Promise<Record<string, never>>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioLog<'ignore' | 'inherit' | Log> & SpawnOptionsEncoding,\n): Promise<Record<string, never>>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioLog<'pipe'> & SpawnOptionsEncoding,\n): Promise<{ stdout: string, stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioPipe, StdioPipe> & SpawnOptionsEncoding,\n): Promise<{ stdout: string, stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioPipe, StdioNullLog> & SpawnOptionsEncoding,\n): Promise<{ stdout: string }>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioNullLog, StdioPipe> & SpawnOptionsEncoding,\n): Promise<{ stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioNullLog, StdioNullLog> & SpawnOptionsEncoding,\n): Promise<Record<string, never>>;\n\nexport async function spawnFile(\n  command: string,\n  args: string[],\n): Promise<Record<string, never>>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioLog<'ignore' | 'inherit' | Log> & SpawnOptionsEncoding,\n): Promise<Record<string, never>>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioLog<'pipe'> & SpawnOptionsEncoding,\n): Promise<{ stdout: string, stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioPipe, StdioPipe> & SpawnOptionsEncoding,\n): Promise<{ stdout: string, stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioPipe, StdioNullLog> & SpawnOptionsEncoding,\n): Promise<{ stdout: string }>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioNullLog, StdioPipe> & SpawnOptionsEncoding,\n): Promise<{ stderr: string }>;\nexport async function spawnFile(\n  command: string,\n  args: string[],\n  options: SpawnOptionsWithStdioTuple<StdioNull | StdioPipe, StdioNullLog, StdioNullLog> & SpawnOptionsEncoding,\n): Promise<Record<string, never>>;\n\nexport async function spawnFile(\n  command: string,\n  args?: string[] | SpawnOptionsLog & SpawnOptionsEncoding,\n  options: SpawnOptionsLog & SpawnOptionsEncoding = {},\n): Promise<{ stdout?: string, stderr?: string }> {\n  let finalArgs: string[] = [];\n\n  if (args && !Array.isArray(args)) {\n    options = args;\n    finalArgs = [];\n  } else {\n    finalArgs = args ?? [];\n  }\n\n  const stdio = options.stdio;\n  const encodings = [\n    undefined, // stdin\n    (typeof options.encoding === 'string') ? options.encoding : options.encoding?.stdout,\n    (typeof options.encoding === 'string') ? options.encoding : options.encoding?.stderr,\n  ];\n  const stdStreams: [stream.Readable | undefined, stream.Writable | undefined, stream.Writable | undefined] = [undefined, undefined, undefined];\n  let mungedStdio: StdioOptions = 'pipe';\n\n  // If we're piping to a stream, and we need to override the encoding, then\n  // we need to do setup here.\n  if (Array.isArray(stdio)) {\n    mungedStdio = ['ignore', 'ignore', 'ignore'];\n    for (let i = 0; i < 3; i++) {\n      const original = stdio[i];\n      let munged: StdioNull | StdioPipe | number;\n\n      if (isLog(original)) {\n        munged = await original.fdStream;\n      } else if (i === 0 && original instanceof stream.Readable) {\n        munged = 'pipe';\n        stdStreams[i] = original;\n      } else {\n        munged = original;\n      }\n      if (munged instanceof stream.Writable && encodings[i]) {\n        stdStreams[i] = munged;\n        munged = 'pipe';\n      }\n      mungedStdio[i] = munged;\n    }\n  } else if (typeof stdio === 'string') {\n    mungedStdio = [stdio, stdio, stdio];\n  } else if (stdio instanceof Log) {\n    mungedStdio = ['ignore', await stdio.fdStream, await stdio.fdStream];\n  }\n\n  // Spawn the child, overriding options.stdio.  This is necessary to support\n  // transcoding the output.\n  const child = spawn(command, finalArgs, {\n    windowsHide: true,\n    ...options,\n    stdio:       mungedStdio,\n  });\n  const resultMap = { 1: 'stdout', 2: 'stderr' } as const;\n  const result: { stdout?: string, stderr?: string } = {};\n  const promises: Promise<void>[] = [];\n\n  if (Array.isArray(mungedStdio)) {\n    if (stdStreams[0] instanceof stream.Readable && child.stdin) {\n      stdStreams[0].pipe(child.stdin);\n    }\n    for (const i of [1, 2] as const) {\n      if (mungedStdio[i] === 'pipe') {\n        const encoding = encodings[i];\n        const childStream = child[resultMap[i]];\n\n        if (!stdStreams[i]) {\n          result[resultMap[i]] = '';\n        }\n        if (childStream) {\n          if (encoding) {\n            childStream.setEncoding(encoding);\n          }\n          childStream.on('data', (chunk) => {\n            if (stdStreams[i]) {\n              (stdStreams[i]).write(chunk);\n            } else {\n              result[resultMap[i]] += chunk;\n            }\n          });\n          promises.push(new Promise<void>(resolve => childStream.on('end', resolve)));\n        }\n      }\n    }\n  }\n\n  promises.push(new Promise<void>((resolve, reject) => {\n    child.on('exit', (code, signal) => {\n      if ((code === 0 && signal === null) || (code === null && signal === 'SIGTERM')) {\n        return resolve();\n      }\n      reject(new SpawnError([command].concat(finalArgs), {\n        code, signal, ...result,\n      }));\n    });\n    child.on('error', reject);\n  }));\n  await Promise.all(promises);\n\n  return result;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/clone.ts",
    "content": "/**\n * Clone a given object, returning a disconnected copy.\n *\n * @note This should be replaced via StructuredClone in NodeJS 18.\n * @note This only supports primitive objects (array, object, string, etc.)\n */\nexport default function clone<T>(input: T): T {\n  return JSON.parse(JSON.stringify(input));\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/commandLine.ts",
    "content": "import Electron from 'electron';\n\nimport Logging from '@pkg/utils/logging';\n\nconst console = Logging.commandLine;\n\n// When running the packaged app, if the app is launched as\n// PATH-TO-RD-APP ELECTRON-OPTIONS -- RD-OPTIONS\n// then process.argv shows up as\n// [ PATH-TO-RD-APP, ...RD-OPTIONS]\n// On macOS, the command-line would be `PATH-TO-RD-APP ELECTRON-OPTIONS --args RD-OPTIONS`\n//\n// When running `yarn dev ELECTRON-OPTIONS -- ARGS\n// then process.argv shows up as\n// [ .../node_modules/PATH-TO-ELECTRON-BINARY, SOURCE-ROOT,  RENDERER-PORT,\n//   PATH-TO-NODE-JS, ../scripts/dev.ts, ...ARGS]\n//\n// When running `yarn test:e2e...`, process.argv is:\n// [PATH-TO-ELECTRON-BINARY, --inspect=0, --remote-debugging-port=0, SOURCE-ROOT,\n//  --disable-gpu, --whitelisted-ips=, --disable-dev-shm-usage ]\n\n// Note that there is an `Electron.app.commandLine` object, but it's used for configuring\n// the internal Chromium instance.\n\nexport default function getCommandLineArgs(): string[] {\n  if (Electron.app.isPackaged) {\n    return process.argv.slice(1);\n  } else if ((process.env.npm_lifecycle_event ?? '').startsWith('test:e2e')) {\n    // Note there are comments in the e2e tests near this arg warning any modifications need to take\n    // this line into consideration.\n    const idx = process.argv.indexOf('--disable-dev-shm-usage');\n\n    return idx > -1 ? process.argv.slice(idx + 1) : [];\n  } else if ((process.env.NODE_ENV ?? '').startsWith('dev')) {\n    // If we're running in dev mode, look for the injected marker.\n    const idx = process.argv.indexOf('## Rancher Desktop Command Line Marker ##');\n\n    return idx >= 0 ? process.argv.slice(idx + 1) : [];\n  }\n  console.log(`Couldn't figure out how we're being run: ENV[NODE_ENV] = ${ process.env.NODE_ENV }, ENV[npm_lifecycle_event] = ${ process.env.npm_lifecycle_event ?? 'unset' }`);\n\n  return [];\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/dateUtils.ts",
    "content": "import dayjs from 'dayjs';\n\nexport function currentTime(): string {\n  const date = dayjs(Date.now());\n\n  return date.format('YYYY-MM-DD HH:mm');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/dockerDirManager.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport yaml from 'yaml';\n\nimport paths from './paths';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport clone from '@pkg/utils/clone';\nimport Logging from '@pkg/utils/logging';\nimport { jsonStringifyWithWhiteSpace } from '@pkg/utils/stringify';\n\nconst console = Logging.background;\n\n/**\n * Goes under the `auths` key in docker config.json.\n */\ninterface AuthConfig {\n  username?:      string,\n  password?:      string,\n  auth?:          string,\n  email?:         string,\n  serveraddress?: string,\n  identitytoken?: string,\n  registrytoken?: string,\n}\n\n/**\n * The parts of a docker config.json file that concern Rancher Desktop.\n */\ninterface PartialDockerConfig {\n  auths?:          Record<string, AuthConfig>,\n  credsStore?:     string,\n  credHelpers?:    Record<string, string>,\n  currentContext?: string,\n}\n\n/**\n * Manages everything under the docker CLI config directory (except, at\n * the time of writing, docker CLI plugins).\n */\nexport class DockerDirManager {\n  protected readonly dockerDirPath:        string;\n  protected readonly dockerContextDirPath: string;\n  /**\n   * Path to the 'rancher-desktop' docker context file.  The parent directory\n   * is the SHA256 hash of the docker context name ('rancher-desktop'), per the\n   * docker convention.\n   */\n  protected readonly dockerContextPath:    string;\n  protected readonly dockerConfigPath:     string;\n  protected readonly defaultDockerSockPath = '/var/run/docker.sock';\n  protected readonly contextName = 'rancher-desktop';\n\n  /**\n   * @param dockerDirPath The path to the directory containing docker CLI config.\n   */\n  constructor(dockerDirPath: string) {\n    this.dockerDirPath = dockerDirPath;\n    this.dockerContextDirPath = path.join(this.dockerDirPath, 'contexts', 'meta');\n    this.dockerContextPath = path.join(this.dockerContextDirPath,\n      'b547d66a5de60e5f0843aba28283a8875c2ad72e99ba076060ef9ec7c09917c8', 'meta.json');\n    this.dockerConfigPath = path.join(this.dockerDirPath, 'config.json');\n    console.debug(`Created new DockerDirManager to manage dir: ${ this.dockerDirPath }`);\n  }\n\n  /**\n   * Gets the docker CLI config.json file as an object.\n   */\n  protected async readDockerConfig(): Promise<PartialDockerConfig> {\n    try {\n      const rawConfig = await fs.promises.readFile(this.dockerConfigPath, { encoding: 'utf-8' });\n\n      return JSON.parse(rawConfig);\n    } catch (cause: any) {\n      if (cause.code !== 'ENOENT') {\n        throw new Error(`Failed to parse Docker config file '${ this.dockerConfigPath }'. Error: ${ cause.message }`, { cause });\n      }\n      console.log('No docker config file found');\n\n      return {};\n    }\n  }\n\n  /**\n   * Writes the docker CLI config.json file.\n   * @param config An object that is the config we want to write.\n   */\n  protected async writeDockerConfig(config: PartialDockerConfig): Promise<void> {\n    const rawConfig = jsonStringifyWithWhiteSpace(config);\n\n    await fs.promises.mkdir(this.dockerDirPath, { recursive: true });\n    await fs.promises.writeFile(this.dockerConfigPath, rawConfig, { encoding: 'utf-8' });\n    console.log(`Wrote docker config: ${ JSON.stringify(config) }`);\n  }\n\n  /**\n   * Read the docker configuration, and return the docker socket in use by the\n   * current context.  If the context is invalid, return the default socket\n   * location.\n   * @param currentContext Docker's current context, as set in the configs.\n   */\n  protected async getCurrentDockerSocket(currentContext?: string): Promise<string> {\n    if (os.platform().startsWith('win')) {\n      throw new Error('getCurrentDockerSocket is not on Windows');\n    }\n    const defaultSocket = `unix://${ this.defaultDockerSockPath }`;\n\n    if (!currentContext) {\n      return defaultSocket;\n    }\n\n    for (const dir of await fs.promises.readdir(this.dockerContextDirPath)) {\n      const contextPath = path.join(this.dockerContextDirPath, dir, 'meta.json');\n\n      try {\n        const data = yaml.parse(await fs.promises.readFile(contextPath, 'utf-8'));\n\n        if (data.Name === currentContext) {\n          return data.Endpoints?.docker?.Host as string ?? defaultSocket;\n        }\n      } catch (ex) {\n        console.log(`Failed to read context ${ dir }, skipping: ${ ex }`);\n      }\n    }\n\n    // If we reach here, the current context is invalid.\n    return defaultSocket;\n  }\n\n  /**\n   * Given some information about state external to this method, returns the\n   * name of the context that should be used. Follows these rules, in order of preference:\n   * 1. If we have control of the default socket (`/var/run/docker.sock`), return a value\n   *    that refers to the default context, which uses the default socket.\n   *    This should have the widest compatibility.\n   * 2. Return the passed current context if:\n   *    - The current context uses a valid unix socket - the user is probably using it.\n   *    - The current context uses a non-unix socket (e.g. tcp) - we can't check if it's valid.\n   * 3. The current context is invalid, so return our context (\"rancher-desktop\").\n   * @param weOwnDefaultSocket Whether Rancher Desktop has control over the default socket.\n   * @param currentContext The current context.\n   * @returns Undefined for default context; string containing context name for other contexts.\n   */\n  async getDesiredDockerContext(weOwnDefaultSocket: boolean, currentContext: string | undefined): Promise<string | undefined> {\n    if (weOwnDefaultSocket) {\n      return undefined;\n    }\n\n    // As things are, we should not get past this point on Windows.\n    if (os.platform().startsWith('win')) {\n      throw new Error('must call getDesiredDockerContext with weOwnDefaultSocket === true on Windows');\n    }\n\n    if (!currentContext) {\n      return this.contextName;\n    }\n\n    if (currentContext === this.contextName) {\n      return this.contextName;\n    }\n\n    const currentSocketUri = await this.getCurrentDockerSocket(currentContext);\n\n    if (!currentSocketUri.startsWith('unix://')) {\n      // Using a non-unix socket (e.g. TCP); assume it's working fine.\n      return currentContext;\n    }\n\n    const currentSocketPath = currentSocketUri.replace(/^unix:\\/\\//, '');\n\n    try {\n      if ((await fs.promises.stat(currentSocketPath)).isSocket()) {\n        return currentContext;\n      }\n      console.log(`Invalid existing context \"${ currentContext }\": ${ currentSocketUri } is not a socket; overriding context.`);\n    } catch (ex) {\n      console.log(`Could not read existing docker socket ${ currentSocketUri }, overriding context \"${ currentContext }\": ${ ex }`);\n    }\n\n    return this.contextName;\n  }\n\n  protected async spawnFileWithExtraPath(command: string, args: string[]) {\n    // The PATH needs to contain our resources directory (on macOS that would\n    // not be in the application's PATH).\n    // NOTE: This needs to match HttpCredentialHelperServer.\n\n    const platform = os.platform();\n    let pathVar = process.env.PATH ?? ''; // This should always be set.\n\n    pathVar += path.delimiter + path.join(paths.resources, platform, 'bin');\n\n    return await spawnFile(command, args, {\n      env:   { ...process.env, PATH: pathVar },\n      stdio: ['ignore', 'ignore', console],\n    });\n  }\n\n  /**\n   * docker-credential-pass will appear to work even when `pass` is not\n   * initialized; this provides a more detailed test to see if it works.\n   */\n  protected async credHelperPassInitialized(): Promise<boolean> {\n    try {\n      const timeoutError = Symbol('timeout');\n      const execPromise = this.spawnFileWithExtraPath('pass', ['ls']);\n      const timeoutPromise = new Promise(resolve => setTimeout(() => resolve(timeoutError), 1_000));\n      const result = await Promise.race([execPromise, timeoutPromise]);\n\n      if (Object.is(result, timeoutError)) {\n        console.debug('Timed out waiting for pass');\n\n        return false;\n      }\n\n      return true;\n    } catch (ex) {\n      console.debug(`The pass command is not working; ignoring docker-credential-pass`);\n\n      return false;\n    }\n  }\n\n  /**\n   * Determines whether the passed credential helper is working.\n   * @param helperName The cred helper name, without the \"docker-credential-\" prefix.\n   */\n  protected async credHelperWorking(helperName: string): Promise<boolean> {\n    const helperBin = `docker-credential-${ helperName }`;\n\n    console.debug(`Checking if credential helper ${ helperName } is working...`);\n\n    if (helperName === 'desktop') {\n      // Special case docker-credentials-desktop: never use it.\n      console.debug(`Rejecting ${ helperName }; blacklisted.`);\n\n      return false;\n    } else if (helperName === 'pass') {\n      if (!await this.credHelperPassInitialized()) {\n        console.debug(`Rejecting ${ helperName }; underlying library not initialized.`);\n\n        return false;\n      }\n    }\n\n    try {\n      await this.spawnFileWithExtraPath(helperBin, ['list']);\n      console.debug(`Credential helper ${ helperBin } is working.`);\n\n      return true;\n    } catch (err) {\n      console.log(`Credential helper \"${ helperBin }\" is not functional: ${ err }`);\n\n      return false;\n    }\n  }\n\n  /**\n   * Returns the default cred helper name for the current platform.\n   */\n  protected async getCredsStoreFor(currentCredsStore: string | undefined): Promise<string> {\n    const platform = os.platform();\n\n    // If the custom credential helper exists, use it.  Note that this may\n    // sometimes fail if the user is in a shell with a different PATH than our\n    // process, but we can't help with that right now.\n    if (currentCredsStore && await this.credHelperWorking(currentCredsStore)) {\n      return currentCredsStore;\n    }\n    // When running E2E tests in CI, use \"none\".  Note that we use the default\n    // value when running unit tests in CI.\n    const e2eInCI = process.env.CI && (process.env.RD_TEST ?? '').includes('e2e');\n\n    if (e2eInCI && await this.credHelperWorking('none')) {\n      return 'none';\n    }\n\n    if (platform.startsWith('win')) {\n      return 'wincred';\n    } else if (platform === 'darwin') {\n      return 'osxkeychain';\n    } else if (platform === 'linux') {\n      // On Linux, we need to match the logic used by oras-go (used by helm):\n      // If `pass` works, use it; otherwise use secret service.\n      if (await this.credHelperWorking('pass')) {\n        return 'pass';\n      }\n      return 'secretservice';\n    } else {\n      throw new Error(`platform \"${ platform }\" is not supported`);\n    }\n  }\n\n  /**\n   * Ensures that the rancher-desktop docker context exists.\n   * @param socketPath Path to the rancher-desktop specific docker socket.\n   */\n  protected async ensureDockerContextFile(socketPath: string): Promise<void> {\n    if (os.platform().startsWith('win')) {\n      throw new Error('ensureDockerContextFile is not on Windows');\n    }\n    const contextContents = {\n      Name:      this.contextName,\n      Metadata:  { Description: 'Rancher Desktop moby context' },\n      Endpoints: {\n        docker: {\n          Host:          `unix://${ socketPath }`,\n          SkipTLSVerify: false,\n        },\n      },\n    };\n\n    console.debug(`Updating docker context: writing to ${ this.dockerContextPath }`, contextContents);\n\n    await fs.promises.mkdir(path.dirname(this.dockerContextPath), { recursive: true });\n    await fs.promises.writeFile(this.dockerContextPath, JSON.stringify(contextContents));\n  }\n\n  /**\n   * Return the current docker context.\n   */\n  get currentDockerContext(): Promise<string | undefined> {\n    return this.readDockerConfig().then(cfg => cfg.currentContext);\n  }\n\n  /**\n   * Clear the docker context if we changed it for running without admin privileges\n   */\n  async clearDockerContext(): Promise<void> {\n    try {\n      await fs.promises.rm(path.dirname(this.dockerContextPath), {\n        recursive: true, force: true, maxRetries: 3,\n      });\n\n      const config = await this.readDockerConfig();\n\n      if (config?.currentContext !== this.contextName) {\n        return;\n      }\n      delete config.currentContext;\n      await this.writeDockerConfig(config);\n    } catch (ex) {\n      // Ignore the error; there really isn't much we can usefully do here.\n      console.debug(`Ignoring error when clearing docker context: ${ ex }`);\n    }\n  }\n\n  /**\n   * Ensures that the Rancher Desktop context file exists, and that the docker context\n   * is set in the config file according to our rules.\n   * @param weOwnDefaultSocket Whether Rancher Desktop has control over the default socket.\n   * @param socketPath Path to the rancher-desktop specific docker socket. Darwin/Linux only.\n   */\n  async ensureDockerContextConfigured(weOwnDefaultSocket: boolean, socketPath?: string): Promise<void> {\n    // read current config\n    const currentConfig = await this.readDockerConfig();\n\n    // Deep-copy the JSON object\n    const newConfig = clone(currentConfig);\n\n    // ensure docker context is set as we want\n    const platform = os.platform();\n\n    if ((platform === 'darwin' || platform === 'linux') && socketPath) {\n      await this.ensureDockerContextFile(socketPath);\n    }\n    newConfig.currentContext = await this.getDesiredDockerContext(weOwnDefaultSocket, currentConfig.currentContext);\n\n    // write config if modified\n    if (JSON.stringify(newConfig) !== JSON.stringify(currentConfig)) {\n      await this.writeDockerConfig(newConfig);\n    }\n\n    // Trigger diagnostics, ignoring results.\n    mainEvents.invoke('diagnostics-trigger', 'DOCKER_CONTEXT').catch(e => console.error(e));\n  }\n\n  /**\n   * Ensures that the docker config file is configured with a valid credential helper.\n   */\n  async ensureCredHelperConfigured(): Promise<void> {\n    // read current config\n    const currentConfig = await this.readDockerConfig();\n\n    // Deep-copy the JSON object\n    const newConfig = clone(currentConfig);\n\n    // ensure we are using one of our preferred credential helpers\n    newConfig.credsStore = await this.getCredsStoreFor(currentConfig.credsStore);\n\n    // write config if modified\n    if (JSON.stringify(newConfig) !== JSON.stringify(currentConfig)) {\n      await this.writeDockerConfig(newConfig);\n    }\n  }\n}\n\n/**\n * Export a singleton instance of the docker dir manager by default.\n */\nexport default new DockerDirManager(path.join(os.homedir(), '.docker'));\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/dockerUtils.ts",
    "content": "/**\n * The return result of parseImageReference().\n *\n * @note This is only exported for the test.\n */\nexport class imageInfo {\n  /**\n   * The registry, as a URL (e.g. `https://registry.opensuse.org/`);\n   * defaults to Docker Hub, i.e. `https://index.docker.io/`.\n   */\n  registry: URL;\n  /**\n   * The image name (e.g. `opensuse/leap`).\n   * For Docker Hub images, `library/` will be added if there is no org.\n   */\n  name:     string;\n  /** Any tags (e.g. `latest`, `15.4`) */\n  tag?:     string;\n\n  constructor(registry: URL, name: string, tag?: string) {\n    this.registry = registry;\n    this.name = name;\n    this.tag = tag;\n  }\n\n  /**\n   * Check if this image (excluding the tag) is the same as another one.\n   */\n  equalName(other?: imageInfo | null): boolean {\n    return this.registry.href === other?.registry.href && this.name === other?.name;\n  }\n}\n\n/**\n * makeRE is a tagged template for making regular expressions with /x (i.e.\n * ignoring any whitespace within the regular expression itself).\n * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates\n */\nfunction makeRE(strings: TemplateStringsArray, ...substitutions: any[]) {\n  const substitutionSources = substitutions.map(s => s instanceof RegExp ? s.source : s);\n  const raw = String.raw(strings, ...substitutionSources);\n  const lines = raw.split(/\\r?\\n/);\n  // Drop comments at end of line\n  const uncommentedLines = lines.map(line => line.replace(/\\s#.*$/, ''));\n\n  return new RegExp(uncommentedLines.join('').replace(/\\s+/g, ''));\n}\n\nconst { ImageNameRegExp, ImageNamePrefixRegExp } = (function() {\n  // a domain component is alpha-numeric-or-dash, but the start and end\n  // characters may not be a dash.\n  const domainComponent = /[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?/;\n  // a domain is two or more domain components joined by dot, and optionally\n  // with a colon followed by a port number.\n  const domain = makeRE`\n    ${ domainComponent }(?:\\.${ domainComponent })+\n    (?::[0-9]+)?\n    `;\n  // a name component is lower-alpha-numeric things, separated by any one of\n  // a set of separators.\n  const nameComponent = /[a-z0-9]+(?:(?:\\.|_|__|-*)[a-z0-9]+)*/;\n\n  /**\n   * ImageNameRegExp is a regular expression that matches a docker image name\n   * (including optional registry and one or more name components).\n   */\n  const ImageNameRegExp = makeRE`\n    (?:(?<domain>${ domain })/)?\n    (?<name>\n      ${ nameComponent }\n      (?:/${ nameComponent })*\n    )\n    `;\n\n  /**\n   * ImageRefPrefixRegExp is a regular expression similar to ImageNameRegExp but\n   * supports looking for prefixes (i.e. a name that ends in a slash).\n   * Note that we may end up with just the domain (no name).\n   */\n  const ImageNamePrefixRegExp = makeRE`\n    (?:\n      (?:(?<domain>${ domain })/)?\n      (?<name>\n        (?:${ nameComponent }/)*\n        (?:${ nameComponent })?\n      )\n    )\n  `;\n\n  return { ImageNameRegExp, ImageNamePrefixRegExp };\n})();\n\n/**\n * ImageTagRegExp is a regular expression that matches a docker image tag (that\n * is, only the bit after the colon).\n */\nconst ImageTagRegExp = /[\\w][\\w.-]{0,127}/;\n\nconst ImageRefRegExp = makeRE`\n  ^\n  ${ ImageNameRegExp }\n  (?::(?<tag>${ ImageTagRegExp }))?\n  $\n  `;\n\nconst ImageRefPrefixRegExp = makeRE`\n  ^\n  ${ ImageNamePrefixRegExp }\n  (?::(?<tag>${ ImageTagRegExp }))?\n  $\n  `;\n\n/**\n * Given an image reference, parse it into (possibly) registry, name, and\n * (possibly) tag components.\n * @param prefix If set, accept prefixes (names that end with a slash).\n */\nexport function parseImageReference(reference: string, prefix = false): imageInfo | null {\n  const result = (prefix ? ImageRefPrefixRegExp : ImageRefRegExp).exec(reference);\n\n  if (!result?.groups) {\n    return null;\n  }\n\n  if (!result.groups['domain'] && !result.groups['name']) {\n    // When checking for a prefix, parsing an empty string can succeed; in that\n    // case, reject it rather than accepting anything from Docker Hub.\n    return null;\n  }\n\n  let registry = result.groups['domain'] ?? 'index.docker.io';\n  let name = result.groups['name'];\n\n  if (registry === 'docker.io') {\n    registry = 'index.docker.io';\n  }\n  if (!registry.includes('://')) {\n    registry = `https://${ registry }`;\n  }\n\n  if (registry.endsWith('.docker.io') && !name.includes('/')) {\n    name = `library/${ name }`;\n  }\n\n  return new imageInfo(new URL(registry), name, result.groups['tag']);\n}\n\n/**\n * Check if a given string is a valid docker image name component (excluding any\n * tags).\n */\nexport function validateImageName(name: string): boolean {\n  return makeRE`^${ ImageNameRegExp }$`.test(name);\n}\n\n/**\n * Check if a given string is a valid docker image tag.\n */\nexport function validateImageTag(tag: string): boolean {\n  return makeRE`^${ ImageTagRegExp }$`.test(tag);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/dom.js",
    "content": "export function getParent(el, parentSelector) {\n  el = el?.parentElement;\n\n  if (!el) {\n    return null;\n  }\n\n  const matchFn = el.matches || el.matchesSelector;\n\n  if (!matchFn.call(el, parentSelector)) {\n    return getParent(el, parentSelector);\n  }\n\n  return el;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/environment.ts",
    "content": "/**\n * Checks if Rancher Desktop is running in a development or test environment\n * @returns True if Rancher Desktop is running in a development or test\n * environment\n */\nconst isDev = /^(?:dev|test)/i.test(process.env.NODE_ENV || '');\nconst isE2E = /e2e/i.test(process.env.RD_TEST ?? '');\n\nexport const isDevEnv = isDev || isE2E;\nexport const isDevBuild = !isE2E && isDev;\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/eventEmitter.ts",
    "content": "/**\n * EventEmitter wrapper, where the events are defined by the given interface.\n * Each property on the given interface is an event name, and the value should\n * be a function where the arguments are the event parameters, and the return\n * value is the return value of the event (most likely void).\n */\nexport default interface EventEmitter<T extends { [P in keyof T]: (...args: any) => void }> {\n  addListener<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  emit<eventName extends keyof T>(\n    event: eventName,\n    ...args: globalThis.Parameters<T[eventName]>\n  ): boolean;\n\n  eventNames(): (keyof T)[];\n\n  getMaxListeners(): number;\n\n  listenerCount<eventName extends keyof T>(event: eventName): number;\n\n  listeners<eventName extends keyof T>(\n    event: eventName\n  ): T[eventName][];\n\n  off<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  on<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  once<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  prependListener<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  prependOnceListener<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  removeAllListeners<eventName extends keyof T>(event: eventName): this;\n\n  removeListener<eventName extends keyof T>(\n    event: eventName,\n    listener: T[eventName]\n  ): this;\n\n  setMaxListeners(n: number): void;\n\n  rawListeners<eventName extends keyof T>(\n    event: eventName\n  ): T[eventName][];\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/filters.ts",
    "content": "import { isArray } from './array';\n\n/**\n * Can be used to merge arrays of primitive data types in lodash.mergeWith() function\n *\n * @param {*} objValue first array\n * @param {*} srcValue second array\n * @returns always second array (incoming value)\n */\nexport function arrayCustomizer(objValue: any, srcValue: any) {\n  if (isArray(objValue) && objValue.every((i: any) => typeof i !== 'object')) {\n    return srcValue;\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/imageOutputCuller.ts",
    "content": "import ImageBuildOutputCuller from '@pkg/utils/processOutputInterpreters/image-build-output';\nimport ImageNonBuildOutputCuller from '@pkg/utils/processOutputInterpreters/image-non-build-output';\nimport TrivyScanImageOutputCuller from '@pkg/utils/processOutputInterpreters/trivy-image-output';\n\nexport interface ImageOutputCuller {\n  addData(data: string): void;\n  getProcessedData(): string;\n}\n\nconst cullersByName: Record<string, new() => ImageOutputCuller> = {\n  build:         ImageBuildOutputCuller,\n  'trivy-image': TrivyScanImageOutputCuller,\n};\n\nexport default function getImageOutputCuller(command: string) {\n  const klass = cullersByName[command] || ImageNonBuildOutputCuller;\n\n  return new klass();\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/ipcRenderer.ts",
    "content": "/**\n * This is a typed version of Electron.ipcRenderer\n */\n\nimport { ipcRenderer as ipcRendererImpl } from 'electron';\n\nimport { IpcMainEvents, IpcMainInvokeEvents, IpcRendererEvents } from '@pkg/typings/electron-ipc';\n\ninterface IpcRenderer {\n  on<eventName extends keyof IpcRendererEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcRendererEvent, ...args: globalThis.Parameters<IpcRendererEvents[eventName]>) => void\n  ): this;\n\n  once<eventName extends keyof IpcRendererEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcRendererEvent, ...args: globalThis.Parameters<IpcRendererEvents[eventName]>) => void\n  ): this;\n\n  removeListener<eventName extends keyof IpcRendererEvents>(\n    channel: eventName,\n    listener: (event: Electron.IpcRendererEvent, ...args: globalThis.Parameters<IpcRendererEvents[eventName]>) => void\n  ): this;\n\n  removeAllListeners<eventName extends keyof IpcRendererEvents>(channel?: eventName): this;\n\n  send<eventName extends keyof IpcMainEvents>(channel: eventName, ...args: Parameters<IpcMainEvents[eventName]>): void;\n  sendSync<eventName extends keyof IpcMainEvents>(channel: eventName, ...args: Parameters<IpcMainEvents[eventName]>): void;\n\n  // When the renderer side is implement in JavaScript (rather than TypeScript),\n  // the type checking for arguments seems to fail and always prefers the\n  // generic overload (which we want to avoid) rather than the specific overload\n  // we provide here.  Until we convert all of the Vue components to TypeScript,\n  // for now we will need to forego checking the arguments.\n  invoke<eventName extends keyof IpcMainInvokeEvents>(\n    channel: eventName,\n    ...args: Parameters<IpcMainInvokeEvents[eventName]>\n  ): Promise<ReturnType<IpcMainInvokeEvents[eventName]>>;\n}\n\nexport const ipcRenderer: IpcRenderer = ipcRendererImpl as unknown as IpcRenderer;\n\nexport default ipcRenderer;\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/iterator.ts",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport Latch from './latch';\n\nconst doneSentinel: unique symbol = Symbol('iterator complete');\n\n/**\n * AsyncCallbackIterator is a utility class that can be used to convert a\n * callback-based API to an async iterator.\n *\n * Usage:\n *   const foo = new AsyncCallbackIterator<Number>;\n *   foo.emit(1);\n *   foo.emit(2);\n *   foo.end();\n *   // elsewhere...\n *   for await (const n of foo) { ... }\n * It's also possible to call .error() to indicate an exception.\n */\nexport default class AsyncCallbackIterator<T> implements AsyncIterableIterator<T> {\n  #next:    Promise<T | typeof doneSentinel>;\n  #resolve: (value: T | PromiseLike<T> | typeof doneSentinel) => void;\n  #reject:  (reason?: any) => void;\n  #done = false;\n\n  // #pending is used to provide backpressure; this will be resolved when we are\n  // ready to emit the next item.\n  #pending = Latch();\n\n  constructor() {\n    this.#resolve = undefined as any;\n    this.#reject = undefined as any;\n    this.#next = new Promise<T | typeof doneSentinel>((resolve, reject) => {\n      this.#resolve = resolve;\n      this.#reject = reject;\n    });\n    this.#pending.resolve();\n  }\n\n  /**\n   * Emit an item to the iterator.\n   * @param item The item to emit.\n   */\n  async emit(item: T) {\n    if (this.#done) {\n      throw new Error('Emitting result when end() has been called');\n    }\n    await this.#pending;\n    this.#pending = Latch();\n    if (!this.#done) {\n      this.#resolve(item);\n    }\n  }\n\n  /**\n   * Signal an error to the iterator.\n   * @param reason The error to emit.\n   */\n  async error(reason?: any) {\n    if (this.#done) {\n      throw new Error('Emitting result when end() has been called');\n    }\n    await this.#pending;\n    this.#pending = Latch();\n    if (!this.#done) {\n      this.#reject(reason);\n      this.#done = true;\n    }\n  }\n\n  /**\n   * Notify the iterator that the enumeration has completed.\n   */\n  end() {\n    this.#resolve(doneSentinel);\n  }\n\n  [Symbol.asyncIterator]() {\n    return this;\n  }\n\n  /**\n   * Implement the JavaScript iterator protocol by returning the next result.\n   */\n  async next(): Promise<IteratorResult<T, undefined>> {\n    if (this.#done) {\n      return { done: true, value: undefined };\n    }\n\n    const result = await this.#next;\n\n    if (result === doneSentinel) {\n      this.#done = true;\n      this.#pending.resolve();\n\n      return { done: true, value: undefined };\n    }\n\n    this.#next = new Promise<T | typeof doneSentinel>((resolve, reject) => {\n      this.#resolve = resolve;\n      this.#reject = reject;\n    });\n    this.#pending.resolve();\n\n    return { value: result };\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/kubeVersions.ts",
    "content": "import semver from 'semver';\n\nexport interface VersionEntry {\n  /**\n   * The version being described. This includes any build-specific data.\n   * This must be a valid semver-parsable string, without any pre-release\n   * versions or build metadata.\n   */\n  version:   string;\n  /**\n   * An array of strings describing the channels that include this version,\n   * if any.\n   */\n  channels?: string[];\n}\n\n/**\n * SemanticVersionEntry is a VersionEntry that contains semver.SemVer objects.\n * This should not be passed over IPC.\n */\nexport class SemanticVersionEntry implements Omit<VersionEntry, 'version'> {\n  /**\n   * The version being described. This includes any build-specific data.\n   */\n  version: semver.SemVer;\n\n  channels?: string[];\n\n  constructor(version: semver.SemVer, channels?: string[]) {\n    this.version = version;\n    this.channels = channels && channels.length > 0 ? channels : undefined;\n  }\n\n  get versionEntry(): VersionEntry {\n    return {\n      version:  this.version.version,\n      channels: this.channels,\n    };\n  }\n}\n\n/**\n * Get the highest stable version from a list of K8s.VersionEntry objects.\n * @param versions The list of K8s.VersionEntry objects.\n * @returns The highest stable version, or highest version if no stable version is found.\n */\nexport function highestStableVersion(versions: VersionEntry[]): VersionEntry | undefined {\n  const highestFirst = versions.slice().sort((a, b) => semver.compare(b.version, a.version));\n\n  return highestFirst.find(v => (v.channels ?? []).includes('stable')) ?? highestFirst[0];\n}\n\nfunction sameMajorMinorVersion(version1: semver.SemVer, version2: semver.SemVer): boolean {\n  return version1.major === version2.major && version1.minor === version2.minor;\n}\n\n/**\n * Get the highest patch release of the lowest available versions\n * @param versions The list of K8s.VersionEntry objects.\n * @returns The highest patch version.\n */\nexport function minimumUpgradeVersion(versions: SemanticVersionEntry[]): SemanticVersionEntry | undefined {\n  const lowestFirst = versions.slice().sort((a, b) => a.version.compare(b.version));\n\n  return lowestFirst.findLast(v => sameMajorMinorVersion(v.version, lowestFirst[0].version));\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/latch.ts",
    "content": "/**\n * Interface Latch is a simple extension on Promise that is resolved via calling\n * a method.  It is essentially a simplified barrier.\n *\n * @see https://en.wikipedia.org/wiki/Barrier_(computer_science)\n */\ninterface Latch extends Promise<void> {\n  /** Calling the resolve() method resolves the Latch. */\n  resolve(): void;\n  /** Calling the reject() method rejects the Latch. */\n  reject(reason: any): void;\n}\n\n/**\n * Creates a Latch that is an extension of a Promise that can be resolved via\n * calling a method on that Promise.\n */\nexport default function Latch(): Latch {\n  const holder: { resolve?: () => void, reject?: (reason: any) => void } = {};\n  const result: Latch = new Promise<void>((resolve, reject) => {\n    holder.resolve = resolve;\n    holder.reject = reject;\n  }) as any;\n\n  if (!holder.resolve || !holder.reject) {\n    throw new Error('Promise created, but resolve/reject function not set');\n  }\n  result.resolve = holder.resolve;\n  result.reject = holder.reject;\n\n  return result;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/logging.ts",
    "content": "/**\n * Logging is a helper class to manage log files; they can be viewed in the\n * Troubleshooting tab in the UI.\n *\n * Usage:\n *\n * import Logging from '.../logging';\n *\n * // Logs are compatible with console.log():\n * const console = Logging.topic;\n * console.log('Normal logging');\n * console.debug('Debug only logging');\n *\n * // It's also possible to use log streams directly:\n * Logging.topic.stream.write(...);\n *\n * // We can also handle logs directly from their path:\n * fs.readFile(Logging.topic.path, ...);\n */\n\nimport { Console } from 'console';\nimport fs from 'fs';\nimport path from 'path';\nimport stream from 'stream';\nimport util from 'util';\n\nimport paths from '@pkg/utils/paths';\n\ntype consoleKey = 'log' | 'error' | 'info' | 'warn';\ntype logLevel = 'debug' | 'info';\n\nlet LOG_LEVEL: logLevel = 'info';\n\nexport function setLogLevel(level: logLevel): void {\n  LOG_LEVEL = level;\n}\n\nexport class Log {\n  constructor(topic: string, directory = paths.logs) {\n    if (process.type === 'renderer') {\n      topic = `${ topic }-renderer`;\n    }\n    this.path = path.join(directory, `${ topic }.log`);\n    this.reopen();\n    // The following lines only exist because TypeScript can't reason about\n    // the call to this.reopen() correctly.  They are unused.\n    this.realStream ??= fs.createWriteStream(this.path, { flags: 'ERROR' });\n    this.console ??= globalThis.console;\n    this.fdPromise ??= Promise.reject();\n  }\n\n  /** The path to the log file. */\n  readonly path: string;\n\n  /** A stream to write to the log file. */\n  get stream(): fs.WriteStream {\n    return this.realStream;\n  }\n\n  /** The underlying console stream. */\n  protected console: Console;\n\n  protected realStream: fs.WriteStream;\n\n  /**\n   * Reopen the logs; this is necessary after a factory reset because the files\n   * would have been deleted from under us (so reopening ensures any new logs\n   * are readable).\n   * @note This is only used during E2E tests where we do a factory reset.\n   */\n  protected reopen(mode = 'w') {\n    if ((process.env.RD_TEST ?? '').includes('e2e')) {\n      // If we're running E2E tests, we may need to create the log directory.\n      // We don't do this normally because it's synchronous and slow.\n      fs.mkdirSync(path.dirname(this.path), { recursive: true });\n    }\n    this.realStream?.close();\n    this.realStream = fs.createWriteStream(this.path, { flags: mode, mode: 0o600 });\n    this.fdPromise = new Promise((resolve) => {\n      this.stream.on('open', resolve);\n    });\n    delete this._fdStream;\n\n    // If we're running unit tests, output to the console rather than file.\n    // However, _don't_ do so for end-to-end tests in Playwright.\n    // We detect Playwright via an environment variable we set in scripts/e2e.ts\n    if (process.env.NODE_ENV === 'test' && (process.env.RD_TEST ?? '').includes('e2e')) {\n      this.console = globalThis.console;\n    } else {\n      this.console = new Console(this.stream);\n    }\n  }\n\n  protected fdPromise: Promise<number>;\n\n  _fdStream: Promise<stream.Writable> | undefined;\n\n  /**\n   * A stream to write to the log file, with the guarantee that it has a\n   * valid fd; this is useful for passing to child_process.spawn().\n   */\n  get fdStream(): Promise<stream.Writable> {\n    if (!this._fdStream) {\n      this._fdStream = (new Promise<stream.Writable>((resolve, reject) => {\n        this.stream.write('', (error) => {\n          if (error) {\n            reject(error);\n          } else {\n            resolve(this.stream);\n          }\n        });\n      }));\n    }\n\n    return this._fdStream;\n  }\n\n  /** Print a log message to the log file; appends a new line as appropriate. */\n  log(message: any, ...optionalParameters: any[]) {\n    this.logWithDate('log', message, optionalParameters);\n  }\n\n  /** Print a log message to the log file; appends a new line as appropriate. */\n  error(message: any, ...optionalParameters: any[]) {\n    this.logWithDate('error', message, optionalParameters);\n  }\n\n  /** Print a log message to the log file; appends a new line as appropriate. */\n  info(message: any, ...optionalParameters: any[]) {\n    this.logWithDate('info', message, optionalParameters);\n  }\n\n  /** Print a log message to the log file; appends a new line as appropriate. */\n  warn(message: any, ...optionalParameters: any[]) {\n    this.logWithDate('warn', message, optionalParameters);\n  }\n\n  /**\n   * Log with the given arguments, but only if debug logging is enabled.\n   */\n  debug(data: any, ...args: any[]) {\n    if (LOG_LEVEL === 'debug') {\n      this.log(data, ...args);\n    }\n  }\n\n  /**\n   * Log a description and an exception.  If running in development or in test,\n   * include the exception logs.  This is useful for exceptions that are\n   * somewhat expected, but can occasionally be relevant.\n   */\n  debugE(message: string, exception: any) {\n    if (process.env.RD_TEST || process.env.NODE_ENV !== 'production') {\n      this.debug(message, exception);\n    } else {\n      this.debug(`${ message } ${ exception }`);\n    }\n  }\n\n  protected logWithDate(method: consoleKey, message: any, optionalParameters: any[]) {\n    this.console[method](`%s: ${ message }`, new Date().toISOString(), ...optionalParameters);\n  }\n\n  async sync() {\n    await util.promisify(fs.fsync)(await this.fdPromise);\n  }\n}\n\ntype Module = Record<string, Log>;\n\nconst logs = new Map<string, Log>();\n\n// We export a Proxy, so that we can catch all accesses to any properties, and\n// dynamically create a new log as necessary.  All property accesses on the\n// Proxy get shunted to the `get()` method, which can handle it similar to\n// Ruby's method_missing.\nexport default new Proxy<Module>({}, {\n  get: (target, prop, receiver) => {\n    if (typeof prop !== 'string') {\n      return Reflect.get(target, prop, receiver);\n    }\n\n    if (!logs.has(prop)) {\n      logs.set(prop, new Log(prop));\n    }\n\n    return logs.get(prop);\n  },\n});\n\n/**\n * Delete any existing log files from the logging directory, with the exception\n * of those that are already in use by Rancher Desktop. Should only be run once\n * we are certain that this is the only instance of Rancher Desktop running on\n * the system, so that logs from another instance are not deleted.\n */\nexport function clearLoggingDirectory(): void {\n  if ((process.env.RD_TEST ?? '').includes('e2e') || process.type !== 'browser') {\n    return;\n  }\n\n  const entries = fs.readdirSync(paths.logs, { withFileTypes: true });\n\n  for (const entry of entries) {\n    if (entry.isFile() && entry.name.endsWith('.log')) {\n      const topic = path.basename(entry.name, '.log');\n\n      if (!logs.has(topic)) {\n        const fullPath = path.join(paths.logs, entry.name);\n\n        try {\n          fs.unlinkSync(fullPath);\n        } catch (ex: any) {\n          console.log(`Failed to delete log file ${ fullPath }: ${ ex }`);\n        }\n      }\n    }\n  }\n}\n\nexport function reopenLogs() {\n  for (const log of logs.values()) {\n    log['reopen']('a');\n    // Trigger making the stream (by passing it to `Array.of()` and ignoring the\n    // result).\n    Array.of(log.fdStream);\n  }\n}\n\nfs.mkdirSync(paths.logs, { recursive: true });\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/networks.ts",
    "content": "import os from 'os';\n\nexport enum networkStatus {\n  CHECKING = 'checking...',\n  CONNECTED = 'online',\n  OFFLINE = 'offline',\n}\n\nexport function wslHostIPv4Address(): string | undefined {\n  const interfaces = os.networkInterfaces();\n  // The veth interface name changed at some time on Windows 11, so try the new name if the old one doesn't exist\n  const iface = interfaces['vEthernet (WSL)'] ?? interfaces['vEthernet (WSL (Hyper-V firewall))'] ?? [];\n\n  return iface.find(addr => addr.family === 'IPv4')?.address;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/object.js",
    "content": "import { JSONPath } from 'jsonpath-plus';\nimport cloneDeep from 'lodash/cloneDeep.js';\nimport compact from 'lodash/compact.js';\nimport difference from 'lodash/difference.js';\nimport flattenDeep from 'lodash/flattenDeep.js';\nimport isArray from 'lodash/isArray.js';\nimport isEqual from 'lodash/isEqual.js';\nimport isObject from 'lodash/isObject.js';\nimport transform from 'lodash/transform.js';\n\nimport { addObject } from '@pkg/utils/array';\nimport { splitObjectPath, joinObjectPath } from '@pkg/utils/string';\n\nexport function set(obj, path, value) {\n  let ptr = obj;\n\n  if (!ptr) {\n    return;\n  }\n\n  const parts = splitObjectPath(path);\n\n  for (let i = 0; i < parts.length; i++) {\n    const key = parts[i];\n\n    if ( i === parts.length - 1 ) {\n      ptr[key] = value;\n    } else if ( !ptr[key] ) {\n      // Make sure parent keys exist\n      ptr[key] = {};\n    }\n\n    ptr = ptr[key];\n  }\n\n  return obj;\n}\n\nexport function getAllValues(obj, path) {\n  const keysInOrder = path.split('.');\n  let currentValue = [obj];\n\n  keysInOrder.forEach((currentKey) => {\n    currentValue = currentValue.map((indexValue) => {\n      if (Array.isArray(indexValue)) {\n        return indexValue.map((arr) => arr[currentKey]).flat();\n      } else if (indexValue) {\n        return indexValue[currentKey];\n      } else {\n        return null;\n      }\n    }).flat();\n  });\n\n  return currentValue.filter((val) => val !== null);\n}\n\nexport function get(obj, path) {\n  if ( !path) {\n    throw new Error('Cannot translate an empty input. The t function requires a string.');\n  }\n  if ( path.startsWith('$') ) {\n    try {\n      return JSONPath({\n        path,\n        json: obj,\n        wrap: false,\n      });\n    } catch (e) {\n      console.log('JSON Path error', e, path, obj);\n\n      return '(JSON Path err)';\n    }\n  }\n  if ( !path.includes('.') ) {\n    return obj?.[path];\n  }\n\n  const parts = splitObjectPath(path);\n\n  for (let i = 0; i < parts.length; i++) {\n    if (!obj) {\n      return;\n    }\n\n    obj = obj[parts[i]];\n  }\n\n  return obj;\n}\n\nexport function remove(obj, path) {\n  const parentAry = splitObjectPath(path);\n\n  // Remove the very last part of the path\n\n  if (parentAry.length === 1) {\n    obj[path] = undefined;\n    delete obj[path];\n  } else {\n    const leafKey = parentAry.pop();\n    const parent = get(obj, joinObjectPath(parentAry));\n\n    if ( parent ) {\n      parent[leafKey] = undefined;\n      delete parent[leafKey];\n    }\n  }\n\n  return obj;\n}\n\n/**\n * `delete` a property at the given path.\n *\n * This is similar to `remove` but doesn't need any fancy kube obj path splitting\n * and doesn't use `Vue.set` (avoids reactivity)\n */\nexport function deleteProperty(obj, path) {\n  const pathAr = path.split('.');\n  const propToDelete = pathAr.pop();\n\n  // Walk down path until final prop, then delete final prop\n  delete pathAr.reduce((o, k) => o[k] || {}, obj)[propToDelete];\n}\n\nexport function getter(path) {\n  return function(obj) {\n    return get(obj, path);\n  };\n}\n\nexport function clone(obj) {\n  return cloneDeep(obj);\n}\n\nexport function isEmpty(obj) {\n  if ( !obj ) {\n    return true;\n  }\n\n  return !Object.keys(obj).length;\n}\n\n/**\n * Checks to see if the object is a simple key value pair where all values are\n * just primitives.\n * @param {any} obj\n */\nexport function isSimpleKeyValue(obj) {\n  return obj !== null &&\n    !Array.isArray(obj) &&\n    typeof obj === 'object' &&\n    Object.values(obj || {}).every((v) => typeof v !== 'object');\n}\n\n/*\nreturns an object with no key/value pairs (including nested) where the value is:\n  empty array\n  empty object\n  null\n  undefined\n*/\nexport function cleanUp(obj) {\n  Object.keys(obj).map((key) => {\n    const val = obj[key];\n\n    if ( Array.isArray(val) ) {\n      obj[key] = val.map((each) => {\n        if (each !== null && each !== undefined) {\n          return cleanUp(each);\n        }\n      });\n      if (obj[key].length === 0) {\n        delete obj[key];\n      }\n    } else if (typeof val === 'undefined' || val === null) {\n      delete obj[key];\n    } else if ( isObject(val) ) {\n      if (isEmpty(val)) {\n        delete obj[key];\n      }\n      obj[key] = cleanUp(val);\n    }\n  });\n\n  return obj;\n}\n\nexport function definedKeys(obj) {\n  const keys = Object.keys(obj).map((key) => {\n    const val = obj[key];\n\n    if ( Array.isArray(val) ) {\n      return `\"${ key }\"`;\n    } else if ( isObject(val) ) {\n      // no need for quotes around the subkey since the recursive call will fill that in via one of the other two statements in the if block\n      return ( definedKeys(val) || [] ).map((subkey) => `\"${ key }\".${ subkey }`);\n    } else {\n      return `\"${ key }\"`;\n    }\n  });\n\n  return compact(flattenDeep(keys));\n}\n\nexport function diff(from, to) {\n  from = from || {};\n  to = to || {};\n\n  // Copy values in 'to' that are different than from\n  const out = transform(to, (res, toVal, k) => {\n    const fromVal = from[k];\n\n    if ( isEqual(toVal, fromVal) ) {\n      return;\n    }\n\n    if ( Array.isArray(toVal) || Array.isArray(fromVal) ) {\n      // Don't diff arrays, just use the whole value\n      res[k] = toVal;\n    } else if ( isObject(toVal) && isObject(from[k]) ) {\n      res[k] = diff(fromVal, toVal);\n    } else {\n      res[k] = toVal;\n    }\n  });\n\n  const fromKeys = definedKeys(from);\n  const toKeys = definedKeys(to);\n\n  // Return keys that are in 'from' but not 'to.'\n  const missing = difference(fromKeys, toKeys);\n\n  for ( const k of missing ) {\n    set(out, k, null);\n  }\n\n  return out;\n}\n\n/**\n * Super simple lodash isEqual equivalent.\n *\n * Only checks root properties for strict equality\n */\nfunction isEqualBasic(from, to) {\n  const fromKeys = Object.keys(from || {});\n  const toKeys = Object.keys(to || {});\n\n  if (fromKeys.length !== toKeys.length) {\n    return false;\n  }\n\n  for (let i = 0; i < fromKeys.length; i++) {\n    const fromValue = from[fromKeys[i]];\n    const toValue = to[fromKeys[i]];\n\n    if (fromValue !== toValue) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\nexport { isEqualBasic as isEqual };\n\nexport function changeset(from, to, parentPath = []) {\n  let out = {};\n\n  if ( isEqual(from, to) ) {\n    return out;\n  }\n\n  for ( const k in from ) {\n    const path = joinObjectPath([...parentPath, k]);\n\n    if ( !(k in to) ) {\n      out[path] = { op: 'remove', path };\n    } else if ( (isObject(from[k]) && isObject(to[k])) || (isArray(from[k]) && isArray(to[k])) ) {\n      out = { ...out, ...changeset(from[k], to[k], [...parentPath, k]) };\n    } else if ( !isEqual(from[k], to[k]) ) {\n      out[path] = {\n        op: 'change', from: from[k], value: to[k],\n      };\n    }\n  }\n\n  for ( const k in to ) {\n    if ( !(k in from) ) {\n      const path = joinObjectPath([...parentPath, k]);\n\n      out[path] = { op: 'add', value: to[k] };\n    }\n  }\n\n  return out;\n}\n\nexport function changesetConflicts(a, b) {\n  let keys = Object.keys(a).sort();\n  const out = [];\n  const seen = {};\n\n  for ( const k of keys ) {\n    let ok = true;\n    const aa = a[k];\n    const bb = b[k];\n\n    // If we've seen a change for a parent of this key before (e.g. looking at `spec.replicas` and there's already been a change to `spec`), assume they conflict\n    for ( const parentKey of parentKeys(k) ) {\n      if ( seen[parentKey] ) {\n        ok = false;\n        break;\n      }\n    }\n\n    seen[k] = true;\n\n    if ( ok && bb ) {\n      switch ( `${ aa.op }-${ bb.op }` ) {\n      case 'add-add':\n      case 'add-change':\n      case 'change-add':\n      case 'change-change':\n        ok = isEqual(aa.value, bb.value);\n        break;\n\n      case 'add-remove':\n      case 'change-remove':\n      case 'remove-add':\n      case 'remove-change':\n        ok = false;\n        break;\n\n      case 'remove-remove':\n      default:\n        ok = true;\n        break;\n      }\n    }\n\n    if ( !ok ) {\n      addObject(out, k);\n    }\n  }\n\n  // Check parent keys going the other way\n  keys = Object.keys(b).sort();\n  for ( const k of keys ) {\n    let ok = true;\n\n    for ( const parentKey of parentKeys(k) ) {\n      if ( seen[parentKey] ) {\n        ok = false;\n        break;\n      }\n    }\n\n    seen[k] = true;\n\n    if ( !ok ) {\n      addObject(out, k);\n    }\n  }\n\n  return out.sort();\n\n  function parentKeys(k) {\n    const out = [];\n    const parts = splitObjectPath(k);\n\n    parts.pop();\n\n    while ( parts.length ) {\n      const path = joinObjectPath(parts);\n\n      out.push(path);\n      parts.pop();\n    }\n\n    return out;\n  }\n}\n\nexport function applyChangeset(obj, changeset) {\n  let entry;\n\n  for ( const path in changeset ) {\n    entry = changeset[path];\n\n    if ( entry.op === 'add' || entry.op === 'change' ) {\n      set(obj, path, entry.value);\n    } else if ( entry.op === 'remove' ) {\n      remove(obj, path);\n    } else {\n      throw new Error(`Unknown operation:${ entry.op }`);\n    }\n  }\n\n  return obj;\n}\n\n/**\n * Creates an object composed of the `object` properties `predicate` returns\n */\nexport function pickBy(obj = {}, predicate = (value, key) => false) {\n  return Object.entries(obj)\n    .reduce((res, [key, value]) => {\n      if (predicate(value, key)) {\n        res[key] = value;\n      }\n\n      return res;\n    }, {});\n}\n\n/**\n * Convert list to dictionary from a given function\n * @param {*} array\n * @param {*} callback\n * @returns\n */\nexport const toDictionary = (array, callback) => Object.assign(\n  {}, ...array.map((item) => ({ [item]: callback(item) })),\n);\n\nexport function dropKeys(obj, keys) {\n  if ( !obj ) {\n    return;\n  }\n\n  for ( const k of keys ) {\n    delete obj[k];\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/osVersion.ts",
    "content": "import * as process from 'process';\n\nimport semver from 'semver';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { Log } from '@pkg/utils/logging';\n\nlet macOsVersion: semver.SemVer;\n\nexport async function fetchMacOsVersion(console?: Log) {\n  let versionString = process.env.RD_MOCK_MACOS_VERSION;\n\n  if (!versionString) {\n    const { stdout } = await spawnFile('/usr/bin/sw_vers', ['-productVersion'], { stdio: ['ignore', 'pipe', console ?? 'ignore'] });\n\n    versionString = stdout.trimEnd();\n  }\n  const currentVersion = semver.coerce(versionString);\n\n  if (currentVersion) {\n    macOsVersion = currentVersion;\n  } else {\n    throw new Error(`Cannot convert \"${ versionString }\" to macOS semver`);\n  }\n}\n\nexport function getMacOsVersion(): semver.SemVer {\n  return macOsVersion;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/paths.ts",
    "content": "/**\n * This module describes the various paths we use to store state & data.\n */\nimport { spawnSync } from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport electron from 'electron';\n\nexport interface Paths {\n  /** appHome: the location of the main appdata directory. */\n  appHome:                    string;\n  /** altAppHome is a secondary directory for application data. */\n  altAppHome:                 string;\n  /** Directory which holds configuration. */\n  config:                     string;\n  /** Directory which holds logs. */\n  logs:                       string;\n  /** Directory which holds caches that may be removed. */\n  cache:                      string;\n  /** Directory that holds resource files in the RD installation. */\n  resources:                  string;\n  /** Directory holding Lima state (Unix-specific). */\n  lima:                       string;\n  /** Directory holding provided binary resources */\n  integration:                string;\n  /** Deployment Profile System-wide startup settings path. */\n  deploymentProfileSystem:    string;\n  /** Secondary Deployment Profile System-wide startup settings path. */\n  altDeploymentProfileSystem: string;\n  /** Deployment Profile User startup settings path. */\n  deploymentProfileUser:      string;\n  /** Directory that will hold extension data. */\n  readonly extensionRoot:     string;\n  /** Directory holding the WSL distribution (Windows-specific). */\n  wslDistro:                  string;\n  /** Directory holding the WSL data distribution (Windows-specific). */\n  wslDistroData:              string;\n  /** Directory that holds snapshots. */\n  snapshots:                  string;\n  /** Directory that holds user-managed containerd-shims. */\n  containerdShims:            string;\n}\n\nexport class UnixPaths implements Paths {\n  appHome = '';\n  altAppHome = '';\n  config = '';\n  logs = '';\n  cache = '';\n  resources = '';\n  lima = '';\n  integration = '';\n  deploymentProfileSystem = '';\n  altDeploymentProfileSystem = '';\n  deploymentProfileUser = '';\n  extensionRoot = '';\n  snapshots = '';\n  containerdShims = '';\n\n  constructor(pathsData: Record<string, unknown>) {\n    Object.assign(this, pathsData);\n  }\n\n  get wslDistro(): string {\n    throw new Error('wslDistro not available for Unix');\n  }\n\n  get wslDistroData(): string {\n    throw new Error('wslDistroData not available for Unix');\n  }\n}\n\nexport class WindowsPaths implements Paths {\n  appHome = '';\n  altAppHome = '';\n  config = '';\n  logs = '';\n  cache = '';\n  resources = '';\n  extensionRoot = '';\n  wslDistro = '';\n  wslDistroData = '';\n  snapshots = '';\n  containerdShims = '';\n\n  constructor(pathsData: Record<string, unknown>) {\n    Object.assign(this, pathsData);\n  }\n\n  get lima(): string {\n    throw new Error('lima not available for Windows');\n  }\n\n  get integration(): string {\n    throw new Error('Internal error: integration path not available for Windows');\n  }\n\n  get deploymentProfileSystem(): string {\n    throw new Error('Internal error: Windows profiles will be read from Registry');\n  }\n\n  get altDeploymentProfileSystem(): string {\n    throw new Error('Internal error: Windows profiles will be read from Registry');\n  }\n\n  get deploymentProfileUser(): string {\n    throw new Error('Internal error: Windows profiles will be read from Registry');\n  }\n}\n\n// Gets the path to rdctl. Returns null if rdctl cannot be found.\nexport function getRdctlPath(): string | null {\n  let basePath: string;\n\n  // If we are running as a script (i.e. yarn postinstall), electron.app is undefined\n  if (electron.app?.isPackaged) {\n    basePath = process.resourcesPath;\n  } else {\n    basePath = process.cwd();\n  }\n  const osSpecificName = os.platform().startsWith('win') ? `rdctl.exe` : 'rdctl';\n  const rdctlPath = path.join(basePath, 'resources', os.platform(), 'bin', osSpecificName);\n\n  if (!fs.existsSync(rdctlPath)) {\n    return null;\n  }\n\n  return rdctlPath;\n}\n\nfunction getPaths(): Paths {\n  const rdctlPath = getRdctlPath();\n  let pathsData: Partial<Paths> | undefined;\n  let errorMsg = '';\n\n  if (rdctlPath) {\n    const result = spawnSync(rdctlPath, ['paths'], { encoding: 'utf8' });\n\n    if (result.status === 0 && result.stdout.length > 0) {\n      pathsData = JSON.parse(result.stdout);\n    } else {\n      errorMsg = `rdctl paths failed: ${ JSON.stringify(result) }`;\n    }\n  }\n  if (!pathsData) {\n    const processType = process.type;\n\n    errorMsg ||= `Internal error: attempting to load the paths module from a ${ processType } process. (rdctl: ${ rdctlPath })`;\n    if (processType === 'renderer') {\n      alert(errorMsg);\n    }\n    throw new Error(errorMsg);\n  }\n\n  switch (process.platform) {\n  case 'darwin':\n    return new UnixPaths(pathsData);\n  case 'linux':\n    return new UnixPaths(pathsData);\n  case 'win32':\n    return new WindowsPaths(pathsData);\n  default:\n    throw new Error(`Platform \"${ process.platform }\" is not supported.`);\n  }\n}\n\nexport default getPaths();\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/platform.js",
    "content": "export const platform = ( typeof window === 'undefined' ? 'server' : window.navigator.platform.toLowerCase() );\nexport const userAgent = ( typeof window === 'undefined' ? 'server' : window.navigator.userAgent );\n\nexport const isLinuxy = platform.includes('linux') || platform.includes('unix');\nexport const isMac = platform.includes('mac');\nexport const isWin = platform.includes('win');\n\nexport const alternateKey = (isMac ? 'metaKey' : 'ctrlKey');\nexport const alternateLabel = (isMac ? 'Command' : 'Control');\n\nexport const moreKey = alternateKey;\nexport const moreLabel = alternateLabel;\n\nexport const rangeKey = 'shiftKey';\nexport const rangeLabel = 'Shift';\n\nexport function isAlternate(event) {\n  return !!event[alternateKey];\n}\n\nexport function isMore(event) {\n  return !!event[moreKey];\n}\n\nexport function isRange(event) {\n  return !!event[rangeKey];\n}\n\nexport function suppressContextMenu(event) {\n  return event.ctrlKey && event.button === 2;\n}\n\n// Only intended to work for Mobile Safari at the moment...\nexport function version() {\n  const match = userAgent.match(/\\s+Version\\/([0-9.]+)/);\n\n  if ( match ) {\n    return parseFloat(match[1]);\n  }\n\n  return null;\n}\n\nexport const isGecko = userAgent.includes('Gecko/');\nexport const isBlink = userAgent.includes('Chrome/');\nexport const isWebKit = !isBlink && userAgent.includes('AppleWebKit/');\nexport const isSafari = !isBlink && userAgent.includes('Safari/');\nexport const isMobile = /Android|webOS|iPhone|iPad|iPod|IEMobile/i.test(userAgent);\n\nexport const KEY = {\n  LEFT:      37,\n  UP:        38,\n  RIGHT:     39,\n  DOWN:      40,\n  ESCAPE:    27,\n  CR:        13,\n  LF:        10,\n  TAB:       9,\n  SPACE:     32,\n  PAGE_UP:   33,\n  PAGE_DOWN: 34,\n  HOME:      35,\n  END:       36,\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/position.js",
    "content": "// @TODO replace this with popper.js...\n\nexport const LEFT = 'left';\nexport const RIGHT = 'right';\nexport const TOP = 'top';\nexport const CENTER = 'center'; // These are both the same externally so you can use either,\nexport const MIDDLE = 'center'; // but have different meaning inside this file (center->left/right, middle->top/bottom)\nexport const BOTTOM = 'bottom';\nexport const AUTO = 'auto';\n\nexport function boundingRect(elem) {\n  const pos = elem.getBoundingClientRect();\n  const width = elem.offsetWidth;\n  const height = elem.offsetHeight;\n\n  return {\n    top:    pos.top,\n    right:  pos.left + width,\n    bottom: pos.top + height,\n    left:   pos.left,\n    width,\n    height,\n  };\n}\n\nexport function fakeRectFor(event) {\n  return {\n    top:    event.clientY,\n    left:   event.clientX,\n    bottom: event.clientY,\n    right:  event.clientX,\n    width:  0,\n    height: 0,\n  };\n}\n\nexport function screenRect() {\n  const width = window.innerWidth;\n  const height = window.innerHeight;\n  const top = window.pageYOffset;\n  const left = window.pageXOffset;\n\n  return {\n    top,\n    right:  left + width,\n    bottom: top + height,\n    left,\n    width,\n    height,\n  };\n}\n\nexport function fitOnScreen(contentElem, triggerElemOrEvent, opt, useDefaults) {\n  let {\n    positionX = AUTO, // Preferred horizontal position\n    positionY = AUTO, // Preferred vertical position\n  } = opt || {};\n\n  const {\n    fudgeX = 0,\n    fudgeY = 0,\n    overlapX = true, // Position on \"top\" of the trigger horizontally\n    overlapY = false, // Position on \"top\" of the trigger vertically\n  } = opt || {};\n\n  const screen = screenRect();\n  let trigger;\n\n  if ( triggerElemOrEvent instanceof Event ) {\n    trigger = fakeRectFor(triggerElemOrEvent);\n  } else {\n    trigger = boundingRect(triggerElemOrEvent);\n  }\n\n  let content = {};\n\n  if (contentElem) {\n    content = boundingRect(contentElem);\n  }\n\n  if (useDefaults) {\n    content = {\n      top:    0,\n      right:  147,\n      bottom: 163,\n      left:   0,\n      width:  147,\n      height: 80,\n    };\n  }\n\n  // console.log('screen', screen);\n  // console.log('trigger', trigger);\n  // console.log('content', content);\n\n  const style = { position: 'absolute' };\n\n  const originFor = {\n    left:   (overlapX ? trigger.left : trigger.right ),\n    center: (trigger.left + trigger.right ) / 2,\n    right:  (overlapX ? trigger.right : trigger.left ),\n    top:    (overlapY ? trigger.bottom : trigger.top ),\n    middle: (trigger.top + trigger.bottom ) / 2,\n    bottom: (overlapY ? trigger.top : trigger.bottom ),\n  };\n\n  // console.log('origin', originFor);\n\n  const gapIf = {\n    left:   screen.right - content.width - originFor.left,\n    center: Math.min(screen.right - (content.width / 2) - originFor.center, originFor.center - (content.width / 2) - screen.left),\n    right:  originFor.right - content.width - screen.left,\n    top:    originFor.bottom - content.height - screen.top,\n    middle: Math.min(originFor.middle - (content.height / 2) - screen.top, screen.bottom - (content.height / 2) - originFor.middle),\n    bottom: screen.bottom - content.height - originFor.top,\n  };\n\n  // console.log('gapIf', gapIf);\n\n  if ( positionX === CENTER && gapIf.center < 0) {\n    positionX = AUTO;\n  }\n\n  if ( positionX === AUTO ) {\n    positionX = gapIf.left < 0 || gapIf.right * 1.5 > gapIf.left ? RIGHT : LEFT;\n  } else if ( positionY === LEFT && gapIf.left < 0 ) {\n    positionX = RIGHT;\n  } else if ( positionY === RIGHT && gapIf.right < 0 ) {\n    positionX = LEFT;\n  }\n\n  switch ( positionX ) {\n  case LEFT:\n    style.left = `${ originFor.left - fudgeX }px`;\n    break;\n  case CENTER:\n    style.left = `${ ((originFor.left + originFor.right) / 2) - (content.width / 2) - fudgeX }px`;\n    break;\n  case RIGHT:\n    style.left = `${ originFor.right + fudgeX - content.width }px`;\n    // style.right = `${ screen.width - originFor.right - fudgeX }px`;\n    break;\n  }\n\n  if ( positionY === MIDDLE && gapIf.middle < 0) {\n    positionY = AUTO;\n  }\n\n  if ( positionY === AUTO ) {\n    positionY = gapIf.top < 0 || gapIf.bottom * 1.5 > gapIf.top ? BOTTOM : TOP;\n  } else if ( positionY === TOP && gapIf.top < 0 ) {\n    positionY = BOTTOM;\n  } else if ( positionY === BOTTOM && gapIf.bottom < 0 ) {\n    positionY = TOP;\n  }\n\n  switch ( positionY ) {\n  case TOP:\n    style.top = `${ originFor.top + fudgeY - content.height }px`;\n    break;\n  case CENTER:\n    style.top = `${ ((originFor.top + originFor.bottom) / 2) + fudgeY - content.height }px`;\n    break;\n  case BOTTOM:\n    style.top = `${ originFor.bottom - fudgeY }px`;\n    break;\n  }\n\n  // console.log(positionX, positionY, style);\n\n  return style;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/build.txt",
    "content": "#1 [internal] load build definition from Dockerfile\n#1 sha256:990b7cb5d01582bd3ca5add09999d1c0109b012594034eec6a7ef6795581ad9e\n#1 transferring dockerfile: 705B done\n#1 DONE 0.0s\n\n#2 [internal] load .dockerignore\n\n#2 sha256:e7977101727ffd05a69234a73ab40410bb60fdcb5a1f7998c89066b9b4c7495a\n#2 transferring context: 64B done\n#2 DONE 0.0s\n\n#4 [internal] load metadata for docker.io/library/golang:1.12-alpine\n#4 sha256:f5c919408b6d6d1768bf192db141e1f1e401d100f8cf6a0096b4743965d13027\n\n#4 ...\n\n#3 [internal] load metadata for docker.io/library/alpine:latest\n#3 sha256:d4fb25f5b5c00defc20ce26f2efc4e288de8834ed5aa59dff877b495ba88fda6\n#3 DONE 3.4s\n\n\n#4 [internal] load metadata for docker.io/library/golang:1.12-alpine\n#4 sha256:f5c919408b6d6d1768bf192db141e1f1e401d100f8cf6a0096b4743965d13027\n\n#4 DONE 4.3s\n\n\n#5 [stage-1 1/2] FROM docker.io/library/alpine@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f\n#5 sha256:c46b598ce45cd8cd40f4705fe76968d7d0b56fd99853cf12e4fad77079938f1d\n#5 resolve docker.io/library/alpine@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f 0.0s done\n#5 DONE 0.0s\n\n#10 [internal] load build context\n#10 sha256:ffbf0b87c9e8a9daacece58e3f1cf93e05a1d576c7fe7f15098b6181998af06c\n#10 transferring context: 3.26MB 0.1s done\n\n#10 DONE 0.1s\n\n#6 [build 1/6] FROM docker.io/library/golang:1.12-alpine@sha256:3f8e3ad3e7c128d29ac3004ac8314967c5ddbfa5bfa7caa59b0de493fc01686a\n#6 sha256:b8a508481fdae5b1fa776f1a845163a1ad774a7dde5345989541baf173c4606e\n#6 resolve docker.io/library/golang:1.12-alpine@sha256:3f8e3ad3e7c128d29ac3004ac8314967c5ddbfa5bfa7caa59b0de493fc01686a 0.0s done\n\n#6 sha256:4985b19198605c3ab910c5b294cc5c9bfe81c48b4547fb0555ce0d81dfb60053 0B / 126B 0.2s\n#6 sha256:665fbbf998e4658c0a6f232f6b2e286eea9c794e8e92a529a92246fb7a7a1597 0B / 124.08MB 0.2s\n#6 sha256:d909eff282003e2d64af08633f4ae58f8cab4efc0a83b86579b4bbcb0ac90956 0B / 155B 0.2s\n#6 sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9 0B / 2.80MB 0.2s\n#6 sha256:cbb0d8da1b304e1b4f86e0a2fb11185850170e41986ce261dc30ac043c6a4e55 0B / 301.26kB 0.2s\n\n#6 sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9 1.05MB / 2.80MB 0.8s\n#6 extracting sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9\n\n#6 sha256:665fbbf998e4658c0a6f232f6b2e286eea9c794e8e92a529a92246fb7a7a1597 6.29MB / 124.08MB 0.9s\n#6 extracting sha256:c9b1b535fdd91a9855fb7f82348177e5f019329a58c53c47272962dd60f71fc9 0.2s done\n#6 extracting sha256:cbb0d8da1b304e1b4f86e0a2fb11185850170e41986ce261dc30ac043c6a4e55\n\n#6 extracting sha256:cbb0d8da1b304e1b4f86e0a2fb11185850170e41986ce261dc30ac043c6a4e55 0.1s done\n#6 extracting sha256:d909eff282003e2d64af08633f4ae58f8cab4efc0a83b86579b4bbcb0ac90956 0.0s done\n\n#6 sha256:665fbbf998e4658c0a6f232f6b2e286eea9c794e8e92a529a92246fb7a7a1597 15.73MB / 124.08MB 1.2s\n\n#6 sha256:665fbbf998e4658c0a6f232f6b2e286eea9c794e8e92a529a92246fb7a7a1597 26.21MB / 124.08MB 1.5s\n\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/pull.txt",
    "content": "index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 0.6 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rmanifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 0.7 s                                                                    total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rmanifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 0.8 s                                                                    total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 0.9 s                                                                  total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.0 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.1 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.2 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.3 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.4 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.5 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  1.0 MiB/9.5 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/49.4 MiB \r\nelapsed: 1.6 s                                                                 total:  3.0 Mi (1.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  2.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  1.0 MiB/9.5 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/49.4 MiB \r\nelapsed: 1.7 s                                                                 total:  4.0 Mi (2.3 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  2.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  1.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  1.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  2.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/49.4 MiB  \r\nelapsed: 1.8 s                                                                 total:  9.0 Mi (4.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  2.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  1.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  1.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  2.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/49.4 MiB  \r\nelapsed: 1.9 s                                                                 total:  9.0 Mi (4.6 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  3.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  1.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++\u001b[0m------------------------------------|  2.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  3.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/49.4 MiB  \r\nelapsed: 2.0 s                                                                 total:  12.0 M (5.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  3.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  2.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++\u001b[0m------------------------------------|  2.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  3.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/49.4 MiB  \r\nelapsed: 2.1 s                                                                 total:  15.0 M (7.0 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  3.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  2.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++\u001b[0m------------------------------------|  2.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  4.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/49.4 MiB  \r\nelapsed: 2.2 s                                                                 total:  17.0 M (7.6 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  4.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  3.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  4.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/49.4 MiB  \r\nelapsed: 2.4 s                                                                 total:  21.0 M (8.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  4.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  3.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------|  5.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/49.4 MiB  \r\nelapsed: 2.5 s                                                                 total:  23.0 M (9.4 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  4.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  3.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------|  6.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/49.4 MiB  \r\nelapsed: 2.6 s                                                                 total:  25.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  5.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  4.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------|  7.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/49.4 MiB  \r\nelapsed: 2.7 s                                                                 total:  29.0 M (10.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  5.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------|  4.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  4.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------|  8.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/49.4 MiB  \r\nelapsed: 2.8 s                                                                 total:  31.0 M (11.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  5.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------|  4.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  4.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---|  9.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/49.4 MiB  \r\nelapsed: 2.9 s                                                                 total:  34.0 M (11.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  6.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  5.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  5.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  9.0 MiB/49.4 MiB  \r\nelapsed: 3.0 s                                                                 total:  29.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  6.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  5.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  5.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  9.0 MiB/49.4 MiB  \r\nelapsed: 3.1 s                                                                 total:  30.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  7.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------|  6.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  6.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 10.0 MiB/49.4 MiB  \r\nelapsed: 3.2 s                                                                 total:  34.0 M (10.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  8.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---|  7.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  6.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/49.4 MiB  \r\nelapsed: 3.3 s                                                                 total:  37.0 M (11.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  9.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---|  7.0 MiB/7.5 MiB   \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  6.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/49.4 MiB  \r\nelapsed: 3.4 s                                                                 total:  39.0 M (11.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------|  7.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/49.4 MiB  \r\nelapsed: 3.5 s                                                                 total:  35.0 M (10.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------|  7.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/49.4 MiB  \r\nelapsed: 3.6 s                                                                 total:  36.0 M (10.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  8.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 13.0 MiB/49.4 MiB  \r\nelapsed: 3.7 s                                                                 total:  38.0 M (10.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  8.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 13.0 MiB/49.4 MiB  \r\nelapsed: 3.8 s                                                                 total:  38.0 M (10.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  8.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 13.0 MiB/49.4 MiB  \r\nelapsed: 3.9 s                                                                 total:  38.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  8.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 13.0 MiB/49.4 MiB  \r\nelapsed: 4.0 s                                                                 total:  39.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 11.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  8.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 13.0 MiB/49.4 MiB  \r\nelapsed: 4.1 s                                                                 total:  40.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 12.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  9.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 14.0 MiB/49.4 MiB  \r\nelapsed: 4.2 s                                                                 total:  43.0 M (10.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 13.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  9.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  9.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 15.0 MiB/49.4 MiB  \r\nelapsed: 4.3 s                                                                 total:  46.0 M (10.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 13.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 10.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  9.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 16.0 MiB/49.4 MiB  \r\nelapsed: 4.4 s                                                                 total:  48.0 M (10.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 14.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 11.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/49.4 MiB  \r\nelapsed: 4.5 s                                                                 total:  53.0 M (11.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 15.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 12.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/49.4 MiB  \r\nelapsed: 4.6 s                                                                 total:  55.0 M (12.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 15.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 13.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 18.0 MiB/49.4 MiB  \r\nelapsed: 4.7 s                                                                 total:  58.0 M (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 18.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 13.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 19.0 MiB/49.4 MiB  \r\nelapsed: 4.8 s                                                                 total:  62.0 M (12.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 19.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 14.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 13.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 21.0 MiB/49.4 MiB  \r\nelapsed: 4.9 s                                                                 total:  67.0 M (13.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 19.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 14.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 13.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 22.0 MiB/49.4 MiB  \r\nelapsed: 5.0 s                                                                 total:  68.0 M (13.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 21.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 15.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 13.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 22.0 MiB/49.4 MiB  \r\nelapsed: 5.1 s                                                                 total:  71.0 M (13.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 22.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 15.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 14.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 24.0 MiB/49.4 MiB  \r\nelapsed: 5.2 s                                                                 total:  75.0 M (14.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 23.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 16.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 14.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 25.0 MiB/49.4 MiB  \r\nelapsed: 5.3 s                                                                 total:  78.0 M (14.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 24.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 16.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 15.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 27.0 MiB/49.4 MiB  \r\nelapsed: 5.4 s                                                                 total:  82.0 M (15.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 25.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 17.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 15.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 28.0 MiB/49.4 MiB  \r\nelapsed: 5.5 s                                                                 total:  85.0 M (15.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 26.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 17.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 16.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 30.0 MiB/49.4 MiB  \r\nelapsed: 5.6 s                                                                 total:  89.0 M (15.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 27.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 18.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 31.0 MiB/49.4 MiB  \r\nelapsed: 5.7 s                                                                 total:  93.0 M (16.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 28.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 18.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++\u001b[0m--------------| 31.4 MiB/49.4 MiB  \r\nelapsed: 5.8 s                                                                 total:  94.4 M (16.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 29.3 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 19.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 18.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 33.0 MiB/49.4 MiB  \r\nelapsed: 5.9 s                                                                 total:  99.3 M (16.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 30.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 19.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 18.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 34.0 MiB/49.4 MiB  \r\nelapsed: 6.0 s                                                                 total:  101.0  (16.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 31.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 19.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 18.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 35.0 MiB/49.4 MiB  \r\nelapsed: 6.1 s                                                                 total:  103.0  (16.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 31.5 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 20.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 19.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 35.0 MiB/49.4 MiB  \r\nelapsed: 6.2 s                                                                 total:  105.5  (16.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 32.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 20.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 19.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 36.0 MiB/49.4 MiB  \r\nelapsed: 6.3 s                                                                 total:  107.0  (16.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 34.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 21.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 20.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 37.0 MiB/49.4 MiB  \r\nelapsed: 6.4 s                                                                 total:  112.0  (17.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 34.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 21.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 20.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 38.0 MiB/49.4 MiB  \r\nelapsed: 6.5 s                                                                 total:  113.0  (17.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 35.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 22.7 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 20.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 39.0 MiB/49.4 MiB  \r\nelapsed: 6.7 s                                                                 total:  116.7  (17.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 37.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 23.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 21.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 40.0 MiB/49.4 MiB  \r\nelapsed: 6.8 s                                                                 total:  121.0  (17.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 37.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 24.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 22.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 41.0 MiB/49.4 MiB  \r\nelapsed: 6.9 s                                                                 total:  124.0  (18.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 38.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 24.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 22.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 42.0 MiB/49.4 MiB  \r\nelapsed: 7.0 s                                                                 total:  126.0  (18.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 40.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 24.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 23.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 44.0 MiB/49.4 MiB  \r\nelapsed: 7.1 s                                                                 total:  131.0  (18.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 40.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 25.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 24.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 45.0 MiB/49.4 MiB  \r\nelapsed: 7.2 s                                                                 total:  134.0  (18.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 41.0 MiB/183.4 MiB \r\nlayer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++++\u001b[0m-| 27.0 MiB/27.1 MiB  \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 24.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 46.0 MiB/49.4 MiB  \r\nelapsed: 7.3 s                                                                 total:  138.0  (19.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 42.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 26.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 48.0 MiB/49.4 MiB  \r\nelapsed: 7.4 s                                                                 total:  116.0  (15.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 43.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 27.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++++\u001b[0m-| 49.0 MiB/49.4 MiB  \r\nelapsed: 7.5 s                                                                 total:  119.0  (15.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 45.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 29.0 MiB/48.1 MiB  \r\nelapsed: 7.6 s                                                                 total:  74.0 M (9.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 46.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 30.0 MiB/48.1 MiB  \r\nelapsed: 7.7 s                                                                 total:  76.0 M (9.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 47.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 32.0 MiB/48.1 MiB  \r\nelapsed: 7.8 s                                                                 total:  79.0 M (10.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 49.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 34.0 MiB/48.1 MiB  \r\nelapsed: 7.9 s                                                                 total:  83.0 M (10.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 51.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 35.0 MiB/48.1 MiB  \r\nelapsed: 8.0 s                                                                 total:  86.0 M (10.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 53.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 36.0 MiB/48.1 MiB  \r\nelapsed: 8.1 s                                                                 total:  89.0 M (11.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 55.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 37.0 MiB/48.1 MiB  \r\nelapsed: 8.2 s                                                                 total:  92.0 M (11.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 57.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 38.1 MiB/48.1 MiB  \r\nelapsed: 8.3 s                                                                 total:  95.1 M (11.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 59.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 40.0 MiB/48.1 MiB  \r\nelapsed: 8.4 s                                                                 total:  99.0 M (11.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 61.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 42.0 MiB/48.1 MiB  \r\nelapsed: 8.5 s                                                                 total:  103.0  (12.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 64.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 42.0 MiB/48.1 MiB  \r\nelapsed: 8.6 s                                                                 total:  106.0  (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 65.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 43.0 MiB/48.1 MiB  \r\nelapsed: 8.7 s                                                                 total:  108.0  (12.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 66.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 45.0 MiB/48.1 MiB  \r\nelapsed: 8.8 s                                                                 total:  111.0  (12.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 67.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 46.0 MiB/48.1 MiB  \r\nelapsed: 8.9 s                                                                 total:  113.0  (12.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 69.0 MiB/183.4 MiB \r\nelapsed: 9.0 s                                                                 total:  69.0 M (7.7 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 73.0 MiB/183.4 MiB \r\nelapsed: 9.1 s                                                                 total:  73.0 M (8.0 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 76.0 MiB/183.4 MiB \r\nelapsed: 9.2 s                                                                 total:  76.0 M (8.2 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 80.0 MiB/183.4 MiB \r\nelapsed: 9.3 s                                                                 total:  80.0 M (8.6 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 83.0 MiB/183.4 MiB \r\nelapsed: 9.4 s                                                                 total:  83.0 M (8.8 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 86.0 MiB/183.4 MiB \r\nelapsed: 9.5 s                                                                 total:  86.0 M (9.0 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 90.0 MiB/183.4 MiB \r\nelapsed: 9.6 s                                                                 total:  90.0 M (9.4 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 94.0 MiB/183.4 MiB \r\nelapsed: 9.7 s                                                                 total:  94.0 M (9.7 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 97.0 MiB/183.4 MiB \r\nelapsed: 9.8 s                                                                 total:  97.0 M (9.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 100.0 Mi/183.4 MiB \r\nelapsed: 9.9 s                                                                 total:  100.0  (10.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 104.0 Mi/183.4 MiB \r\nelapsed: 10.0s                                                                 total:  104.0  (10.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 108.0 Mi/183.4 MiB \r\nelapsed: 10.2s                                                                 total:  108.0  (10.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 112.0 Mi/183.4 MiB \r\nelapsed: 10.3s                                                                 total:  112.0  (10.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 115.0 Mi/183.4 MiB \r\nelapsed: 10.4s                                                                 total:  115.0  (11.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++\u001b[0m--------------| 119.0 Mi/183.4 MiB \r\nelapsed: 10.5s                                                                 total:  119.0  (11.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 122.0 Mi/183.4 MiB \r\nelapsed: 10.6s                                                                 total:  122.0  (11.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 123.0 Mi/183.4 MiB \r\nelapsed: 10.7s                                                                 total:  123.0  (11.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 127.0 Mi/183.4 MiB \r\nelapsed: 10.8s                                                                 total:  127.0  (11.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 131.0 Mi/183.4 MiB \r\nelapsed: 10.9s                                                                 total:  131.0  (12.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 135.0 Mi/183.4 MiB \r\nelapsed: 11.0s                                                                 total:  135.0  (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 139.0 Mi/183.4 MiB \r\nelapsed: 11.1s                                                                 total:  139.0  (12.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 143.0 Mi/183.4 MiB \r\nelapsed: 11.2s                                                                 total:  143.0  (12.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 146.9 Mi/183.4 MiB \r\nelapsed: 11.3s                                                                 total:  146.9  (13.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 150.0 Mi/183.4 MiB \r\nelapsed: 11.4s                                                                 total:  150.0  (13.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 154.0 Mi/183.4 MiB \r\nelapsed: 11.5s                                                                 total:  154.0  (13.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 154.0 Mi/183.4 MiB \r\nelapsed: 11.6s                                                                 total:  154.0  (13.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 154.0 Mi/183.4 MiB \r\nelapsed: 11.7s                                                                 total:  154.0  (13.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 154.0 Mi/183.4 MiB \r\nelapsed: 11.8s                                                                 total:  154.0  (13.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 156.0 Mi/183.4 MiB \r\nelapsed: 11.9s                                                                 total:  156.0  (13.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 160.0 Mi/183.4 MiB \r\nelapsed: 12.0s                                                                 total:  160.0  (13.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 164.0 Mi/183.4 MiB \r\nelapsed: 12.1s                                                                 total:  164.0  (13.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 168.0 Mi/183.4 MiB \r\nelapsed: 12.2s                                                                 total:  168.0  (13.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 168.0 Mi/183.4 MiB \r\nelapsed: 12.3s                                                                 total:  168.0  (13.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 170.0 Mi/183.4 MiB \r\nelapsed: 12.4s                                                                 total:  170.0  (13.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 174.0 Mi/183.4 MiB \r\nelapsed: 12.5s                                                                 total:  174.0  (13.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 178.0 Mi/183.4 MiB \r\nelapsed: 12.6s                                                                 total:  178.0  (14.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++++\u001b[0m-| 181.0 Mi/183.4 MiB \r\nelapsed: 12.7s                                                                 total:  181.0  (14.2 MiB/s)                                      \r\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/pull03.txt",
    "content": "docker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.1 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.2 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.3 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.4 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.5 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.6 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.7 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.8 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest: resolving      |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.9 s                  total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.3 KiB \nelapsed: 1.0 s                                                                    total:   0.0 B (0.0 B/s)                                         \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/2.2 KiB \nelapsed: 1.1 s                                                                    total:  1.3 Ki (1.2 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/2.2 KiB \nelapsed: 1.2 s                                                                    total:  1.3 Ki (1.1 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.3 s                                                                    total:  3.5 Ki (2.7 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 KiB  \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/2.2 MiB  \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 MiB  \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/3.1 KiB  \nelapsed: 1.4 s                                                                    total:  3.5 Ki (2.5 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 KiB  \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/2.2 MiB  \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 MiB  \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.5 s                                                                    total:  6.6 Ki (4.4 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 KiB  \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/2.2 MiB  \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 MiB  \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.6 s                                                                    total:  6.6 Ki (4.1 KiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 MiB  \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.7 s                                                                    total:  2.2 Mi (1.3 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m+\u001b[0m-------------------------------------|  1.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    downloading    |\u001b[32m\u001b[0m--------------------------------------|    0.0 B/1.1 MiB  \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.8 s                                                                    total:  3.2 Mi (1.8 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++\u001b[0m------------------------------------|  2.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 1.9 s                                                                    total:  5.3 Mi (2.8 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  5.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.0 s                                                                    total:  8.3 Mi (4.2 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  7.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.1 s                                                                    total:  10.3 M (4.9 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 11.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.2 s                                                                    total:  14.3 M (6.5 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 13.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.3 s                                                                    total:  16.3 M (7.1 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 15.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.4 s                                                                    total:  18.3 M (7.6 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 18.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.5 s                                                                    total:  21.3 M (8.5 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 21.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.6 s                                                                    total:  24.3 M (9.4 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 24.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.7 s                                                                    total:  27.3 M (10.1 MiB/s)                                      \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 25.0 MiB/26.2 MiB \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.8 s                                                                    total:  28.3 M (10.1 MiB/s)                                      \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 2.9 s                                                                    total:  29.6 M (10.2 MiB/s)                                      \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.0 s                                                                    total:  29.6 M (9.9 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.1 s                                                                    total:  29.6 M (9.5 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.2 s                                                                    total:  29.6 M (9.2 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.3 s                                                                    total:  29.6 M (9.0 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.4 s                                                                    total:  29.6 M (8.7 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.5 s                                                                    total:  29.6 M (8.4 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.6 s                                                                    total:  29.6 M (8.2 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.7 s                                                                    total:  29.6 M (8.0 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.8 s                                                                    total:  29.6 M (7.8 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 3.9 s                                                                    total:  29.6 M (7.6 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.0 s                                                                    total:  29.6 M (7.4 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.1 s                                                                    total:  29.6 M (7.2 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.2 s                                                                    total:  29.6 M (7.0 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.3 s                                                                    total:  29.6 M (6.9 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.4 s                                                                    total:  29.6 M (6.7 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.5 s                                                                    total:  29.6 M (6.6 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.6 s                                                                    total:  29.6 M (6.4 MiB/s)                                       \ndocker.io/camelpunch/pr:latest:                                                   resolved       |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387: done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nconfig-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nelapsed: 4.7 s                                                                    total:  29.6 M (6.3 MiB/s)                                       \n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/pull2.txt",
    "content": "index-sha256:07c51c65ab9c1a156a1fb51eff3ec04feff7b85b2acb7d6cc65148b218d67402: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 0.9 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rmanifest-sha256:b004a71e38f8ace26e7554d5c2fa802a8bb39a5818cbe10ab49fd0b408a40c20: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.0 s                                                                    total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rmanifest-sha256:b004a71e38f8ace26e7554d5c2fa802a8bb39a5818cbe10ab49fd0b408a40c20: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.1 s                                                                    total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:49e3c70d884f2640959e0392c03f5352d852e673a28dca3b16d2d9c400149906: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.2 s                                                                  total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:49e3c70d884f2640959e0392c03f5352d852e673a28dca3b16d2d9c400149906: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.3 s                                                                  total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:49e3c70d884f2640959e0392c03f5352d852e673a28dca3b16d2d9c400149906: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.4 s                                                                  total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d7a9fa72f19290251b0f5df9144f9da3b63189e321d4b0f330344b464f1168c8: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.5 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d7a9fa72f19290251b0f5df9144f9da3b63189e321d4b0f330344b464f1168c8: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.6 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d7a9fa72f19290251b0f5df9144f9da3b63189e321d4b0f330344b464f1168c8: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.7 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d7a9fa72f19290251b0f5df9144f9da3b63189e321d4b0f330344b464f1168c8: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.8 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d7a9fa72f19290251b0f5df9144f9da3b63189e321d4b0f330344b464f1168c8: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 1.9 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nelapsed: 2.0 s                                                                 total:   0.0 B (0.0 B/s)                                         \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  1.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------|  1.0 MiB/2.1 MiB \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/49.4 MiB \r\nelapsed: 2.1 s                                                                 total:  4.0 Mi (1.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++\u001b[0m------------------------------------|  1.0 MiB/18.3 MiB \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  1.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/48.1 MiB  \r\nlayer-sha256:d1460947be4310d0aae84803ddd0a3c1e2fcdd1eccb455a01db6525fb70ddc67: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  1.4 MiB/2.1 MiB   \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  1.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/49.4 MiB  \r\nelapsed: 2.2 s                                                                 total:  8.4 Mi (3.7 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++\u001b[0m------------------------------------|  1.0 MiB/18.3 MiB \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  2.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  2.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m\u001b[0m--------------------------------------|  1.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  1.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/49.4 MiB  \r\nelapsed: 2.4 s                                                                 total:  10.0 M (4.3 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  2.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  1.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  3.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  2.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  2.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/49.4 MiB  \r\nelapsed: 2.5 s                                                                 total:  16.0 M (6.5 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  2.6 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  1.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  4.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  2.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  2.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/49.4 MiB  \r\nelapsed: 2.6 s                                                                 total:  18.6 M (7.3 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  3.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  2.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m\u001b[0m--------------------------------------|  4.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  3.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  3.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/49.4 MiB  \r\nelapsed: 2.7 s                                                                 total:  23.0 M (8.6 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  3.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  2.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  5.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------|  4.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++\u001b[0m------------------------------------|  3.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------|  3.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/49.4 MiB  \r\nelapsed: 2.8 s                                                                 total:  26.0 M (9.4 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  4.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------|  3.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  6.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------|  4.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  4.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/49.4 MiB  \r\nelapsed: 2.9 s                                                                 total:  32.0 M (11.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  4.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------|  3.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  6.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  5.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  4.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------|  4.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/49.4 MiB  \r\nelapsed: 3.0 s                                                                 total:  33.0 M (11.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++\u001b[0m------------------------------|  4.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  4.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  7.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  5.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------|  5.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/49.4 MiB  \r\nelapsed: 3.1 s                                                                 total:  38.0 M (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  5.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------|  5.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  8.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------|  5.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------|  5.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  9.0 MiB/49.4 MiB  \r\nelapsed: 3.2 s                                                                 total:  42.0 M (13.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------|  5.0 MiB/18.3 MiB  \r\nlayer-sha256:1ecef690e281c31d68c3cd0628207e8b02fb0e60575604fc5436404d1007a75e: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------|  5.0 MiB/5.9 MiB   \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  9.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------|  6.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++\u001b[0m-----------------------------------|  5.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------|  6.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 10.0 MiB/49.4 MiB  \r\nelapsed: 3.3 s                                                                 total:  46.0 M (14.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  6.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+\u001b[0m-------------------------------------|  9.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------|  6.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------|  6.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 10.0 MiB/49.4 MiB  \r\nelapsed: 3.4 s                                                                 total:  43.0 M (12.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------|  6.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 10.0 MiB/183.4 MiB \r\nlayer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---|  7.0 MiB/7.5 MiB   \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++\u001b[0m----------------------------------|  6.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------|  7.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/49.4 MiB  \r\nelapsed: 3.5 s                                                                 total:  47.0 M (13.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------|  7.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 11.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------|  7.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/49.4 MiB  \r\nelapsed: 3.6 s                                                                 total:  44.0 M (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------|  7.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 12.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++\u001b[0m---------------------------------|  7.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------|  8.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/49.4 MiB  \r\nelapsed: 3.7 s                                                                 total:  46.0 M (12.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------|  8.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 13.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/48.1 MiB  \r\nlayer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---|  9.0 MiB/9.5 MiB   \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 14.0 MiB/49.4 MiB  \r\nelapsed: 3.8 s                                                                 total:  52.0 M (13.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------|  8.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++\u001b[0m------------------------------------| 14.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++\u001b[0m--------------------------------|  8.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 14.0 MiB/49.4 MiB  \r\nelapsed: 3.9 s                                                                 total:  44.0 M (11.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------|  9.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 15.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  9.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 16.0 MiB/49.4 MiB  \r\nelapsed: 4.0 s                                                                 total:  49.0 M (12.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------|  9.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 16.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------|  9.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/49.4 MiB  \r\nelapsed: 4.1 s                                                                 total:  51.0 M (12.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 10.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 17.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 10.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 18.0 MiB/49.4 MiB  \r\nelapsed: 4.2 s                                                                 total:  55.0 M (13.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 11.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++\u001b[0m-----------------------------------| 18.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 10.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 19.0 MiB/49.4 MiB  \r\nelapsed: 4.3 s                                                                 total:  58.0 M (13.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 11.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 20.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 21.0 MiB/49.4 MiB  \r\nelapsed: 4.4 s                                                                 total:  63.0 M (14.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m++++++++++++++++++++++++\u001b[0m--------------| 12.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 20.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 11.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 22.0 MiB/49.4 MiB  \r\nelapsed: 4.5 s                                                                 total:  65.0 M (14.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 13.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 21.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 24.0 MiB/49.4 MiB  \r\nelapsed: 4.6 s                                                                 total:  70.0 M (15.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 14.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 23.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 12.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 24.0 MiB/49.4 MiB  \r\nelapsed: 4.7 s                                                                 total:  73.0 M (15.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 14.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++\u001b[0m----------------------------------| 24.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 13.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 26.0 MiB/49.4 MiB  \r\nelapsed: 4.8 s                                                                 total:  77.0 M (16.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 15.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 25.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 13.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 27.0 MiB/49.4 MiB  \r\nelapsed: 4.9 s                                                                 total:  80.0 M (16.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 16.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 25.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 14.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 29.0 MiB/49.4 MiB  \r\nelapsed: 5.0 s                                                                 total:  84.0 M (16.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 16.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 27.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 14.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 31.0 MiB/49.4 MiB  \r\nelapsed: 5.1 s                                                                 total:  88.0 M (17.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:0649c5bd98518385dfa6b98f398c409f54f3c2744297d7cab30b03ecba9fcfb5: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 17.0 MiB/18.3 MiB  \r\nlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++\u001b[0m---------------------------------| 28.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 15.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++\u001b[0m--------------| 32.0 MiB/49.4 MiB  \r\nelapsed: 5.2 s                                                                 total:  92.0 M (17.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 29.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 15.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 34.0 MiB/49.4 MiB  \r\nelapsed: 5.3 s                                                                 total:  78.0 M (14.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 30.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 16.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 35.0 MiB/49.4 MiB  \r\nelapsed: 5.4 s                                                                 total:  81.0 M (14.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 32.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 37.0 MiB/49.4 MiB  \r\nelapsed: 5.5 s                                                                 total:  86.0 M (15.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++\u001b[0m--------------------------------| 33.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 17.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 39.0 MiB/49.4 MiB  \r\nelapsed: 5.6 s                                                                 total:  89.0 M (15.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 34.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 18.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 41.0 MiB/49.4 MiB  \r\nelapsed: 5.7 s                                                                 total:  93.0 M (16.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 36.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 19.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 42.0 MiB/49.4 MiB  \r\nelapsed: 5.8 s                                                                 total:  97.0 M (16.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++\u001b[0m-------------------------------| 37.9 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 19.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 43.0 MiB/49.4 MiB  \r\nelapsed: 5.9 s                                                                 total:  99.9 M (16.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 40.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 20.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 44.0 MiB/49.4 MiB  \r\nelapsed: 6.0 s                                                                 total:  104.0  (17.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 40.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 21.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 46.0 MiB/49.4 MiB  \r\nelapsed: 6.1 s                                                                 total:  107.0  (17.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 41.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 22.0 MiB/48.1 MiB  \r\nlayer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 48.0 MiB/49.4 MiB  \r\nelapsed: 6.3 s                                                                 total:  111.0  (17.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++\u001b[0m------------------------------| 43.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 23.0 MiB/48.1 MiB  \r\nelapsed: 6.4 s                                                                 total:  66.0 M (10.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 45.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 25.0 MiB/48.1 MiB  \r\nelapsed: 6.5 s                                                                 total:  70.0 M (10.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++\u001b[0m-----------------------------| 48.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 27.0 MiB/48.1 MiB  \r\nelapsed: 6.6 s                                                                 total:  75.0 M (11.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 50.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 29.0 MiB/48.1 MiB  \r\nelapsed: 6.7 s                                                                 total:  79.0 M (11.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++\u001b[0m----------------------------| 52.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 30.0 MiB/48.1 MiB  \r\nelapsed: 6.8 s                                                                 total:  82.0 M (12.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 55.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 32.0 MiB/48.1 MiB  \r\nelapsed: 6.9 s                                                                 total:  87.0 M (12.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++\u001b[0m---------------------------| 57.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 34.0 MiB/48.1 MiB  \r\nelapsed: 7.0 s                                                                 total:  91.0 M (13.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 59.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 35.6 MiB/48.1 MiB  \r\nelapsed: 7.1 s                                                                 total:  94.6 M (13.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++\u001b[0m--------------------------| 61.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 38.0 MiB/48.1 MiB  \r\nelapsed: 7.2 s                                                                 total:  99.0 M (13.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 63.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 39.0 MiB/48.1 MiB  \r\nelapsed: 7.3 s                                                                 total:  102.0  (14.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++\u001b[0m-------------------------| 65.7 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 41.0 MiB/48.1 MiB  \r\nelapsed: 7.4 s                                                                 total:  106.7  (14.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 68.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 43.0 MiB/48.1 MiB  \r\nelapsed: 7.5 s                                                                 total:  111.0  (14.8 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 69.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 44.0 MiB/48.1 MiB  \r\nelapsed: 7.6 s                                                                 total:  113.0  (14.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++\u001b[0m------------------------| 71.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 46.0 MiB/48.1 MiB  \r\nelapsed: 7.7 s                                                                 total:  117.0  (15.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++\u001b[0m-----------------------| 74.0 MiB/183.4 MiB \r\nlayer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++++\u001b[0m-| 47.0 MiB/48.1 MiB  \r\nelapsed: 7.8 s                                                                 total:  121.0  (15.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 77.8 MiB/183.4 MiB \r\nelapsed: 7.9 s                                                                 total:  77.8 M (9.9 MiB/s)                                       \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++\u001b[0m----------------------| 81.0 MiB/183.4 MiB \r\nelapsed: 8.0 s                                                                 total:  81.0 M (10.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++\u001b[0m---------------------| 86.0 MiB/183.4 MiB \r\nelapsed: 8.1 s                                                                 total:  86.0 M (10.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++\u001b[0m--------------------| 90.0 MiB/183.4 MiB \r\nelapsed: 8.2 s                                                                 total:  90.0 M (11.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++\u001b[0m-------------------| 94.0 MiB/183.4 MiB \r\nelapsed: 8.3 s                                                                 total:  94.0 M (11.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++\u001b[0m------------------| 98.0 MiB/183.4 MiB \r\nelapsed: 8.4 s                                                                 total:  98.0 M (11.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 102.0 Mi/183.4 MiB \r\nelapsed: 8.5 s                                                                 total:  102.0  (12.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++\u001b[0m-----------------| 106.0 Mi/183.4 MiB \r\nelapsed: 8.6 s                                                                 total:  106.0  (12.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++\u001b[0m----------------| 110.0 Mi/183.4 MiB \r\nelapsed: 8.7 s                                                                 total:  110.0  (12.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++\u001b[0m---------------| 114.1 Mi/183.4 MiB \r\nelapsed: 8.8 s                                                                 total:  114.1  (12.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++\u001b[0m--------------| 119.0 Mi/183.4 MiB \r\nelapsed: 8.9 s                                                                 total:  119.0  (13.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++\u001b[0m-------------| 122.0 Mi/183.4 MiB \r\nelapsed: 9.0 s                                                                 total:  122.0  (13.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 127.0 Mi/183.4 MiB \r\nelapsed: 9.1 s                                                                 total:  127.0  (13.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++\u001b[0m------------| 130.0 Mi/183.4 MiB \r\nelapsed: 9.2 s                                                                 total:  130.0  (14.1 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++\u001b[0m-----------| 134.0 Mi/183.4 MiB \r\nelapsed: 9.3 s                                                                 total:  134.0  (14.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++\u001b[0m----------| 138.0 Mi/183.4 MiB \r\nelapsed: 9.4 s                                                                 total:  138.0  (14.6 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++\u001b[0m---------| 141.8 Mi/183.4 MiB \r\nelapsed: 9.5 s                                                                 total:  141.8  (14.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 145.0 Mi/183.4 MiB \r\nelapsed: 9.6 s                                                                 total:  145.0  (15.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++\u001b[0m--------| 149.0 Mi/183.4 MiB \r\nelapsed: 9.7 s                                                                 total:  149.0  (15.3 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++\u001b[0m-------| 153.0 Mi/183.4 MiB \r\nelapsed: 9.8 s                                                                 total:  153.0  (15.5 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++\u001b[0m------| 156.5 Mi/183.4 MiB \r\nelapsed: 9.9 s                                                                 total:  156.5  (15.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++\u001b[0m-----| 160.5 Mi/183.4 MiB \r\nelapsed: 10.0s                                                                 total:  160.5  (16.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 165.0 Mi/183.4 MiB \r\nelapsed: 10.2s                                                                 total:  165.0  (16.2 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++\u001b[0m----| 168.0 Mi/183.4 MiB \r\nelapsed: 10.3s                                                                 total:  168.0  (16.4 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++\u001b[0m---| 173.0 Mi/183.4 MiB \r\nelapsed: 10.4s                                                                 total:  173.0  (16.7 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m++++++++++++++++++++++++++++++++++++\u001b[0m--| 177.0 Mi/183.4 MiB \r\nelapsed: 10.5s                                                                 total:  177.0  (16.9 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rlayer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6: downloading    |\u001b[32m+++++++++++++++++++++++++++++++++++++\u001b[0m-| 180.0 Mi/183.4 MiB \r\nelapsed: 10.6s                                                                 total:  180.0  (17.0 MiB/s)                                      \r\n\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\r\nchirico:assets(155b-image-c) $ exit\r\nexit\r\n\nScript done on Sun Apr 25 15:07:42 2021\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/push.txt",
    "content": "config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.1 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.2 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.3 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.4 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.5 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.6 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.7 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.8 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 0.9 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   waiting        |\u001b[32m\u001b[0m--------------------------------------| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 1.0 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 1.1 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 1.2 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    waiting        |\u001b[32m\u001b[0m--------------------------------------| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 1.3 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\rconfig-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:   done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \r\nlayer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nlayer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:    done           |\u001b[32m++++++++++++++++++++++++++++++++++++++\u001b[0m| \nmanifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d: waiting        |\u001b[32m\u001b[0m--------------------------------------| \nelapsed: 1.4 s                                                                    total:   0.0 B (0.0 B/s)                                         \n\u001b[1A\u001b[2K\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\u001b[1A\u001b[2K\r\r\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/trivy-image-metric-server-input.txt",
    "content": "2021-07-09T15:58:24.556-0700\t\u001b[34mINFO\u001b[0m\tDetected OS: debian\n2021-07-09T15:58:24.557-0700\t\u001b[34mINFO\u001b[0m\tDetecting Debian vulnerabilities...\n2021-07-09T15:58:24.559-0700\t\u001b[34mINFO\u001b[0m\tNumber of PL dependency files: 0\n\nrancher/metrics-server:v0.3.6 (debian 9.9)\n==========================================\nTotal: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)\n\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/trivy-image-metric-server-output.txt",
    "content": "INFO Detected OS: debian\nINFO Detecting Debian vulnerabilities...\nINFO Number of PL dependency files: 0\n\nrancher/metrics-server:v0.3.6 (debian 9.9)\n==========================================\nTotal: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)\n\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/trivy-image-postgres-input.txt",
    "content": "2021-07-13T13:49:11.694-0700\t\u001b[34mINFO\u001b[0m\tDetected OS: debian\n2021-07-13T13:49:11.694-0700\t\u001b[34mINFO\u001b[0m\tDetecting Debian vulnerabilities...\n2021-07-13T13:49:11.705-0700\t\u001b[34mINFO\u001b[0m\tNumber of PL dependency files: 0\n[\n{\n\n\n    \n  \"Target\": \"postgres (debian 10.10)\",\n  \"Vulnerabilities\": [\n  \n  \n    \n  {\n    \"Package\": \"apt\",\n    \"Severity\": \"LOW\",\n    \"Title\": \"\",\n    \"VulnerabilityID\": \"CVE-2011-3374\",\n    \"InstalledVersion\": \"1.8.2.3\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2011-3374\",\n    \"Description\": \"It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.\"\n  }\n  \n  , \n  {\n    \"Package\": \"bash\",\n    \"Severity\": \"LOW\",\n    \"Title\": \"bash: when effective UID is not equal to its real UID the saved UID is not dropped\",\n    \"VulnerabilityID\": \"CVE-2019-18276\",\n    \"InstalledVersion\": \"5.0-4\",\n    \"FixedVersion\": \"5.0-5\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2019-18276\",\n    \"Description\": \"An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \\\"saved UID\\\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \\\"enable -f\\\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.\"\n  }\n  \n  , \n  {\n    \"Package\": \"bash\",\n    \"Severity\": \"LOW\",\n    \"Title\": \"\",\n    \"VulnerabilityID\": \"TEMP-0841856-B18BAF\",\n    \"InstalledVersion\": \"5.0-4\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF\",\n    \"Description\": \"\"\n  }\n  \n  \n  , \n  {\n    \"Package\": \"libc-bin\",\n    \"Severity\": \"CRITICAL\",\n    \"Title\": \"glibc: mq_notify does not handle separately allocated thread attributes\",\n    \"VulnerabilityID\": \"CVE-2021-33574\",\n    \"InstalledVersion\": \"2.28-10\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2021-33574\",\n    \"Description\": \"The mq_notify function in the GNU C Library (aka glibc) versions 2.32 and 2.33 has a use-after-free. It may use the notification thread attributes object (passed through its struct sigevent parameter) after it has been freed by the caller, leading to a denial of service (application crash) or possibly unspecified other impact.\"\n  }\n  \n  , \n  {\n    \"Package\": \"libc-bin\",\n    \"Severity\": \"HIGH\",\n    \"Title\": \"glibc: array overflow in backtrace functions for powerpc\",\n    \"VulnerabilityID\": \"CVE-2020-1751\",\n    \"InstalledVersion\": \"2.28-10\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2020-1751\",\n    \"Description\": \"An out-of-bounds write vulnerability was found in glibc before 2.31 when handling signal trampolines on PowerPC. Specifically, the backtrace function did not properly check the array bounds when storing the frame address, resulting in a denial of service or potential code execution. The highest threat from this vulnerability is to system availability.\"\n  }\n  \n  , \n  {\n    \"Package\": \"libc-bin\",\n    \"Severity\": \"HIGH\",\n    \"Title\": \"glibc: use-after-free in glob() function when expanding ~user\",\n    \"VulnerabilityID\": \"CVE-2020-1752\",\n    \"InstalledVersion\": \"2.28-10\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2020-1752\",\n    \"Description\": \"A use-after-free vulnerability introduced in glibc upstream version 2.14 was found in the way the tilde expansion was carried out. Directory paths containing an initial tilde followed by a valid username were affected by this issue. A local attacker could exploit this flaw by creating a specially crafted path that, when processed by the glob function, would potentially lead to arbitrary code execution. This was fixed in version 2.32.\"\n  }\n  \n  \n  , \n  {\n    \"Package\": \"libc-bin\",\n    \"Severity\": \"MEDIUM\",\n    \"Title\": \"glibc: buffer over-read in iconv when processing invalid multi-byte input sequences in the EUC-KR encoding\",\n    \"VulnerabilityID\": \"CVE-2019-25013\",\n    \"InstalledVersion\": \"2.28-10\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2019-25013\",\n    \"Description\": \"The iconv feature in the GNU C Library (aka glibc or libc6) through 2.32, when processing invalid multi-byte input sequences in the EUC-KR encoding, may have a buffer over-read.\"\n  }\n  \n  , \n  {\n    \"Package\": \"libc-bin\",\n    \"Severity\": \"MEDIUM\",\n    \"Title\": \"glibc: stack corruption from crafted input in cosl, sinl, sincosl, and tanl functions\",\n    \"VulnerabilityID\": \"CVE-2020-10029\",\n    \"InstalledVersion\": \"2.28-10\",\n    \"FixedVersion\": \"\",\n    \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2020-10029\",\n    \"Description\": \"The GNU C Library (aka glibc or libc6) before 2.32 could overflow an on-stack buffer during range reduction if an input to an 80-bit long double function contains a non-canonical bit pattern, a seen when passing a 0x5d414141414141410000 value to sinl on x86 targets. This is related to sysdeps/ieee754/ldbl-96/e_rem_pio2l.c.\"\n  }\n\n  ]\n\n}\n]\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets/trivy-image-postgres-output.txt",
    "content": "INFO Detected OS: debian\nINFO Detecting Debian vulnerabilities...\nINFO Number of PL dependency files: 0\nTarget: postgres (debian 10.10)\n\nPackage: libc-bin\nVulnerabilityID: CVE-2021-33574\nSeverity: CRITICAL\nTitle: glibc: mq_notify does not handle separately allocated thread attributes\nInstalledVersion: 2.28-10\nDescription: The mq_notify function in the GNU C Library (aka glibc) versions 2.32 and 2.33 has a use-after-free. It may use the notification thread attributes object (passed through its struct sigevent parameter) after it has been freed by the caller, leading to a denial of service (application crash) or possibly unspecified other impact.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2021-33574\n\nPackage: libc-bin\nVulnerabilityID: CVE-2020-1751\nSeverity: HIGH\nTitle: glibc: array overflow in backtrace functions for powerpc\nInstalledVersion: 2.28-10\nDescription: An out-of-bounds write vulnerability was found in glibc before 2.31 when handling signal trampolines on PowerPC. Specifically, the backtrace function did not properly check the array bounds when storing the frame address, resulting in a denial of service or potential code execution. The highest threat from this vulnerability is to system availability.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2020-1751\n\nPackage: libc-bin\nVulnerabilityID: CVE-2020-1752\nSeverity: HIGH\nTitle: glibc: use-after-free in glob() function when expanding ~user\nInstalledVersion: 2.28-10\nDescription: A use-after-free vulnerability introduced in glibc upstream version 2.14 was found in the way the tilde expansion was carried out. Directory paths containing an initial tilde followed by a valid username were affected by this issue. A local attacker could exploit this flaw by creating a specially crafted path that, when processed by the glob function, would potentially lead to arbitrary code execution. This was fixed in version 2.32.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2020-1752\n\nPackage: libc-bin\nVulnerabilityID: CVE-2019-25013\nSeverity: MEDIUM\nTitle: glibc: buffer over-read in iconv when processing invalid multi-byte input sequences in the EUC-KR encoding\nInstalledVersion: 2.28-10\nDescription: The iconv feature in the GNU C Library (aka glibc or libc6) through 2.32, when processing invalid multi-byte input sequences in the EUC-KR encoding, may have a buffer over-read.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2019-25013\n\nPackage: libc-bin\nVulnerabilityID: CVE-2020-10029\nSeverity: MEDIUM\nTitle: glibc: stack corruption from crafted input in cosl, sinl, sincosl, and tanl functions\nInstalledVersion: 2.28-10\nDescription: The GNU C Library (aka glibc or libc6) before 2.32 could overflow an on-stack buffer during range reduction if an input to an 80-bit long double function contains a non-canonical bit pattern, a seen when passing a 0x5d414141414141410000 value to sinl on x86 targets. This is related to sysdeps/ieee754/ldbl-96/e_rem_pio2l.c.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2020-10029\n\nPackage: apt\nVulnerabilityID: CVE-2011-3374\nSeverity: LOW\nInstalledVersion: 1.8.2.3\nDescription: It was found that apt-key in apt, all versions, do not correctly validate gpg keys with the master keyring, leading to a potential man-in-the-middle attack.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2011-3374\n\nPackage: bash\nVulnerabilityID: CVE-2019-18276\nSeverity: LOW\nTitle: bash: when effective UID is not equal to its real UID the saved UID is not dropped\nInstalledVersion: 5.0-4\nFixedVersion: 5.0-5\nDescription: An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.\nPrimaryURL: https://avd.aquasec.com/nvd/cve-2019-18276\n\nPackage: bash\nVulnerabilityID: TEMP-0841856-B18BAF\nSeverity: LOW\nInstalledVersion: 5.0-4\nPrimaryURL: https://security-tracker.debian.org/tracker/TEMP-0841856-B18BAF\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/image-build-output.spec.js",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport ImageBuildOutputCuller from '@pkg/utils/processOutputInterpreters/image-build-output';\n\ndescribe('image build output', () => {\n  it('returns the raw text back', () => {\n    const buildOutputPath = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'build.txt');\n    const data = fs.readFileSync(buildOutputPath).toString();\n    const culler = new ImageBuildOutputCuller();\n\n    culler.addData(data);\n    expect(culler.getProcessedData()).toBe(data.replace(/\\r/g, ''));\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/image-non-build-output.spec.js",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport ImageNonBuildOutputCuller from '@pkg/utils/processOutputInterpreters/image-non-build-output';\n\ndescribe('simple image output', () => {\n  describe('push', () => {\n    it('culls by SHA', () => {\n      const fname = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'push.txt');\n      const data = fs.readFileSync(fname).toString();\n      const lines = data.split(/(\\r?\\n)/);\n      const culler = new ImageNonBuildOutputCuller();\n\n      expect(lines.length).toBeGreaterThan(6);\n      culler.addData(lines.slice(0, 24).join(''));\n      let processedLines = culler.getProcessedData().split(/\\r?\\n/);\n\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: 0.1 s/);\n\n      culler.addData(lines.slice(24, 48).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(2 * 12 * 2, 10 * 12 * 2).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(10 * 12 * 2, 11 * 12 * 2).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+done/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+done/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+done/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(11 * 12 * 2, 12 * 12 * 2).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+done/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+done/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+done/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+done/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+done/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(12 * 12 * 2, 13 * 12 * 2).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+done/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+done/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+done/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+done/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+done/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+done/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+done/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+done/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+done/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(13 * 12 * 2, 14 * 12 * 2).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^config-sha256:4760d6065fe005e991da592c40c14f58abbbed5167e248336b7f5586aa844068:\\s+done/);\n      expect(processedLines[1]).toMatch(/^layer-sha256:056a5bf54c27d99d6ed420ba6cb481647ee99b9c11faacc02110d37f12edc1cf:\\s+done/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:60dde8851b86f5f7adf602f7f2a4dfe4ab45ba8c979ed91105e62db026ee02a3:\\s+done/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:97e34918dcd1d5a4999f8d084f1aed8b9b981a357cab20391ec352e2bf0a2c78:\\s+done/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:9949d7879153e978338242fe30b7b0c4d3207361a227d49d1969c189b43451e5:\\s+done/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:9aae54b2144e5b2b00c610f8805128f4f86822e1e52d3714c463744a431f0f4a:\\s+done/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9d1f343c69b3579d6f03ab967906427a372d4ac9c921ad6d4d2a288a8be0757d:\\s+done/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:9ef1121d3b90a9befcf2b8ac285e1653eb196a0ce5e8be1320feb09bdb69a967:\\s+done/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:dd3f9c1f5db9ad0120095ff2cef4c467222151487db61f8b9c424ace486e7d04:\\s+done/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:ffed9dad286c82fb74ed76005208eb2195ff464a2619e1484d1d5f6e3538477b:\\s+done/);\n      expect(processedLines[10]).toMatch(/^manifest-sha256:15d001306a2a981e553544aac749ed442cd55de0d889228c0eb083c68bec4f2d:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n    });\n  });\n  describe('pull', () => {\n    it('culls by SHA', () => {\n      const fname = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'pull.txt');\n      const data = fs.readFileSync(fname).toString();\n      const lines = data.split(/(\\r?\\n)/);\n      const culler = new ImageNonBuildOutputCuller();\n\n      expect(lines.length).toBeGreaterThan(6);\n      culler.addData(lines.slice(0, 16).join(''));\n      let processedLines = culler.getProcessedData().split(/\\r?\\n/);\n\n      expect(processedLines.length).toBe(4);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(16, 34).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^layer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(34, 34 + 4 * 2 * 9).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^layer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(34 + 4 * 2 * 9, 34 + 4 * 2 * 9 + 2 * 7).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6:\\s+waiting/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64:\\s+waiting/);\n      expect(processedLines[10]).toMatch(/^layer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172:\\s+waiting/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(34 + 4 * 2 * 9 + 2 * 7, 34 + 4 * 2 * 9 + 4 * 8).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6:\\s+downloading/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60:\\s+waiting/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a:\\s+waiting/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc:\\s+waiting/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64:\\s+downloading/);\n      expect(processedLines[10]).toMatch(/^layer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172:\\s+downloading/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(34 + 4 * 2 * 9 + 4 * 8).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(12);\n      expect(processedLines[0]).toMatch(/^index-sha256:091ee4779c0d90155b6d1a317855ce64714e6485f9db4413c812ddd112df7dc7:\\s+waiting/);\n      expect(processedLines[1]).toMatch(/^manifest-sha256:b3a3389753c2b6d682378051ff775b7122ed3a62d708cc73a52a10421b7c7206:\\s+waiting/);\n      expect(processedLines[2]).toMatch(/^config-sha256:343efcc83bc0172ddd0ab1b2e787cd46712a3dd0551718b978187d8792518375:\\s+waiting/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:3923d444ed0552ce73ef51fa235f1b45edafdec096abda6abab710637dac7ec6:\\s+downloading/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:44718e6d535d365250316b02459f98a1b0fa490158cc53057d18858507504d60:\\s+downloading/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:6d245082de987bb6168e91693b43d7e0a7de48a26f500e42acc30ee3fc8ad58e:\\s+waiting/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:9878c33f813b971dd2ee28563af9275ea845786d8c428ae2abc181f5aecb4c8a:\\s+downloading/);\n      expect(processedLines[7]).toMatch(/^layer-sha256:bd8f6a7501ccbe80b95c82519ed6fd4f7236a41e0ae59ba4a8df76af24629efc:\\s+downloading/);\n      expect(processedLines[8]).toMatch(/^layer-sha256:e95942c4e21d00fe2aa7d8d59d745f53b8ee816795b7315f313b4d9625ec373c:\\s+waiting/);\n      expect(processedLines[9]).toMatch(/^layer-sha256:efe9738af0cb2184ee8f3fb3dcb130455385aa428a27d14e1e07a5587ff16e64:\\s+downloading/);\n      expect(processedLines[10]).toMatch(/^layer-sha256:f37aabde37b87d272286df45e6a3145b0884b72e07e657bf1a2a1e74a92c6172:\\s+downloading/);\n      expect(processedLines[11]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n    });\n  });\n  describe('pull with nerdctl', () => {\n    it('culls by SHA', () => {\n      const fname = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'pull03.txt');\n      const data = fs.readFileSync(fname).toString();\n      const lines = data.split(/(\\r?\\n)/);\n      const culler = new ImageNonBuildOutputCuller();\n\n      expect(lines.length).toBeGreaterThan(6);\n      culler.addData(lines.slice(0, 16).join(''));\n      let processedLines = culler.getProcessedData().split(/\\r?\\n/);\n\n      expect(processedLines.length).toBe(2);\n      expect(processedLines[0]).toMatch(/^docker.io\\/camelpunch\\/pr:latest:\\s+resolving/);\n      expect(processedLines[1]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n\n      culler.addData(lines.slice(16).join(''));\n      processedLines = culler.getProcessedData().split(/\\r?\\n/);\n      expect(processedLines.length).toBe(9);\n      expect(processedLines[0]).toMatch(/^manifest-sha256:f6b002c6f990cdc3fa37d72758c07eac19474062616c14abf16bf3dbd8774387:\\s+\\w+/);\n      expect(processedLines[1]).toMatch(/^config-sha256:f1c8c98faff0d97b3db8bffef6ea2ba46adacb931c7546a81eec4a25264fefc6:\\s+\\w+/);\n      expect(processedLines[2]).toMatch(/^layer-sha256:37c312f1a2a16f5f3bb8ee3c1675c5a880d88455004bc0c6559cf492a3c036b4:\\s+\\w+/);\n      expect(processedLines[3]).toMatch(/^layer-sha256:e110a4a1794126ef308a49f2d65785af2f25538f06700721aad8283b81fdfa58:\\s+\\w+/);\n      expect(processedLines[4]).toMatch(/^layer-sha256:923daccf3632d196d3835182d8a2ab0dad87cad52facb11fb68867c68058a590:\\s+\\w+/);\n      expect(processedLines[5]).toMatch(/^layer-sha256:cc10bd68fc4e1f492195886d2379cfba5ca38648908c05bd2a51bbeaf2d76fd4:\\s+\\w+/);\n      expect(processedLines[6]).toMatch(/^layer-sha256:aa88609bf330d1483a52ae369ce88bdfa51aa264adf0814c2853d4f9860d5387:\\s+\\w+/);\n      expect(processedLines[7]).toMatch(/^\\s*docker.io\\/camelpunch\\/pr:latest:\\s+resolved/);\n      expect(processedLines[8]).toMatch(/^\\s*elapsed: (?:\\d*\\.)?\\d+\\s*s/);\n    });\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/trivy-image-output.spec.js",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport TrivyScanImageOutputCuller from '@pkg/utils/processOutputInterpreters/trivy-image-output';\n\ndescribe('trivy image output', () => {\n  it('echoes a zero-vul image back', () => {\n    const inputPath = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-metric-server-input.txt');\n    const outputPath = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-metric-server-output.txt');\n    const inputData = fs.readFileSync(inputPath).toString();\n    const expectedOutputData = fs.readFileSync(outputPath).toString().replace(/\\r/g, '');\n    const culler = new TrivyScanImageOutputCuller();\n\n    culler.addData(inputData);\n    const processedData = culler.getProcessedData();\n\n    expect(expectedOutputData).toEqual(processedData);\n  });\n\n  it('converts lines to records and handles inherited cells', () => {\n    const inputPath = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-postgres-input.txt');\n    const outputPath = path.join('./pkg/rancher-desktop/utils/processOutputInterpreters/__tests__/assets', 'trivy-image-postgres-output.txt');\n    const inputData = fs.readFileSync(inputPath).toString();\n    const expectedOutputData = fs.readFileSync(outputPath).toString().replace(/\\r/g, '');\n    const culler = new TrivyScanImageOutputCuller();\n\n    culler.addData(inputData);\n    const processedData = culler.getProcessedData();\n\n    expect(expectedOutputData).toEqual(processedData);\n  });\n});\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/image-build-output.ts",
    "content": "const LineSplitter = /\\r?\\n/;\n\nexport default class ImageBuildOutputCuller {\n  lines: string[];\n\n  constructor() {\n    this.lines = [];\n  }\n\n  addData(data: string): void {\n    // TODO (possibly): Deal with partial final lines - I haven't seen this happen yet\n    const lines = data.split(LineSplitter);\n\n    this.lines.push(...lines);\n  }\n\n  getProcessedData(): string {\n    return this.lines.join('\\n');\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/image-non-build-output.ts",
    "content": "const LineSplitter = /\\r?\\n/;\nconst ShaLineMatcher = /^[-\\w]+-sha256:(\\w+):\\s*\\w+\\s*\\|.*?\\|/;\n// this line appears only in nerdctl output for pull commands:\nconst SummaryLine1Matcher = /:\\s*resolv(?:ing|ed)\\s*\\|/;\n// this line appears in both containerd/buildkit and nerdctl pull output\nconst SummaryLine2Matcher = /^elapsed:.*total:/;\n\nexport default class ImageNonBuildOutputCuller {\n  buffering:    boolean;\n  lines:        string[];\n  summaryLine1: string;\n  summaryLine2: string;\n\n  constructor() {\n    this.buffering = true;\n    this.lines = [];\n    this.summaryLine1 = '';\n    this.summaryLine2 = '';\n  }\n\n  addData(data: string) {\n    // TODO (possibly): Deal with partial final lines - I haven't seen this happen yet\n    const lines = data.split(LineSplitter);\n\n    for (const rawLine of lines) {\n      /* eslint-disable-next-line no-control-regex */\n      const line = rawLine.replace(/\\x1B\\[[\\d;,.]*[a-zA-Z]\\r?/g, '');\n\n      if (!this.buffering) {\n        this.lines.push(line);\n      } else if (SummaryLine1Matcher.test(line)) {\n        this.summaryLine1 = line;\n      } else if (SummaryLine2Matcher.test(line)) {\n        this.summaryLine2 = line;\n      } else if (/^\\s*$/.test(line)) {\n        // do nothing\n      } else {\n        const m = ShaLineMatcher.exec(line);\n\n        if (m) {\n          const idx = this.lines.findIndex(elt => elt.includes(m[1]));\n          const strippedLine = line.replace(/\\[\\d+m/g, '');\n\n          if (idx === -1) {\n            this.lines.push(strippedLine);\n          } else {\n            // Replace an updated line in place\n            this.lines[idx] = strippedLine;\n          }\n        } else {\n          this.buffering = false;\n          if (this.summaryLine1) {\n            this.lines.push(this.summaryLine1);\n            this.summaryLine1 = '';\n          }\n          if (this.summaryLine2) {\n            this.lines.push(this.summaryLine2);\n            this.summaryLine2 = '';\n          }\n          this.lines.push(line);\n        }\n      }\n    }\n  }\n\n  getProcessedData() {\n    const lines = ([] as string[]).concat(this.lines);\n\n    if (this.summaryLine1) {\n      lines.push(this.summaryLine1);\n    }\n    if (this.summaryLine2) {\n      lines.push(this.summaryLine2);\n    }\n\n    return lines.join('\\n');\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/processOutputInterpreters/trivy-image-output.ts",
    "content": "const LineSplitter = /\\r?\\n/;\n// eslint-disable-next-line no-control-regex -- Need to catch ANSI control\nconst logFormat = /^[-\\d]+T[-:.\\d]+Z?\\s+\\x1B\\[\\d+m([A-Z]+)\\x1B\\[\\d+m\\s+(.*)$/;\n\nconst CVEKeys = ['Package', 'VulnerabilityID', 'Severity', 'Title', 'InstalledVersion', 'FixedVersion', 'Description', 'PrimaryURL'];\nconst severityRatings: Record<string, number> = {\n  LOW:      1,\n  MEDIUM:   2,\n  HIGH:     3,\n  CRITICAL: 4,\n  UNKNOWN:  5,\n};\nconst MaxSeverityRating = Math.max(...Object.values(severityRatings));\n\ntype finalVulType = Record<string, string>;\n\nexport default class TrivyScanImageOutputCuller {\n  prelimLines: string[];\n  JSONLines:   string[];\n  inJSON = false;\n\n  constructor() {\n    this.prelimLines = [];\n    this.JSONLines = [];\n  }\n\n  getRating(key: string) {\n    return key in severityRatings ? severityRatings[key] : MaxSeverityRating;\n  }\n\n  fixLines(lines: string[]) {\n    // \"key\": \"value with an escaped \\' single quote isn't valid json\"\n    return lines.map(line => line.replace(/\\\\'/g, \"'\"));\n  }\n\n  addData(data: string): void {\n    if (this.inJSON) {\n      this.JSONLines.push(data.replace(/\\\\'/g, \"'\"));\n\n      return;\n    }\n    const lines = data.split(LineSplitter);\n    const jsonStartIndex = lines.indexOf('[');\n\n    if (jsonStartIndex >= 0) {\n      this.prelimLines = this.prelimLines.concat(lines.slice(0, jsonStartIndex));\n      this.inJSON = true;\n      this.JSONLines = this.fixLines(lines.slice(jsonStartIndex));\n    } else {\n      this.prelimLines = this.prelimLines.concat(lines);\n    }\n  }\n\n  getProcessedData() {\n    const prelimLines = this.prelimLines.map(line => line.replace(logFormat, '$1 $2'));\n\n    if (!this.inJSON) {\n      // No JSON, just so return the lines we have\n      return prelimLines.join('\\n');\n    }\n    let core;\n\n    try {\n      core = JSON.parse(this.JSONLines.join(''));\n    } catch (e) {\n      console.log(`Error json parsing ${ this.JSONLines.join('') }`);\n\n      return prelimLines.join('\\n');\n    }\n    const detailLines: string[] = [];\n\n    core.forEach((targetWithVuls: Record<string, any>) => {\n      const target = targetWithVuls['Target'];\n      const sourceVulnerabilities = targetWithVuls['Vulnerabilities'];\n\n      if (!sourceVulnerabilities.length) {\n        return;\n      }\n      detailLines.push(`Target: ${ target }`, '');\n\n      const processedVulnerabilities: finalVulType[] = sourceVulnerabilities.map((v: any) => {\n        const record: finalVulType = {};\n\n        CVEKeys.forEach((key) => {\n          if (v[key]) {\n            record[key] = v[key];\n          }\n        });\n\n        return record;\n      });\n\n      processedVulnerabilities.sort();\n      processedVulnerabilities.sort((a, b) => {\n        return this.getRating(b['Severity']) - this.getRating(a['Severity']);\n      });\n\n      processedVulnerabilities.forEach((vul) => {\n        CVEKeys.forEach((key) => {\n          if (key in vul) {\n            detailLines.push(`${ key }: ${ vul[key] }`);\n          }\n        });\n        detailLines.push('');\n      });\n    });\n\n    return prelimLines.concat(detailLines).join('\\n');\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/protocols.ts",
    "content": "import path from 'path';\nimport { URL, pathToFileURL } from 'url';\n\nimport Electron from 'electron';\n\nimport mainEvents from '@pkg/main/mainEvents';\nimport { isDevBuild } from '@pkg/utils/environment';\nimport Latch from '@pkg/utils/latch';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\n\nconst console = Logging['protocol-handler'];\n\n/**\n * Create a URL that consists of a base combined with the provided path\n * @param relPath The destination path for the requested resource\n * @returns A URL that consists of the combined base (http://localhost:8888)\n * and provided path\n */\nfunction redirectedUrl(relPath: string) {\n  if (isDevBuild) {\n    return `http://localhost:8888${ relPath }`;\n  }\n  if (Electron.app.isPackaged) {\n    return path.join(Electron.app.getAppPath(), 'dist', 'app', relPath);\n  }\n\n  // Unpackaged non-dev build; this normally means E2E tests, where\n  // `app.getAppPath()` is `.../dist/app.\n  return path.join(process.cwd(), 'dist', 'app', relPath);\n}\n\n// Latch that is set when the app:// protocol handler has been registered.\n// This is used to ensure that we don't attempt to open the window before we've\n// done that, when the user attempts to open a second instance of the window.\nexport const protocolsRegistered = Latch();\n\n/**\n * Set up protocol handler for app://\n * This is needed because in packaged builds we'll not be allowed to access\n * file:// URLs for our resources. Use the same app:// protocol for both dev and\n * production environments.\n */\nfunction setupAppProtocolHandler() {\n  Electron.protocol.handle(\n    'app',\n    (request) => {\n      const relPath = new URL(request.url).pathname;\n      const redirectUrl = redirectedUrl(relPath);\n\n      if (isDevBuild) {\n        return Electron.net.fetch(redirectUrl);\n      }\n\n      return Electron.net.fetch(pathToFileURL(redirectUrl).toString());\n    });\n}\n\n/**\n * Set up protocol handler for x-rd-extension://\n *\n * This handler is used for extensions; the format is:\n * x-rd-extension://<extension id>/...\n * Where the extension id is the extension image id, hex encoded (to avoid\n * issues with slashes).  Base64 was not available in Vue.\n * @param partition The Electron session partition name; if unset, set it up for\n *                  the default session.\n */\nfunction setupExtensionProtocolHandler(partition?: string): Promise<void> {\n  const scheme = 'x-rd-extension';\n  const session = partition ? Electron.session.fromPartition(partition) : Electron.session.defaultSession;\n\n  if (!session.protocol.isProtocolHandled(scheme)) {\n    session.protocol.handle(\n      scheme,\n      (request) => {\n        const url = new URL(request.url);\n        // Re-encoding the extension ID here also ensures it doesn't contain any\n        // directory traversal etc. issues.\n        const extensionID = Buffer.from(url.hostname, 'hex').toString('base64url');\n        const resourcePath = path.normalize(url.pathname);\n        const filepath = path.join(paths.extensionRoot, extensionID, resourcePath);\n\n        return Electron.net.fetch(pathToFileURL(filepath).toString());\n      });\n  }\n\n  return Promise.resolve();\n}\n\nexport function setupProtocolHandlers() {\n  try {\n    setupAppProtocolHandler();\n    setupExtensionProtocolHandler();\n    mainEvents.handle('extensions/register-protocol', setupExtensionProtocolHandler);\n\n    protocolsRegistered.resolve();\n  } catch (ex) {\n    console.error('Error registering protocol handlers:', ex);\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/resources.ts",
    "content": "import path from 'path';\n\nimport memoize from 'lodash/memoize';\n\nimport paths from '@pkg/utils/paths';\n\n/**\n * executableMap is a mapping of valid executable names and their path.\n * If the value is `undefined`, then it's assumed to be an executable in the\n * user-accessible `bin` directory.\n * Otherwise, it's an array containing the path to the executable.\n */\nconst executableMap: Record<string, string[] | undefined> = {\n  docker:             undefined,\n  kubectl:            undefined,\n  nerdctl:            undefined,\n  rdctl:              undefined,\n  spin:               undefined,\n  'setup-spin':       [paths.resources, 'setup-spin'],\n  'wsl-helper':       [paths.resources, process.platform, 'internal', platformBinary('wsl-helper')],\n  'wsl-helper-linux': [paths.resources, 'linux', 'internal', 'wsl-helper'],\n};\n\nfunction platformBinary(name: string): string {\n  return process.platform === 'win32' ? `${ name }.exe` : name;\n}\n\n/**\n * Gets the absolute path to an executable. Adds \".exe\" to the end\n * if running on Windows.\n * @param name The name of the binary, without file extension.\n */\nfunction _executable(name: keyof typeof executableMap): string {\n  const parts = executableMap[name];\n\n  if (parts === undefined) {\n    return path.join(paths.resources, process.platform, 'bin', platformBinary(name));\n  }\n\n  return path.join(...parts);\n}\nexport const executable = memoize(_executable);\n\nexport default { executable };\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/safeRename.ts",
    "content": "import fs from 'fs';\n\nimport fsExtra from 'fs-extra';\n\nconst fsPromises = fs.promises;\n\n/**\n * Normally we can use `fs.rename` to relocate (and rename) both files and directories.\n * But there is a known limitation that on Windows systems `fs.rename` fails when the\n * source and destination are on different drives. Same for different volumes on Unix-based systems.\n * So if `fs.rename` fails, this function does a `copy` and `delete` instead.\n *\n * The `safe` in `safeRename` is because using this function for existing arguments should not throw\n * an exception.\n *\n * @param srcPath: string\n * @param destPath: string\n */\nexport default async function safeRename(srcPath: string, destPath: string): Promise<void> {\n  try {\n    await fsPromises.rename(srcPath, destPath);\n  } catch (e) {\n    // https://github.com/nodejs/node/issues/19077 :\n    // rename uses hardlinks, fails cross-devices: marked 'wontfix'\n    if ((await fsPromises.stat(srcPath)).isDirectory()) {\n      // https://github.com/jprichardson/node-fs-extra/blob/HEAD/docs/copy.md\n      // \"Note that if src is a directory it will copy everything inside of this directory, not the entire directory itself\"\n      // https://github.com/jprichardson/node-fs-extra/issues/537\n      // This is exactly what we want.\n\n      await fsExtra.copy(srcPath, destPath);\n      await fsPromises.rm(srcPath, {\n        recursive: true, force: true, maxRetries: 2,\n      });\n    } else {\n      await fsPromises.copyFile(srcPath, destPath);\n      await fsPromises.unlink(srcPath);\n    }\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/select.js",
    "content": "export function onClickOption(option, e) {\n  if (!this.$attrs.multiple) {\n    return;\n  }\n\n  const getValue = (opt) => (this.optionKey ? this.get(opt, this.optionKey) : this.getOptionLabel(opt));\n  const optionValue = getValue(option);\n  const value = this.value || [];\n  const optionIndex = value.findIndex((option) => getValue(option) === optionValue);\n\n  if (optionIndex < 0) {\n    return;\n  }\n\n  this.value.splice(optionIndex, 1);\n\n  this.$emit('update:value', this.value);\n  e.preventDefault();\n  e.stopPropagation();\n\n  if (this.closeOnSelect) {\n    this.$refs['select-input'].closeSearchOptions();\n  }\n}\n\n// This is a simpler positioner for the dropdown for a select control\n// We used to use popper for these, but it does not support fractional pixel placements which\n// means the dropdown does not appear aligned to the control when placed in a column-based layout\nexport function calculatePosition(dropdownList, component, width, placement) {\n  const selectEl = component.$parent.$el;\n  const r = selectEl.getBoundingClientRect();\n  const p = placement || 'bottom-start';\n  const docHeight = document.body.offsetHeight;\n  const bottom = docHeight - window.scrollY - r.y - 1;\n  let top;\n\n  // If placement is not at the top, then position if underneath\n  if (!p.includes('top')) {\n    // Position is bottom\n    top = r.y + r.height - 1;\n\n    // Check to see if the dropdown would fall off the screen, if so, try putting it above\n    const end = top + dropdownList.offsetHeight;\n\n    if (end > window.innerHeight) {\n      top = undefined;\n    } else {\n      top += window.scrollY;\n    }\n  }\n\n  if (!top) {\n    dropdownList.style.bottom = `${ bottom }px`;\n    dropdownList.classList.add('vs__dropdown-up');\n    selectEl.classList.add('vs__dropdown-up');\n  } else {\n    dropdownList.style.top = `${ top }px`;\n    dropdownList.classList.remove('vs__dropdown-up');\n    selectEl.classList.remove('vs__dropdown-up');\n  }\n\n  dropdownList.style.left = `${ r.x }px`;\n  dropdownList.style.width = 'min-content';\n  dropdownList.style.minWidth = `${ r.width }px`;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/shortcuts.ts",
    "content": "import os from 'os';\n\nimport { BrowserWindow } from 'electron';\n\nimport { toArray } from '@pkg/utils/array';\nimport Logging from '@pkg/utils/logging';\n\nconst log = Logging.shortcuts;\n\nconst Shortcut = {\n  key: (shortcut: Shortcut | Electron.Input) => {\n    const keyName = shortcut.key.toString();\n\n    return [\n      shortcut.meta ? 'Cmd+' : '',\n      shortcut.control ? 'Ctrl+' : '',\n      shortcut.shift ? 'Shift+' : '',\n      shortcut.alt ? 'Alt+' : '',\n      keyName.length === 1 ? keyName.toUpperCase() : keyName,\n    ].join('');\n  },\n};\n\nexport interface Shortcut {\n  platform?: NodeJS.Platform | NodeJS.Platform[];\n  meta?:     boolean;\n  shift?:    boolean;\n  control?:  boolean;\n  alt?:      boolean;\n  key:       string | number;\n}\n\ninterface ShortcutExt extends Shortcut {\n  callback: () => void;\n}\n\nfunction matchPlatform(shortcut: Shortcut): boolean {\n  if (shortcut.platform) {\n    return toArray(shortcut.platform).includes(os.platform());\n  }\n\n  return true;\n}\n\nexport const CommandOrControl = {\n  meta:    os.platform() === 'darwin' ? true : undefined,\n  control: os.platform() !== 'darwin' ? true : undefined,\n};\n\nclass WindowShortcuts {\n  private window:    BrowserWindow;\n  private shortcuts: Record<string, ShortcutExt> = {};\n\n  constructor(window: BrowserWindow) {\n    this.window = window;\n    this.addListener();\n  }\n\n  private inputCallback = (event: Electron.Event, input: Electron.Input) => {\n    const key = Shortcut.key(input);\n\n    if (this.shortcuts[key]) {\n      this.shortcuts[key].callback();\n      event.preventDefault();\n    }\n  };\n\n  get shortcutsList() {\n    return Object.values(this.shortcuts);\n  }\n\n  addShortcut(shortcut: ShortcutExt) {\n    const key = Shortcut.key(shortcut);\n\n    if (this.shortcuts[key]) {\n      log.warn(`window [${ this.window.id }] - key [${ key }] is already registered; skip.`);\n\n      return;\n    }\n\n    this.shortcuts[key] = shortcut;\n\n    log.info(`add: window [${ this.window.id }] - key [${ key }]`);\n  }\n\n  removeShortcut(shortcut: Shortcut) {\n    const key = Shortcut.key(shortcut);\n\n    if (!this.shortcuts[key]) {\n      log.warn(`window [${ this.window.id }] - key [${ key }] is not registered; skip.`);\n\n      return;\n    }\n\n    delete this.shortcuts[key];\n\n    log.info(`remove: window [${ this.window.id }] - key [${ key }]`);\n  }\n\n  addListener() {\n    this.window.webContents.on('before-input-event', this.inputCallback);\n  }\n\n  removeListener() {\n    this.window.webContents.off('before-input-event', this.inputCallback);\n  }\n}\n\nclass ShortcutsImpl {\n  private windows: Record<number, WindowShortcuts> = {};\n\n  /**\n   *\n   * @param window where the shortcuts takes effect, if it focused\n   * @param _shortcuts definition of the shortcut, check Shortcut type\n   * @param description\n   * @param callback\n   * @returns void\n   */\n  register(window: BrowserWindow, _shortcuts: Shortcut | Shortcut[], callback: () => void, description?: string) {\n    const id = window?.id;\n    const shortcuts = toArray(_shortcuts);\n\n    if (id === undefined) {\n      log.warn('window is undefined; skip.');\n\n      return;\n    }\n\n    if (shortcuts.length === 0) {\n      log.warn('key definition is empty; skip.');\n\n      return;\n    }\n\n    if (description) {\n      log.info(`register: window [${ id }] - [${ description }]`);\n    }\n\n    if (!this.windows[id]) {\n      this.windows[id] = new WindowShortcuts(window);\n\n      window.on('close', () => {\n        delete this.windows[id];\n      });\n    }\n\n    shortcuts.forEach((s) => {\n      if (matchPlatform(s)) {\n        this.windows[id].addShortcut({\n          ...s,\n          callback,\n        });\n      }\n    });\n  }\n\n  /**\n   *\n   * @param window where the shortcuts is registered\n   * @param _shortcuts shortcuts to be unregistered\n   * @param description\n   * @returns\n   */\n  unregister(window: BrowserWindow, _shortcuts?: Shortcut | Shortcut[], description?: string) {\n    const id = window?.id;\n    const shortcuts: Shortcut[] = _shortcuts ? toArray(_shortcuts) : [];\n\n    if (id === undefined) {\n      log.warn('window is undefined; skip.');\n\n      return;\n    }\n\n    if (description) {\n      log.info(`unregister: window [${ id }] - [${ description }]`);\n    }\n\n    shortcuts.forEach((s) => {\n      if (matchPlatform(s)) {\n        this.windows[id].removeShortcut(s);\n      }\n    });\n\n    if (shortcuts.length === 0 || this.windows[id].shortcutsList.length === 0) {\n      this.windows[id].removeListener();\n      delete this.windows[id];\n\n      log.info(`window [${ id }] - all keys removed`);\n    }\n  }\n\n  isRegistered(window: BrowserWindow): boolean {\n    const id = window?.id;\n\n    if (id === undefined) {\n      log.warn('window is undefined; skip.');\n\n      return false;\n    }\n\n    return !!this.windows[id];\n  }\n}\n\nexport const Shortcuts = new ShortcutsImpl();\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/sort.js",
    "content": "import { get } from './object';\nimport { strPad } from './string';\n\n// Based on https://github.com/emberjs/ember.js/blob/master/packages/@ember/-internals/runtime/lib/type-of.js\n// and      https://github.com/emberjs/ember.js/blob/master/packages/@ember/-internals/runtime/lib/mixins/array.js\n/*\nCopyright (c) 2019 Yehuda Katz, Tom Dale and Ember.js contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies\nof the Software, and to permit persons to whom the Software is furnished to do\nso, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n*/\n\n// ........................................\n// TYPING & ARRAY MESSAGING\n//\nconst TYPE_MAP = {\n  '[object Boolean]':  'boolean',\n  '[object Number]':   'number',\n  '[object String]':   'string',\n  '[object Function]': 'function',\n  '[object Array]':    'array',\n  '[object Date]':     'date',\n  '[object RegExp]':   'regexp',\n  '[object Object]':   'object',\n  '[object FileList]': 'filelist',\n};\n\nconst { toString } = Object.prototype;\n\n/**\n  Returns a consistent type for the passed object.\n\n  Use this instead of the built-in `typeof` to get the type of an item.\n  It will return the same result across all browsers and includes a bit\n  more detail. Here is what will be returned:\n\n      | Return Value  | Meaning                                              |\n      |---------------|------------------------------------------------------|\n      | 'string'      | String primitive or String object.                   |\n      | 'number'      | Number primitive or Number object.                   |\n      | 'boolean'     | Boolean primitive or Boolean object.                 |\n      | 'null'        | Null value                                           |\n      | 'undefined'   | Undefined value                                      |\n      | 'function'    | A function                                           |\n      | 'array'       | An instance of Array                                 |\n      | 'regexp'      | An instance of RegExp                                |\n      | 'date'        | An instance of Date                                  |\n      | 'filelist'    | An instance of FileList                              |\n      | 'error'       | An instance of the Error object                      |\n      | 'object'      | A JavaScript object                                  |\n\n  Examples:\n\n  import { typeOf } from '@shell/utils/type-of';\n\n  typeOf();                       // 'undefined'\n  typeOf(null);                   // 'null'\n  typeOf(undefined);              // 'undefined'\n  typeOf('michael');              // 'string'\n  typeOf(new String('michael'));  // 'string'\n  typeOf(101);                    // 'number'\n  typeOf(new Number(101));        // 'number'\n  typeOf(true);                   // 'boolean'\n  typeOf(new Boolean(true));      // 'boolean'\n  typeOf(A);                      // 'function'\n  typeOf([1, 2, 90]);             // 'array'\n  typeOf(/abc/);                  // 'regexp'\n  typeOf(new Date());             // 'date'\n  typeOf(event.target.files);     // 'filelist'\n  typeOf(new Error('teamocil'));  // 'error'\n\n  // 'normal' JavaScript object\n  typeOf({ a: 'b' });             // 'object'\n*/\nexport function typeOf(item) {\n  if (item === null) {\n    return 'null';\n  }\n  if (item === undefined) {\n    return 'undefined';\n  }\n  let ret = TYPE_MAP[toString.call(item)] || 'object';\n\n  if (ret === 'object') {\n    if (item instanceof Error) {\n      ret = 'error';\n    } else if (item instanceof Date) {\n      ret = 'date';\n    }\n  }\n\n  return ret;\n}\n\nexport function spaceship(a, b) {\n  const diff = a - b;\n\n  return (diff > 0) - (diff < 0);\n}\n\nconst TYPE_ORDER = {\n  undefined: 0,\n  null:      1,\n  boolean:   2,\n  number:    3,\n  string:    4,\n  array:     5,\n  object:    6,\n  instance:  7,\n  function:  8,\n  class:     9,\n  date:      10,\n};\n\nexport function compare(a, b) {\n  const typeA = typeOf(a);\n  const typeB = typeOf(b);\n\n  const res = spaceship(TYPE_ORDER[typeA], TYPE_ORDER[typeB]);\n\n  if ( res ) {\n    return res;\n  }\n\n  switch (typeA) {\n  case 'boolean':\n  case 'number':\n    return spaceship(a, b);\n\n  case 'string':\n    return spaceship(a.localeCompare(b), 0);\n\n  case 'array': {\n    const aLen = a.length;\n    const bLen = b.length;\n    const len = Math.min(aLen, bLen);\n\n    for (let i = 0; i < len; i++) {\n      const r = compare(a[i], b[i]);\n\n      if (r !== 0) {\n        return r;\n      }\n    }\n\n    // all elements are equal now\n    // shorter array should be ordered first\n    return spaceship(aLen, bLen);\n  }\n  case 'date':\n    return spaceship(a.getTime(), b.getTime());\n  }\n\n  return 0;\n}\n\nexport function parseField(str) {\n  const parts = str.split(/:/);\n\n  if ( parts.length === 2 && parts[1] === 'desc' ) {\n    return { field: parts[0], reverse: true };\n  } else {\n    return { field: str, reverse: false };\n  }\n}\n\nexport function sortBy(ary, keys, desc) {\n  if ( !Array.isArray(keys) ) {\n    keys = [keys];\n  }\n\n  return (ary || []).slice().sort((objA, objB) => {\n    for ( let i = 0; i < keys.length; i++ ) {\n      const parsed = parseField(keys[i]);\n      const a = get(objA, parsed.field);\n      const b = get(objB, parsed.field);\n      let res = compare(a, b);\n\n      if ( res ) {\n        if ( desc ) {\n          res *= -1;\n        }\n\n        if ( parsed.reverse ) {\n          res *= -1;\n        }\n\n        return res;\n      }\n    }\n\n    return 0;\n  });\n}\n\n// Turn foo1-bar2 into foo0000000001-bar0000000002 so that the numbers sort numerically\nconst splitRegex = /([^\\d]+)/;\nconst notNumericRegex = /^[0-9]+$/;\n\nexport function sortableNumericSuffix(str) {\n  if ( typeof str !== 'string' ) {\n    return str;\n  }\n\n  return str.split(splitRegex).map((x) => x.match(notNumericRegex) ? strPad(x, 10, '0') : x).join('').trim();\n}\n\nexport function isNumeric(num) {\n  return !!`${ num }`.match(notNumericRegex);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/string-encode.ts",
    "content": "export const hexEncode = (str: string): string => Array.from(str)\n  .map(c => `0${ c.charCodeAt(0).toString(16) }`.slice(-2))\n  .join('');\n\nexport const hexDecode = (hexString: string): string | undefined => hexString.match(/.{1,2}/g)\n  ?.map(hex => String.fromCharCode(parseInt(hex, 16)))\n  .join('');\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/string.js",
    "content": "export function camelToTitle(str) {\n  return dasherize((str || '')).split('-').map((str) => {\n    return ucFirst(str);\n  }).join(' ');\n}\n\nexport function ucFirst(str) {\n  str = str || '';\n\n  return str.substr(0, 1).toUpperCase() + str.substr(1);\n}\n\nexport function lcFirst(str) {\n  str = str || '';\n\n  return str.substr(0, 1).toLowerCase() + str.substr(1);\n}\n\nexport function strPad(str, toLength, padChars = ' ', right = false) {\n  str = `${ str }`;\n\n  if (str.length >= toLength) {\n    return str;\n  }\n\n  const neededLen = toLength - str.length + 1;\n  const padStr = (new Array(neededLen)).join(padChars).substr(0, neededLen);\n\n  if (right) {\n    return str + padStr;\n  } else {\n    return padStr + str;\n  }\n}\n\n// Turn thing1 into thing00000001 so that the numbers sort numerically\nexport function sortableNumericSuffix(str) {\n  str = str || '';\n  const match = str.match(/^(.*[^0-9])([0-9]+)$/);\n\n  if (match) {\n    return match[1] + strPad(match[2], 8, '0');\n  }\n\n  return str;\n}\n\nconst entityMap = {\n  '&': '&amp;',\n  '<': '&lt;',\n  '>': '&gt;',\n  '\"': '&quot;',\n  \"'\": '&#39;',\n  '/': '&#x2F;',\n};\n\nexport function escapeHtml(html) {\n  return String(html).replace(/[&<>\"']/g, (s) => {\n    return entityMap[s];\n  });\n}\n\n/**\n * Return HTML markup from escaped HTML string, allowing specific tags\n * @param text string\n * @returns string\n */\nexport function decodeHtml(text) {\n  const div = document.createElement('div');\n\n  div.innerHTML = text;\n\n  return div.textContent || div.innerText || '';\n}\n\nexport function escapeRegex(string) {\n  return string.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'); // $& means the whole matched string\n}\n\nexport function random32(count) {\n  count = Math.max(0, count || 1);\n\n  const out = [];\n  let i;\n\n  if (window.crypto && window.crypto.getRandomValues) {\n    const tmp = new Uint32Array(count);\n\n    window.crypto.getRandomValues(tmp);\n    for (i = 0; i < tmp.length; i++) {\n      out[i] = tmp[i];\n    }\n  } else {\n    for (i = 0; i < count; i++) {\n      out[i] = Math.random() * 4294967296; // Math.pow(2,32);\n    }\n  }\n\n  if (count === 1) {\n    return out[0];\n  } else {\n    return out;\n  }\n}\n\nconst alpha = 'abcdefghijklmnopqrstuvwxyz';\nconst num = '0123456789';\nconst sym = '!@#$%^&*()_+-=[]{};:,./<>?|';\n\nexport const CHARSET = {\n  NUMERIC:     num,\n  NO_VOWELS:   'bcdfghjklmnpqrstvwxz2456789',\n  ALPHA:       alpha + alpha.toUpperCase(),\n  ALPHA_NUM:   alpha + alpha.toUpperCase() + num,\n  ALPHA_LOWER: alpha,\n  ALPHA_UPPER: alpha.toUpperCase(),\n  HEX:         `${ num }ABCDEF`,\n  PASSWORD:    alpha + alpha.toUpperCase() + num + alpha + alpha.toUpperCase() + num + sym,\n  // ^-- includes alpha / ALPHA / num twice to reduce the occurrence of symbols\n};\n\nexport function randomStr(length = 16, chars = CHARSET.ALPHA_NUM) {\n  if (!chars || !chars.length) {\n    return null;\n  }\n\n  return random32(length).map((val) => {\n    return chars[val % chars.length];\n  }).join('');\n}\n\nexport function formatPercent(value, maxPrecision = 2) {\n  if (value < 1 && maxPrecision >= 2) {\n    return `${ Math.round(value * 100) / 100 }%`;\n  } else if (value < 10 && maxPrecision >= 1) {\n    return `${ Math.round(value * 10) / 10 }%`;\n  } else {\n    return `${ Math.round(value) }%`;\n  }\n}\n\nexport function pluralize(str) {\n  if ( str.match(/.*[^aeiou]y$/i) ) {\n    return `${ str.substr(0, str.length - 1) }ies`;\n  } else if ( str.endsWith('ics') ) {\n    return str;\n  } else if ( str.endsWith('s') ) {\n    return `${ str }es`;\n  } else {\n    return `${ str }s`;\n  }\n}\n\nexport function resourceNames(names, plusMore, t) {\n  return names.reduce((res, name, i) => {\n    if (i >= 5) {\n      return res;\n    }\n    res += `<b>${ escapeHtml( name ) }</b>`;\n    if (i === names.length - 1) {\n      res += plusMore;\n    } else {\n      res += i === names.length - 2 ? t('generic.and') : t('generic.comma');\n    }\n\n    return res;\n  }, '');\n}\n\nexport function indent(lines, count = 2, token = ' ', afterRegex = null) {\n  if (typeof lines === 'string') {\n    lines = lines.split(/\\n/);\n  } else {\n    lines = lines || [];\n  }\n\n  const padStr = (new Array(count + 1)).join(token);\n\n  const out = lines.map((line) => {\n    let prefix = '';\n    let suffix = line;\n\n    if (afterRegex) {\n      const match = line.match(afterRegex);\n\n      if (match) {\n        prefix = match[match.length - 1];\n        suffix = line.substr(match[0].length);\n      }\n    }\n\n    return `${ prefix }${ padStr }${ suffix }`;\n  });\n\n  const str = out.join('\\n');\n\n  return str;\n}\n\nconst decamelizeRegex = /([a-z\\d])([A-Z])/g;\n\nexport function decamelize(str) {\n  return str.replace(decamelizeRegex, '$1_$2').toLowerCase();\n}\n\nconst dasherizeRegex = /[ _]/g;\n\nexport function dasherize(str) {\n  return decamelize(str).replace(dasherizeRegex, '-');\n}\n\nexport function asciiLike(str) {\n  str = str || '';\n\n  if ( str.match(/[^\\r\\n\\t\\x20-\\x7F]/) ) {\n    return false;\n  }\n\n  return true;\n}\n\nexport function coerceStringTypeToScalarType(val, type) {\n  if ( type === 'float' ) {\n    // Coerce strings to floats\n    val = parseFloat(val) || null; // NaN becomes null\n  } else if ( type === 'int' ) {\n    // Coerce strings to ints\n    val = parseInt(val, 10);\n\n    if ( isNaN(val) ) {\n      val = null;\n    }\n  } else if ( type === 'boolean') {\n    // Coerce strings to boolean\n    if (val.toLowerCase() === 'true') {\n      val = true;\n    } else if (val.toLowerCase() === 'false') {\n      val = false;\n    }\n  }\n\n  return val;\n}\n\nexport function matchesSomeRegex(stringRaw, regexes = []) {\n  return regexes.some((regexRaw) => {\n    const string = stringRaw || '';\n    const regex = ensureRegex(regexRaw);\n\n    return string.match(regex);\n  });\n}\n\nexport function ensureRegex(strOrRegex, exact = true) {\n  if ( typeof strOrRegex === 'string' ) {\n    if ( exact ) {\n      return new RegExp(`^${ escapeRegex(strOrRegex) }$`, 'i');\n    } else {\n      return new RegExp(`${ escapeRegex(strOrRegex) }`, 'i');\n    }\n  }\n\n  return strOrRegex;\n}\n\nexport function nlToBr(value) {\n  return escapeHtml(value || '').replace(/(\\r\\n|\\r|\\n)/g, '<br/>\\n');\n}\n\nconst quotedMatch = /[^.\"']+|\"([^\"]*)\"|'([^']*)'/g;\n\nexport function splitObjectPath(path) {\n  if ( path.includes('\"') || path.includes(\"'\") ) {\n    // Path with quoted section\n    return path.match(quotedMatch).map((x) => x.replace(/['\"]/g, ''));\n  }\n\n  // Regular path\n  return path.split('.');\n}\n\nexport function joinObjectPath(ary) {\n  let out = '';\n\n  for ( const p of ary ) {\n    if ( p.includes('.') ) {\n      out += `.\"${ p }\"`;\n    } else {\n      out += `.${ p }`;\n    }\n  }\n\n  if ( out.startsWith('.') ) {\n    out = out.substr(1);\n  }\n\n  return out;\n}\n\nexport function shortenedImage(image) {\n  return (image || '')\n    .replace(/^(index\\.)?docker.io\\/(library\\/)?/, '')\n    .replace(/:latest$/, '')\n    .replace(/^(.*@sha256:)([0-9a-f]{8})[0-9a-f]+$/i, '$1$2…');\n}\n\nexport function isIpv4(ip) {\n  const reg = /^((25[0-5]|2[0-4]\\d|[01]?\\d\\d?)\\.){3}(25[0-5]|2[0-4]\\d|[01]?\\d\\d?)$/;\n\n  return reg.test(ip);\n}\n\nexport function sanitizeKey(k) {\n  return (k || '').replace(/[^a-z0-9./_-]/ig, '');\n}\n\nexport function sanitizeValue(v) {\n  return (v || '').replace(/[^a-z0-9._-]/ig, '');\n}\n\nexport function sanitizeIP(v) {\n  return (v || '').replace(/[^a-z0-9.:_-]/ig, '');\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/stringify.ts",
    "content": "export function jsonStringifyWithWhiteSpace(obj: Record<string, any>): string {\n  return `${ JSON.stringify(obj, undefined, 2) }\\n`;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/testUtils/mockModules.ts",
    "content": "import path from 'node:path';\n\nimport { jest } from '@jest/globals';\n\nconst defaultOverrides = {\n  '@pkg/utils/logging': (() => {\n    class Log {\n      log = jest.fn();\n      error = jest.fn();\n      info = jest.fn();\n      warn = jest.fn();\n      debug = jest.fn();\n      debugE = jest.fn();\n    }\n    return ({\n      Log,\n      default:    new Proxy({}, {\n        get: (target, prop, receiver) => {\n          return new Log();\n        },\n      }),\n    });\n  })(),\n  electron: {\n    app: {\n      isPackaged: false,\n      getAppPath: () => path.resolve('.'),\n    },\n    BrowserWindow: {},\n    dialog:        {},\n    ipcMain:       {},\n    ipcRenderer:   {},\n    nativeTheme:   {},\n    net:           {\n      fetch: jest.fn<typeof fetch>(() => {\n        return Promise.resolve(new Response());\n      }),\n    },\n    screen:          {},\n    shell:           {},\n    WebContentsView: {},\n  },\n};\n\ntype defaultOutputType = typeof defaultOverrides;\ntype defaultInputType = { [key in keyof defaultOutputType]: undefined };\ntype explicitModuleType = Record<string, any>;\ntype mockModuleParamType = Record<string, explicitModuleType> | Partial<defaultInputType>;\ntype mockModuleReturnType<T extends mockModuleParamType> = {\n  [key in keyof T]:\n  key extends keyof defaultOutputType\n    ? T[key] extends undefined\n      ? defaultOutputType[key]\n      : T[key]\n    : T[key];\n};\n\n/**\n * This is a helper function to mock ES modules.\n * @param modules The modules to mock; the key is the module name (e.g. `os`),\n * and the values are the things to export (e.g. `{arch: jest.fn(() => return '68k'}`).\n * The value may be `undefined`, in which case a default is used.\n * @returns The input, to facilitate working with the mocks.  When the value is\n * `undefined`, it is the default override instead.\n */\nexport default function mockModules<T extends mockModuleParamType>(modules: T): mockModuleReturnType<T> {\n  const results: mockModuleReturnType<T> = {} as any;\n  for (let [name, exports] of Object.entries(modules)) {\n    if (exports === undefined && name in defaultOverrides) {\n      exports = defaultOverrides[name as keyof typeof defaultOverrides];\n    }\n    jest.unstable_mockModule(name, () => ({\n      __esModule: true,\n      default:    exports,\n      ...exports,\n    }));\n    (results as any)[name] = exports;\n  }\n\n  return results;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/testUtils/mockResources.ts",
    "content": "import type { MockInstance } from 'jest-mock/build';\n\n// `Symbol.dispose` exists as of NodeJS 20; if it's unset, set it (because we\n// are currently on NodeJS 18).\n(Symbol as any).dispose ??= Symbol.for('nodejs.dispose');\n\n/**\n * Given a Jest SpyInstance, return it as a Disposable such that mockRestore will\n * be called when the instance goes out of scope.\n * @note This will no longer be needed as of Jest 30 (where it's built in).\n */\nexport function withResource<\n  T extends (...args: any) => any,\n  U extends MockInstance<T>,\n>(input: U): U & Disposable {\n  const impl = input.getMockImplementation();\n  (input as any)[Symbol.dispose] = () => {\n    input.mockRestore();\n    if (impl) {\n      input.mockImplementation(impl);\n    }\n  };\n\n  return input as U & Disposable;\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/testUtils/setupVue.ts",
    "content": "/**\n * This file is preloaded into all Jest tests (see package.json,\n * `jest.setupFiles`) and is used to set up default plugins in Vue.\n */\n\nimport { config } from '@vue/test-utils';\n\nconfig.global.mocks = {\n  t: (key: string) => key,\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/testUtils/vue-jest.js",
    "content": "// This is a transformer for Vue to compile single-file components in jest.\n// @vue/vue3-jest forces CommonJS which breaks with dependencies that are now\n// ESM-only.\n\n// @ts-check\n\nimport crypto from 'crypto';\n\nimport babelJest from 'babel-jest';\nimport typescript from 'typescript';\nimport { compileTemplate, parse } from 'vue/compiler-sfc';\n\n/**\n * @import { SFCDescriptor, SFCParseOptions } from 'vue/compiler-sfc'\n * @import { SyncTransformer, TransformOptions } from '@jest/transform'\n * @import { TransformOptions as BabelTransformOptions } from '@babel/core';\n */\n\n/**\n * @typedef {Object} VueJestTransformOptions\n * @property vue {SFCParseOptions}\n * @property babel {BabelTransformOptions}\n */\n\nconst babelTransformer = (function() {\n  const result = babelJest.createTransformer();\n  if ('then' in result) {\n    throw new Error('babel transformer creation should be synchronous');\n  }\n  return result;\n}());\n\n/** @type (source: string, fileName: string) => string */\nfunction compileTypeScript(source, fileName) {\n  const result = typescript.transpileModule(source, {\n    fileName,\n    compilerOptions: {\n      module: typescript.ModuleKind.ESNext,\n    },\n  });\n\n  return result.outputText;\n}\n\n/** @type (descriptor: SFCDescriptor, options: TransformOptions<VueJestTransformOptions>) => string */\nfunction processScript(descriptor, options) {\n  const { script } = descriptor;\n\n  if (!script) {\n    return '';\n  }\n\n  if (!script.content) {\n    throw new Error(`Script ${ descriptor.filename } has no content`);\n  }\n\n  const isTS = /typescript|^ts/.test(script.lang ?? 'js');\n  let { content } = script;\n\n  if (isTS) {\n    content = compileTypeScript(content, descriptor.filename);\n  }\n\n  return content.replace(/^export default/m, 'const __default__ =');\n}\n\n/** @type (descriptor: SFCDescriptor) => string */\nfunction processTemplate(descriptor) {\n  const { template } = descriptor;\n\n  if (!template) {\n    return '';\n  }\n\n  if (!template.content) {\n    throw new Error(`Template ${ descriptor.filename } does not have content`);\n  }\n\n  const lang = descriptor.scriptSetup?.lang ?? descriptor.script?.lang ?? 'js';\n  const isTS = /typescript|^ts/.test(lang);\n  const results = compileTemplate({\n    source:          template.content,\n    ast:             template.ast,\n    filename:        descriptor.filename,\n    id:              descriptor.filename,\n    compilerOptions: { mode: 'module', isTS },\n    preprocessLang:  template.lang,\n  });\n\n  if (isTS) {\n    return compileTypeScript(results.code, descriptor.filename);\n  }\n\n  return results.code;\n}\n\n/** @type {SyncTransformer<VueJestTransformOptions>} */\nexport default {\n  getCacheKey(sourceText, sourcePath, options) {\n    const sourceHasher = crypto.createHash('sha512');\n\n    sourceHasher.update(sourceText, 'utf-8');\n\n    return sourceHasher.digest('hex') + sourcePath;\n  },\n\n  process(sourceText, filename, options) {\n    const { descriptor } = parse(sourceText, { filename, ...options.transformerConfig });\n    const code = `\n      ${ processScript(descriptor, options) }\n      ${ processTemplate(descriptor) }\n      /* Don't bother with styles, we don't need it yet */\n      __default__.render = render;\n      export default __default__;\n    `;\n\n    return { code };\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/type-helpers.ts",
    "content": "export default {\n  memberOfObject: <V = string>(obj: Record<string, any>, key: string): V => {\n    return Object.entries(obj || {}).find(([k]) => k === key) as unknown as V;\n  },\n  memberOfComponent: <V = string>(obj: object | undefined, key: string): V => {\n    return (obj as any as Record<string, any>)[key] as V;\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/typeUtils.ts",
    "content": "// Partial<T> (https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype)\n// only allows missing properties on the top level; if anything is given, then all\n// properties of that top-level property must exist.  RecursivePartial<T> instead\n// allows any descendent properties to be omitted.\nexport type RecursivePartial<T> = {\n  [P in keyof T]?:\n  T[P] extends (infer U)[] ? RecursivePartial<U>[] :\n\n    T[P] extends Record<string, unknown> ? RecursivePartial<T[P]> :\n      T[P];\n};\n\nexport type RecursiveReadonly<T> = {\n  readonly [P in keyof T]:\n  T[P] extends (infer U)[] ? readonly RecursiveReadonly<U>[] :\n\n    T[P] extends Record<string, unknown> ? RecursiveReadonly<T[P]> :\n      T[P];\n};\n\nexport type ReadWrite<T> = {\n  -readonly [P in keyof T]: T[P];\n};\n\n/** UpperAlpha is the set of upper-case alphabets. */\ntype UpperAlpha =\n  'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' |\n  'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';\n\n/** Alpha is the set of upper- or lower-case alphabets. */\ntype Alpha<T> = T extends UpperAlpha ? T : T extends Lowercase<UpperAlpha> ? T : never;\n\ntype UpperSnakeCaseInner<T extends string> =\n  T extends '' ? never :\n    T extends UpperAlpha ? `_${ T }` :\n      T extends Alpha<T> ? Uppercase<T> :\n        T extends `${ infer C }${ infer U }` ? `${ UpperSnakeCaseInner<C> }${ UpperSnakeCaseInner<U> }` :\n          never;\n\n/**\n * UpperSnakeCase transforms a string into upper snake case (all upper case,\n * underscore word separators.\n *\n * @example UpperSnakeCase<'HelloWorld'> == 'HELLO_WORLD'\n * @note This fails if there are any non-alphabetic characters.\n */\nexport type UpperSnakeCase<T extends string | symbol | number > =\n  T extends symbol | number ? never :\n    T extends Alpha<T> ? Uppercase<T> :\n      T extends `${ infer C }${ infer U }` ? `${ Uppercase<C> }${ UpperSnakeCaseInner<U> }`\n        : T;\n\n/**\n * RecursiveKeys returns the set of all keys of a type, recursively, separated\n * by dots.\n *\n * @example RecursiveKeys<{a: { b: number}, c: number}> = 'a' | 'a.b' | 'c'\n */\nexport type RecursiveKeys<T> =\n  Record<string, unknown> extends T ? string :\n    T extends readonly unknown[] ? RecursiveKeys<T[number]> :\n      T extends Record<string, unknown> ? keyof T & string | RecursiveKeysInner<T, keyof T & string> :\n        never;\n\ntype RecursiveKeysInner<T, K extends string> = K extends keyof T ? `${ K }.${ RecursiveKeys<T[K]> }` : never;\n\n/**\n * RecursiveTypes returns a single-level type mapping of RecursiveKeys<T> to\n * the value type in T.\n */\nexport type RecursiveTypes<T extends Record<string, any>> =\n  Record<string, unknown> extends T ? never :\n    {\n      [P in RecursiveKeys<T>]:\n      P extends keyof T ?\n        T[P] :\n        P extends `${ infer K }.${ infer R }` ?\n          (\n            K extends keyof T ?\n              (\n                T[K] extends Record<string, unknown> ?\n                  ( R extends keyof RecursiveTypes<T[K]> ? RecursiveTypes<T[K]>[R] : never ) :\n                  never\n              ) :\n              never\n          ) :\n          never;\n    };\n\n/**\n * Check if a given object is defined (i.e. not undefined, and not null).\n */\nexport function defined<T>(input: T | undefined | null): input is T {\n  return typeof input !== 'undefined' && input !== null;\n}\n\nexport type Direction = 'back' | 'forward';\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/units.js",
    "content": "export const UNITS = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];\nexport const FRACTIONAL = ['', 'm', 'u', 'n', 'p', 'f', 'a', 'z', 'y']; // milli micro nano pico femto\n\nexport function formatSi(inValue, {\n  increment = 1000,\n  addSuffix = true,\n  suffix = '',\n  firstSuffix = null,\n  startingExponent = 0,\n  minExponent = 0,\n  maxExponent = 99,\n  maxPrecision = 2,\n} = {}) {\n  let val = inValue;\n  let exp = startingExponent;\n  const divide = maxExponent >= 0;\n\n  // TODO More to think about re: min > max\n  if (divide) {\n    while ( ( val >= increment && exp + 1 < UNITS.length && exp < maxExponent ) || exp < minExponent ) {\n      val = val / increment;\n      exp++;\n    }\n  } else {\n    while ( ( val < increment && exp + 1 < FRACTIONAL.length && exp < (maxExponent * -1) ) || exp < (minExponent * -1) ) {\n      val = val * increment;\n      exp++;\n    }\n  }\n\n  let out = '';\n\n  if ( val < 10 && maxPrecision >= 1 ) {\n    out = `${ Math.round(val * (10 ** maxPrecision) ) / (10 ** maxPrecision) }`;\n  } else {\n    out = `${ Math.round(val) }`;\n  }\n\n  if ( addSuffix ) {\n    if ( exp === 0 && firstSuffix !== null) {\n      out += ` ${ firstSuffix }`;\n    } else {\n      out += ` ${ divide ? UNITS[exp] : FRACTIONAL[exp] }${ suffix }` || '';\n    }\n  }\n\n  return out;\n}\n\nexport function exponentNeeded(val, increment = 1000) {\n  let exp = 0;\n\n  while ( val >= increment ) {\n    val = val / increment;\n    exp++;\n  }\n\n  return exp;\n}\n\nexport function parseSi(inValue, opt) {\n  opt = opt || {};\n  let increment = opt.increment;\n  const allowFractional = opt.allowFractional !== false;\n\n  if ( !inValue || typeof inValue !== 'string' || !inValue.length ) {\n    return NaN;\n  }\n\n  inValue = inValue.replace(/,/g, '');\n\n  let [, valStr, unit, incStr] = inValue.match(/^([0-9.-]+)\\s*([^0-9.-]?)([^0-9.-]?)/);\n  const val = parseFloat(valStr);\n\n  if ( !unit ) {\n    return val;\n  }\n\n  // micro \"mu\" symbol -> u\n  if ( unit.charCodeAt(0) === 181 ) {\n    unit = 'u';\n  }\n\n  const divide = FRACTIONAL.includes(unit);\n  const multiply = UNITS.includes(unit.toUpperCase());\n\n  if ( !increment ) {\n    // Automatically handle 1 KB = 1000B, 1 KiB = 1024B if no increment set\n    if ( (multiply || divide) && incStr === 'i' ) {\n      increment = 1024;\n    } else {\n      increment = 1000;\n    }\n  }\n\n  if ( divide && allowFractional ) {\n    const exp = FRACTIONAL.indexOf(unit);\n\n    return val / (increment ** exp);\n  }\n\n  if ( multiply ) {\n    const exp = UNITS.indexOf(unit.toUpperCase());\n\n    return val * (increment ** exp);\n  }\n\n  // Unrecognized unit character\n  return val;\n}\n\nexport const MEMORY_PARSE_RULES = {\n  memory: {\n    format: {\n      addSuffix:        true,\n      firstSuffix:      'B',\n      increment:        1024,\n      maxExponent:      99,\n      maxPrecision:     2,\n      minExponent:      0,\n      startingExponent: 0,\n      suffix:           'iB',\n    },\n  },\n};\n\nexport function createMemoryFormat(n) {\n  const exponent = exponentNeeded(n, MEMORY_PARSE_RULES.memory.format.increment);\n\n  return {\n    ...MEMORY_PARSE_RULES.memory.format,\n    maxExponent: exponent,\n    minExponent: exponent,\n  };\n}\n\nfunction createMemoryUnits(n) {\n  const exponent = exponentNeeded(n, MEMORY_PARSE_RULES.memory.format.increment);\n\n  return `${ UNITS[exponent] }${ MEMORY_PARSE_RULES.memory.format.suffix }`;\n}\n\nexport function createMemoryValues(total, useful) {\n  const parsedTotal = parseSi((total || '0').toString());\n  const parsedUseful = parseSi((useful || '0').toString());\n  const format = createMemoryFormat(parsedTotal);\n  const formattedTotal = formatSi(parsedTotal, format);\n  const formattedUseful = formatSi(parsedUseful, format);\n\n  return {\n    total:  Number.parseFloat(formattedTotal),\n    useful: Number.parseFloat(formattedUseful),\n    units:  createMemoryUnits(parsedTotal),\n  };\n}\n\nexport default {\n  exponentNeeded,\n  formatSi,\n  parseSi,\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/version.ts",
    "content": "import Electron from 'electron';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\n\nexport function getProductionVersion() {\n  try {\n    return Electron.app.getVersion();\n  } catch (err) {\n    console.log(`Can't get app version: ${ err }`);\n\n    return '?';\n  }\n}\n\nasync function getDevVersion() {\n  try {\n    const { stdout } = await spawnFile('git', ['describe', '--tags'], { stdio: ['ignore', 'pipe', 'inherit'] });\n\n    return stdout.trim();\n  } catch (err) {\n    console.log(`Can't get app version: ${ err }`);\n\n    return '?';\n  }\n}\n\nexport async function getVersion() {\n  if (process.env.RD_MOCK_VERSION) {\n    return process.env.RD_MOCK_VERSION;\n  }\n\n  if (process.env.NODE_ENV === 'production' || process.env.RD_MOCK_FOR_SCREENSHOTS) {\n    return getProductionVersion();\n  }\n\n  return await getDevVersion();\n}\n\nexport function parseDocsVersion(version: string) {\n  // Match '1.9.0-tech-preview' (returns '1.9-tech-preview'), but not '1.9.0-123-g1234567' (returns 'next')\n  const releasePattern = /^v?(\\d+\\.\\d+)\\.\\d+(-[a-z].*)?$/;\n  const matches = releasePattern.exec(version);\n\n  if (matches) {\n    if (matches[2]) {\n      return matches[1].concat(matches[2]);\n    }\n\n    return matches[1];\n  }\n\n  return 'next';\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/width.js",
    "content": "/**\n * Sets the width of a DOM element. Adapted from [youmightnotneedjquery.com](https://youmightnotneedjquery.com/#set_width)\n * @param {Element} el - The target DOM element\n * @param {function | string | number} val - The desired width represented as a Number\n */\nexport function setWidth(el, val) {\n  if (!el) {\n    return;\n  }\n\n  if (typeof val === 'function') {\n    val = val();\n  }\n\n  if (typeof val === 'string') {\n    el.style.width = val;\n\n    return;\n  }\n\n  el.style.width = `${ val }px`;\n}\n\n/**\n * Gets the width of a DOM element. Adapted from [youmightnotneedjquery.com](https://youmightnotneedjquery.com/#get_width)\n * @param {Element} el - The target DOM element\n * @returns Number representing the width for the provided element\n */\nexport function getWidth(el) {\n  if (!el || !el.length) {\n    return;\n  }\n\n  if (el.length) {\n    return parseFloat(getComputedStyle(el[0]).width.replace('px', ''));\n  } else {\n    return parseFloat(getComputedStyle(el).width.replace('px', ''));\n  }\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/utils/wslVersion.ts",
    "content": "/**\n * This exports a single function to ask wsl-helper about the current WSL\n * version.\n */\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport logging from '@pkg/utils/logging';\nimport { executable } from '@pkg/utils/resources';\n\ninterface Version {\n  major:    number;\n  minor:    number;\n  build:    number;\n  revision: number;\n};\n\n/**\n * WSLVersionInfo describes the output from `wsl-helper info`; note that this\n * gets serialized across Electron IPC boundaries.\n */\nexport interface WSLVersionInfo {\n  installed: boolean;\n  inbox:     boolean;\n\n  has_kernel:      boolean;\n  outdated_kernel: boolean;\n  version:         Version;\n  kernel_version:  Version;\n};\n\nconst console = logging['wsl-version'];\n\nexport function makeVersion(major: number, minor = 0, build = 0, revision = 0): Version {\n  return { major, minor, build, revision };\n}\n\nexport function versionString(version: Version): string {\n  const { major, minor, build, revision } = version;\n\n  return [major, minor, build, revision].join('.');\n}\n\nexport function compareVersion(left: Version, right: Version): -1 | 0 | 1 {\n  for (const key of ['major', 'minor', 'build', 'revision'] as const) {\n    if (left[key] !== right[key]) {\n      return left[key] < right[key] ? -1 : 1;\n    }\n  }\n  return 0;\n}\n\n/**\n * Get information about the currently installed WSL version.\n */\nexport default async function getWSLVersion(): Promise<WSLVersionInfo> {\n  const { stdout } = await spawnFile(executable('wsl-helper'),\n    ['wsl', 'info'], { stdio: ['ignore', 'pipe', console] });\n\n  return JSON.parse(stdout);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/vue.config.mjs",
    "content": "import path from 'node:path';\n\nimport _ from 'lodash';\nimport webpack from 'webpack';\n\nconst rootDir = path.resolve(import.meta.dirname, '..', '..');\n\nexport default {\n  publicPath:          '/',\n  outputDir:           path.resolve(rootDir, 'dist', 'app'),\n  productionSourceMap: false,\n\n  /** @type { (config: import('webpack-chain')) => void } */\n  chainWebpack: (config) => {\n    config.target('electron-renderer');\n    config.resolve.alias.set('@pkg', path.resolve(rootDir, 'pkg', 'rancher-desktop'));\n    config.resolve.extensions.add('.ts');\n\n    config.module.rule('ts')\n      .test(/\\.ts$/)\n      .use('ts-loader')\n      .loader('ts-loader')\n      .options({\n        transpileOnly:    process.env.NODE_ENV === 'development',\n        appendTsSuffixTo: ['\\\\.vue$'],\n        happyPackMode:    true,\n      });\n\n    config.module.rule('yaml')\n      .test(/\\.ya?ml(?:\\?[a-z0-9=&.]+)?$/)\n      .use('js-yaml-loader')\n      .loader('js-yaml-loader')\n      .options({ name: '[path][name].[ext]' });\n\n    config.module.rule('raw')\n      .test(/(?:^|[/\\\\])assets[/\\\\]scripts[/\\\\]/)\n      .use('raw-loader')\n      .loader('raw-loader');\n\n    config.plugin('define-plugin').use(webpack.DefinePlugin, [{\n      'process.client':       JSON.stringify(true),\n      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),\n      'process.env.RD_TEST':  JSON.stringify(process.env.RD_TEST || ''),\n\n      'process.env.FEATURE_DIAGNOSTICS_FIXES': process.env.RD_ENV_DIAGNOSTICS_FIXES === '1',\n\n      __VUE_OPTIONS_API__:                     true,\n      __VUE_PROD_DEVTOOLS__:                   false,\n      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,\n    }]);\n\n    config.module.rule('vue').use('vue-loader').tap((options) => {\n      _.set(options, 'loaders.ts', 'ts-loader');\n\n      return options;\n    });\n  },\n\n  css: {\n    loaderOptions: {\n      sass: {\n        additionalData: `\n          @use 'sass:math';\n          @import \"@pkg/assets/styles/base/_variables.scss\";\n          @import \"@pkg/assets/styles/base/_functions.scss\";\n          @import \"@pkg/assets/styles/base/_mixins.scss\";\n        `,\n      },\n    },\n  },\n\n  pluginOptions: {\n    i18n: {\n      locale:         'en',\n      fallbackLocale: 'en',\n      localeDir:      'locales',\n      enableInSFC:    false,\n    },\n  },\n\n  transpileDependencies: ['yaml'],\n\n  pages: {\n    index: {\n      entry:    path.join(import.meta.dirname, 'entry', 'index.ts'),\n      template: path.join(import.meta.dirname, 'public', 'index.html'),\n    },\n  },\n};\n"
  },
  {
    "path": "pkg/rancher-desktop/window/constants.ts",
    "content": "export const mainRoutes = [\n  { route: '/General' },\n  { route: '/Containers', experimental: true },\n  { route: '/Volumes', experimental: true },\n  { route: '/PortForwarding' },\n  { route: '/Images' },\n  { route: '/Snapshots' },\n  { route: '/Troubleshooting' },\n  { route: '/Diagnostics' },\n  { route: '/Extensions' },\n];\n"
  },
  {
    "path": "pkg/rancher-desktop/window/dashboard.ts",
    "content": "import { BrowserWindow } from 'electron';\n\nimport { windowMapping, restoreWindow } from '.';\n\nconst dashboardURL = 'http://127.0.0.1:6120/c/local/explorer';\n\nconst getDashboardWindow = () => ('dashboard' in windowMapping) ? BrowserWindow.fromId(windowMapping['dashboard']) : null;\n\nexport function openDashboard() {\n  let window = getDashboardWindow();\n\n  if (restoreWindow(window)) {\n    return window;\n  }\n\n  window = new BrowserWindow({\n    title:  'Rancher Dashboard',\n    width:  800,\n    height: 600,\n    show:   false,\n  });\n\n  window.loadURL(dashboardURL);\n\n  windowMapping['dashboard'] = window.id;\n\n  window.once('ready-to-show', () => {\n    window?.show();\n  });\n}\n\nexport function closeDashboard() {\n  getDashboardWindow()?.close();\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/window/index.ts",
    "content": "import os from 'os';\nimport path from 'path';\n\nimport Electron, {\n  BrowserWindow, app, shell, ipcMain, nativeTheme, screen, WebContentsView,\n} from 'electron';\n\nimport * as K8s from '@pkg/backend/k8s';\nimport { getSettings } from '@pkg/config/settingsImpl';\nimport { IpcRendererEvents } from '@pkg/typings/electron-ipc';\nimport { isDevBuild } from '@pkg/utils/environment';\nimport Logging from '@pkg/utils/logging';\nimport paths from '@pkg/utils/paths';\nimport { CommandOrControl, Shortcuts } from '@pkg/utils/shortcuts';\nimport { mainRoutes } from '@pkg/window/constants';\nimport { openPreferences } from '@pkg/window/preferences';\n\nconst console = Logging[`window_${ process.type || 'unknown' }`];\n\n/**\n * A mapping of window key (which is our own construct) to a window ID (which is\n * assigned by electron).\n */\nexport const windowMapping: Record<string, number> = {};\n\nexport const webRoot = `app://${ isDevBuild ? '' : '.' }`;\n\n/**\n * Restore or focus a window if it is already open\n * @param window The Electron Browser window to show or restore\n * @returns Boolean: True if the browser window is shown or restored\n */\nexport const restoreWindow = (window: Electron.BrowserWindow | null): window is Electron.BrowserWindow => {\n  if (window) {\n    if (!window.isFocused()) {\n      if (window.isMinimized()) {\n        window.restore();\n      }\n      window.show();\n    }\n\n    return true;\n  }\n\n  return false;\n};\n\n/**\n * Return an existing window of the given ID.\n */\nexport function getWindow(name: string): Electron.BrowserWindow | null {\n  return (name in windowMapping) ? BrowserWindow.fromId(windowMapping[name]) : null;\n}\n\nfunction isInternalURL(url: string) {\n  return url.startsWith(`${ webRoot }/`) || url.startsWith('x-rd-extension://');\n}\n\n/**\n * Open a given window; if it is already open, focus it.\n * @param name The window identifier; this controls window re-use.\n * @param url The URL to load into the window.\n * @param options A hash of options used by `new BrowserWindow(options)`\n */\nexport function createWindow(name: string, url: string, options: Electron.BrowserWindowConstructorOptions) {\n  let window = getWindow(name);\n\n  if (restoreWindow(window)) {\n    return window;\n  }\n\n  window = new BrowserWindow(options);\n  window.webContents.on('console-message', (event) => {\n    const { level, lineNumber, message } = event;\n    const method = level === 'warning' ? 'warn' : level;\n\n    console[method](`${ name }@${ lineNumber }: ${ message }`);\n  });\n  window.webContents.on('will-navigate', (event, input) => {\n    if (isInternalURL(input)) {\n      return;\n    }\n    shell.openExternal(input);\n    event.preventDefault();\n  });\n  window.webContents.setWindowOpenHandler((details) => {\n    if (isInternalURL(details.url)) {\n      window?.webContents.loadURL(details.url);\n    } else {\n      shell.openExternal(details.url);\n    }\n\n    return { action: 'deny' };\n  });\n  window.webContents.on('did-fail-load', (event, errorCode, errorDescription, url) => {\n    console.log(`Failed to load ${ url }: ${ errorCode } (${ errorDescription })`, event);\n  });\n  console.debug('createWindow() name:', name, ' url:', url);\n  window.loadURL(url);\n  windowMapping[name] = window.id;\n\n  return window;\n}\n\nconst mainUrl = process.env.RD_ENV_PLUGINS_DEV ? 'https://localhost:8888' : `${ webRoot }/index.html`;\n\n/**\n * Open the main window; if it is already open, focus it.\n */\nexport function openMain() {\n  console.debug('openMain() webRoot:', webRoot);\n\n  const { width, height } = screen.getPrimaryDisplay().workAreaSize;\n\n  const defaultWidth = Math.min(Math.trunc(width * 0.8), 1280);\n  const defaultHeight = Math.min(Math.trunc(height * 0.8), 720);\n\n  const window = createWindow(\n    'main',\n    mainUrl,\n    {\n      width:          defaultWidth,\n      height:         defaultHeight,\n      resizable:      !process.env.RD_MOCK_FOR_SCREENSHOTS, // remove window's shadows while taking screenshots\n      icon:           path.join(paths.resources, 'icons', 'logo-square-512.png'),\n      webPreferences: {\n        devTools:         !app.isPackaged,\n        nodeIntegration:  true,\n        contextIsolation: false,\n      },\n    });\n\n  if (!Shortcuts.isRegistered(window)) {\n    Shortcuts.register(\n      window,\n      {\n        ...CommandOrControl,\n        key: ',',\n      },\n      () => openPreferences(),\n      'open preferences',\n    );\n\n    mainRoutes.forEach(({ route }, index) => {\n      Shortcuts.register(\n        window,\n        {\n          ...CommandOrControl,\n          key: index + 1,\n        },\n        () => window.webContents.send('route', { path: route }),\n        `switch main tabs ${ route }`,\n      );\n    });\n\n    Shortcuts.register(\n      window,\n      {\n        ...CommandOrControl,\n        key: ']',\n      },\n      () => window.webContents.send('route', { direction: 'forward' }),\n      'switch preferences tabs by cycle [forward]',\n    );\n\n    Shortcuts.register(\n      window,\n      {\n        ...CommandOrControl,\n        key: '[',\n      },\n      () => window.webContents.send('route', { direction: 'back' }),\n      'switch preferences tabs by cycle [back]',\n    );\n  }\n\n  window.on('closed', () => {\n    const cfg = getSettings();\n\n    if (cfg.application.window.quitOnClose) {\n      BrowserWindow.getAllWindows().forEach((window) => {\n        window.close();\n      });\n      app.quit();\n    }\n  });\n\n  app.dock?.show();\n}\n\nlet view: WebContentsView | undefined;\n/** The extension that has been navigated to (but might not be loaded yet). */\nlet currentExtension: { id: string, relPath: string } | undefined;\n/** The extension that has been loaded. */\nlet lastOpenExtension: { id: string, relPath: string } | undefined;\n\nfunction updateViewBackground() {\n  if (view) {\n    view.setBackgroundColor(nativeTheme.shouldUseDarkColors ? '#202c33' : '#f4f4f6');\n  }\n}\n\n/**\n * Attaches a browser view to the main window\n */\nfunction createView() {\n  const mainWindow = getWindow('main');\n  const decodedExtensionId = currentExtension?.id ? Buffer.from(currentExtension.id, 'hex').toString() : undefined;\n  const extensionVersion = decodedExtensionId ? getSettings().application.extensions.installed[decodedExtensionId] : undefined;\n  const extensionContext = {\n    arch:             process.arch,\n    hostname:         os.hostname(),\n    extensionVersion,\n  };\n\n  if (!mainWindow) {\n    throw new Error('Failed to get main window, cannot create view');\n  }\n\n  const webPreferences: Electron.WebPreferences = {\n    nodeIntegration:     false,\n    contextIsolation:    true,\n    sandbox:             true,\n    additionalArguments: [JSON.stringify(extensionContext)],\n  };\n\n  if (currentExtension?.id) {\n    webPreferences.partition = `persist:rdx-${ currentExtension.id }`;\n    const session = Electron.session.fromPartition(webPreferences.partition);\n    const { webRequest } = session;\n    const id = `rdx-preload-${ currentExtension.id }`;\n\n    if (!session.getPreloadScripts().some(script => script.id === id)) {\n      session.registerPreloadScript({\n        id,\n        filePath: path.join(paths.resources, 'preload.js'),\n        type:     'frame',\n      });\n    }\n\n    webRequest.onBeforeSendHeaders((details, callback) => {\n      const source = details.webContents?.getURL() ?? '';\n      const requestHeaders = { ...details.requestHeaders };\n\n      if (isInternalURL(source)) {\n        // If the request is coming from the extension, remove the Origin: header\n        // because it has x-rd-extension:// nonsense (relative to the server).\n        delete requestHeaders.Origin;\n      }\n      callback({ requestHeaders });\n    });\n\n    webRequest.onHeadersReceived((details, callback) => {\n      const sourceURL = details.webContents?.getURL() ?? '';\n\n      if (!isInternalURL(sourceURL)) {\n        // Do not rewrite requests from outside the extension (e.g. iframe).\n        callback({});\n\n        return;\n      }\n\n      // Insert (or overwrite) CORS headers to pretend this was allowed.  While\n      // ideally we can just disable `webSecurity` instead, that seems to break\n      // the preload script (which breaks the extension APIs).\n      const responseHeaders: Record<string, string | string[]> = { ...details.responseHeaders };\n      // HTTP headers use case-insensitive comparison; but accents should count\n      // as different characters (even though it should be ASCII only).\n      const { compare } = new Intl.Collator('en', { sensitivity: 'accent' });\n      const overwriteHeaders = [\n        'Access-Control-Allow-Headers',\n        'Access-Control-Allow-Methods',\n        'Access-Control-Allow-Origin',\n      ];\n\n      for (const header of overwriteHeaders) {\n        const match = Object.keys(responseHeaders).find(k => compare(header, k) === 0);\n\n        responseHeaders[match ?? header] = '*';\n      }\n\n      if (details.method !== 'OPTIONS') {\n        // For any request that's not a CORS preflight, just overwrite the headers.\n        callback({ responseHeaders });\n      } else {\n        // For CORS preflights, also change the status code.\n        const prefix = /\\s+/.exec(details.statusLine)?.shift() ?? 'HTTP/1.1';\n\n        callback({ responseHeaders, statusLine: `${ prefix } 204 No Content` });\n      }\n    });\n  }\n  view = new WebContentsView({ webPreferences });\n  nativeTheme.off('updated', updateViewBackground);\n  nativeTheme.on('updated', updateViewBackground);\n  mainWindow.contentView.addChildView(view);\n  mainWindow.contentView.addListener('bounds-changed', () => {\n    setImmediate(() => mainWindow.webContents.send('extensions/getContentArea'));\n  });\n\n  const backgroundColor = nativeTheme.shouldUseDarkColors ? '#202c33' : '#f4f4f6';\n\n  view.setBackgroundColor(backgroundColor);\n}\n\n/**\n * Updates the browser view size and position\n * @param window The main window\n * @param payload Payload representing coordinates for view position\n */\nconst updateView = (window: Electron.BrowserWindow, payload: { top: number, right: number, bottom: number, left: number }) => {\n  if (!view) {\n    return;\n  }\n\n  const zoomFactor = window.webContents.getZoomFactor();\n\n  view.setBounds({\n    x:      Math.round(payload.left * zoomFactor),\n    y:      Math.round(payload.top * zoomFactor),\n    width:  Math.round((payload.right - payload.left) * zoomFactor),\n    height: Math.round((payload.bottom - payload.top) * zoomFactor),\n  });\n};\n\n/**\n * Navigates to the current desired extension\n */\nfunction extensionNavigate() {\n  if (!currentExtension) {\n    return;\n  }\n  const { id, relPath } = currentExtension;\n\n  const url = `x-rd-extension://${ id }/ui/dashboard-tab/${ relPath }`;\n\n  view?.webContents\n    .loadURL(url)\n    .then(() => {\n      lastOpenExtension = currentExtension;\n    })\n    .then(() => {\n      view?.webContents.setZoomLevel(getWindow('main')?.webContents.getZoomLevel() ?? 0);\n    })\n    .catch((err) => {\n      console.error(`Can't load the extension URL ${ url }: `, err);\n    });\n}\n\nconst zoomInKeys = new Set(`+${ process.platform === 'darwin' ? '=' : '' }`);\nconst zoomOutKeys = new Set('-');\nconst zoomResetKeys = new Set('0');\nconst zoomAllKeys = new Set([...zoomInKeys, ...zoomOutKeys, ...zoomResetKeys]);\n\nfunction isZoomKeyCombo(input: Electron.Input) {\n  const modifier = input.control || input.meta;\n\n  return input.type === 'keyDown' && modifier && zoomAllKeys.has(input.key);\n}\n\n/**\n * Adjusts the zoom level of the main window and attached browser view based on\n * the given input.\n * @param event The Electron Event that triggered this listener\n * @param input The Electron Input associated with the event\n */\nconst extensionZoomListener = (event: Electron.Event, input: Electron.Input) => {\n  const window = getWindow('main');\n\n  if (!window) {\n    return;\n  }\n\n  if (isZoomKeyCombo(input)) {\n    event.preventDefault();\n    const currentZoomLevel = window.webContents.getZoomLevel();\n    const newZoomLevel = (() => {\n      if (zoomInKeys.has(input.key)) {\n        return currentZoomLevel + 0.5;\n      }\n      if (zoomOutKeys.has(input.key)) {\n        return currentZoomLevel - 0.5;\n      }\n      if (zoomResetKeys.has(input.key)) {\n        return 0;\n      }\n    })();\n\n    if (typeof newZoomLevel === 'undefined') {\n      console.debug('Extension Zoom Listener: Unable to determine zoom level');\n\n      return;\n    }\n\n    window.webContents.setZoomLevel(newZoomLevel);\n    view?.webContents.setZoomLevel(newZoomLevel);\n    setImmediate(() => window.webContents.send('extensions/getContentArea'));\n  }\n};\n\n/**\n * Creates and positions the extension's browser view after coordinates are\n * received from the renderer\n * @param _event The Electron Ipc Main Event that triggered this listener\n * @param args Arguments associated with the event\n */\nfunction extensionGetContentAreaListener(_event: Electron.IpcMainEvent, payload: { top: number, right: number, bottom: number, left: number }) {\n  const window = getWindow('main');\n\n  if (!window) {\n    return;\n  }\n\n  if (!view) {\n    try {\n      createView();\n      window.webContents.on('before-input-event', extensionZoomListener);\n    } catch (e) {\n      window.webContents.send('err:extensions/open', e);\n      console.error(e);\n    }\n  }\n\n  updateView(window, payload);\n  if (currentExtension && (currentExtension.id !== lastOpenExtension?.id || currentExtension.relPath !== lastOpenExtension?.relPath)) {\n    extensionNavigate();\n  }\n}\n\n/**\n * Opens an extension in a browser view and attaches it to the main window\n * @param id The extension ID\n * @param relPath The relative path to the extension root\n */\nexport function openExtension(id: string, relPath: string) {\n  console.debug(`openExtension(${ id })`);\n\n  const window = getWindow('main') ?? undefined;\n\n  if (!window) {\n    return;\n  }\n\n  currentExtension = { id, relPath };\n\n  if (!ipcMain.eventNames().includes('ok:extensions/getContentArea')) {\n    ipcMain.on('ok:extensions/getContentArea', extensionGetContentAreaListener);\n  }\n\n  window.webContents.send('extensions/getContentArea');\n\n  if (!Electron.app.isPackaged) {\n    Shortcuts.register(\n      window, {\n        ...CommandOrControl,\n        shift: true,\n        key:   'O', // U+004F Latin Capital Letter O\n      },\n      () => view?.webContents.openDevTools({ mode: 'detach' }),\n      'open developer tools for the extension',\n    );\n  }\n}\n\n/**\n * Removes the extension's browser view from the main window\n */\nexport function closeExtension() {\n  currentExtension = undefined;\n  lastOpenExtension = undefined;\n  if (!view) {\n    return;\n  }\n\n  view.webContents.close();\n  nativeTheme.off('updated', updateViewBackground);\n\n  const window = getWindow('main');\n\n  window?.contentView.removeChildView(view);\n  window?.webContents.removeListener('before-input-event', extensionZoomListener);\n  ipcMain.removeListener('ok:extensions/getContentArea', extensionGetContentAreaListener);\n  view = undefined;\n}\n\n/**\n * Attempts to resize and center window on parent or screen\n * @param window Electron Browser Window that needs to be resized\n * @param width Width of the browser window\n * @param height Height of the browser window\n */\nfunction resizeWindow(window: Electron.BrowserWindow, width: number, height: number): void {\n  const parent = window.getParentWindow();\n\n  if (!parent) {\n    window.center();\n    window.setContentSize(width, height);\n\n    return;\n  }\n\n  const { x: prefX, y: prefY, width: prefWidth } = parent.getBounds();\n  const centered = prefX + Math.round((prefWidth / 2) - (width / 2));\n\n  window.setContentBounds(\n    {\n      x: centered, y: prefY, width, height,\n    },\n  );\n}\n\n/**\n * Internal helper function to open a given modal dialog. Note that you\n * may have to send the dialog/ready event from ipcRenderer to get\n * your dialog to show.\n *\n * @param id The URL for the dialog, corresponds to a Nuxt page; e.g. FirstRun.\n * @param opts The usual browser-window options\n * @returns The opened window\n */\nexport function openDialog(id: string, opts?: Electron.BrowserWindowConstructorOptions, escapeKey = true) {\n  console.debug('openDialog() id: ', id);\n  const window = createWindow(\n    id,\n    // We use hash mode for the router, so `index.html#FirstRun` loads\n    // pkg/rancher-desktop/pages/FirstRun.vue.\n    `${ webRoot }/index.html#${ id }`,\n    {\n      width:           100,\n      height:          100,\n      autoHideMenuBar: !app.isPackaged,\n      show:            false,\n      modal:           true,\n      resizable:       false,\n      frame:           !(os.platform() === 'linux'),\n      ...opts ?? {},\n      webPreferences:  {\n        devTools:                !app.isPackaged,\n        nodeIntegration:         true, // required for ipcRenderer\n        contextIsolation:        false,\n        enablePreferredSizeMode: true,\n        ...opts?.webPreferences ?? {},\n      },\n    },\n  );\n\n  window.menuBarVisible = false;\n\n  window.webContents.on('ipc-message', (_event, channel) => {\n    if (channel === 'dialog/ready') {\n      window.show();\n    }\n  });\n\n  window.webContents.on('preferred-size-changed', async(_event, { width, height }) => {\n    switch (process.platform) {\n    case 'linux':\n      resizeWindow(window, width, height);\n      break;\n    case 'win32': {\n      // On Windows, if the primary display DPI is not the same as the current\n      // display DPI, or if the DPI has been change since Rancher Desktop\n      // started, we end up getting successively larger heights.  To work around\n      // this, check for the actual height of the body element and jump to the\n      // desired height directly, but only if the current content height is\n      // smaller.  Note that all the units here are already affected by DPI\n      // scaling, so we don't need to do that manually.\n      const scripts = [{ code: `document.body.offsetHeight` }];\n      const bodyHeight = await window.webContents.executeJavaScriptInIsolatedWorld(0, scripts);\n      const [, currentHeight] = window.getContentSize();\n\n      if (currentHeight < bodyHeight) {\n        window.setContentSize(width, bodyHeight);\n      }\n      break;\n    }\n    default:\n      window.setContentSize(width, height);\n    }\n  });\n\n  if (!Shortcuts.isRegistered(window) && escapeKey) {\n    Shortcuts.register(\n      window,\n      { key: 'Escape' },\n      () => {\n        window.close();\n      },\n      'Close dialog',\n    );\n  }\n\n  return window;\n}\n\n/**\n * Open the first run window, and return once the user has accepted any\n * configuration required.\n */\nexport async function openFirstRunDialog() {\n  const window = openDialog('FirstRun', { frame: true });\n\n  await (new Promise<void>((resolve) => {\n    window.on('closed', resolve);\n  }));\n}\n\n/**\n * Open a dialog warning the user that RD cannot be run as root/administrator,\n * and return once the user has acknowledged this.\n */\nexport async function openDenyRootDialog() {\n  const window = openDialog('DenyRoot', {\n    frame:  true,\n    width:  336,\n    height: 170,\n  });\n\n  await (new Promise<void>((resolve) => {\n    window.on('closed', resolve);\n  }));\n}\n\nexport type reqMessageId = 'ok' | 'linux-nested' | 'win32-release' | 'win32-kernel' | 'macOS-release';\n\n/**\n * Open a dialog to show reason Desktop will not start\n * @param reasonId Specifies which message to show in dialog\n */\nexport async function openUnmetPrerequisitesDialog(reasonId: reqMessageId, ...args: any[]) {\n  const window = openDialog('UnmetPrerequisites', { frame: true });\n\n  window.webContents.on('ipc-message', (event, channel) => {\n    if (channel === 'dialog/load') {\n      window.webContents.send('dialog/populate', reasonId, ...args);\n    }\n  });\n  await (new Promise<void>((resolve) => {\n    window.on('closed', resolve);\n  }));\n}\n\n/**\n * Open the error message window as a modal window.\n */\nexport async function openKubernetesErrorMessageWindow(titlePart: string, mainMessage: string, failureDetails: K8s.FailureDetails) {\n  const window = openDialog('KubernetesError', {\n    title:  `Rancher Desktop - Error`,\n    width:  800,\n    height: 494,\n    parent: getWindow('main') ?? undefined,\n    frame:  true,\n  });\n\n  window.webContents.on('ipc-message', (event, channel) => {\n    if (channel === 'dialog/load') {\n      window.webContents.send('dialog/populate', titlePart, mainMessage, failureDetails);\n    }\n  });\n  await (new Promise<void>((resolve) => {\n    window.on('closed', resolve);\n  }));\n}\n\n/**\n * Show the prompt describing why we would like sudo permissions.\n *\n * @param explanations A list of reasons why we want sudo permissions.\n * @returns A promise that is resolved when the window closes. It is true if\n *   the user does not want to allow sudo, and never wants to see the prompt\n *   again.\n */\nexport async function openSudoPrompt(explanations: Record<string, string[]>): Promise<boolean> {\n  const window = openDialog('SudoPrompt', { parent: getWindow('main') ?? undefined });\n\n  /**\n   * The result of the dialog; this is true if the user asked to never be\n   * prompted again (and therefore we should not attempt to run sudo).\n   */\n  let result = false;\n\n  window.webContents.on('ipc-message', (event, channel, ...args) => {\n    if (channel === 'dialog/load') {\n      window.webContents.send('dialog/populate', explanations);\n    } else if (channel === 'sudo-prompt/closed') {\n      result = args[0] ?? false;\n    }\n  });\n  await (new Promise<void>((resolve) => {\n    window.on('closed', resolve);\n  }));\n\n  return result;\n}\n\nexport async function showMessageBox(options: Electron.MessageBoxOptions, couldBeModal = false) {\n  const mainWindow = couldBeModal ? getWindow('main') : null;\n\n  return await (mainWindow ? Electron.dialog.showMessageBox(mainWindow, options) : Electron.dialog.showMessageBox(options));\n}\n\n/**\n * Send a message to all windows in the renderer process.\n * @param channel The channel to send on.\n * @param  args Any arguments to pass.\n */\nexport function send<eventName extends keyof IpcRendererEvents>(\n  channel: eventName,\n  ...args: Parameters<IpcRendererEvents[eventName]>\n): void;\n/** @deprecated The channel to send on must be declared. */\nexport function send(channel: string, ...args: any[]) {\n  for (const windowId of Object.values(windowMapping)) {\n    const window = BrowserWindow.fromId(windowId);\n\n    if (window && !window.isDestroyed()) {\n      window.webContents.send(channel, ...args);\n    }\n  }\n}\n\n/**\n * Center the dialog window in the middle of parent window - used on Windows / MacOs\n * @param window parent window\n * @param dialog dialog window\n * @param offsetX\n * @param offsetY\n */\nexport function centerDialog(window: BrowserWindow, dialog: BrowserWindow, offsetX = 0, offsetY = 0) {\n  const windowBounds = window.getBounds();\n  const dialogBounds = dialog.getBounds();\n\n  const x = Math.floor(windowBounds.x + ((windowBounds.width - dialogBounds.width) / 2) + offsetX);\n  const y = Math.floor(windowBounds.y + offsetY);\n\n  dialog.setPosition(x, y);\n}\n"
  },
  {
    "path": "pkg/rancher-desktop/window/preferenceConstants.ts",
    "content": "import { NavItemName } from '@pkg/config/transientSettings';\n\ninterface NavItems {\n  name:  NavItemName;\n  tabs?: string[];\n}\nconst wslTabs: string[] = ['integrations', 'network', 'proxy'];\nconst vmLinuxTabs: string[] = ['hardware', 'volumes'];\nconst vmDarwinTabs: string[] = vmLinuxTabs.concat(['network', 'emulation']);\n\nexport const preferencesNavItems: NavItems[] = [\n  {\n    name: 'Application',\n    tabs: ['general', 'behavior', 'environment'],\n  },\n  {\n    name: process.platform === 'win32' ? 'WSL' : 'Virtual Machine',\n    tabs: process.platform === 'win32' ? wslTabs : ( process.platform === 'linux' ? vmLinuxTabs : vmDarwinTabs ),\n  },\n  {\n    name: 'Container Engine',\n    tabs: ['general', 'allowed-images'],\n  },\n  { name: 'Kubernetes' },\n];\n"
  },
  {
    "path": "pkg/rancher-desktop/window/preferences.ts",
    "content": "import path from 'path';\n\nimport { app, dialog } from 'electron';\n\nimport { webRoot, createWindow, getWindow } from '.';\n\nimport { Help } from '@pkg/config/help';\nimport paths from '@pkg/utils/paths';\nimport { CommandOrControl, Shortcuts } from '@pkg/utils/shortcuts';\nimport { getVersion } from '@pkg/utils/version';\nimport { preferencesNavItems } from '@pkg/window/preferenceConstants';\n\nlet isDirty = false;\n\n/**\n * Open the main window; if it is already open, focus it.\n */\nexport function openPreferences() {\n  const window = createWindow('preferences', `${ webRoot }/index.html#preferences`, {\n    title:           'Rancher Desktop - Preferences',\n    width:           768,\n    height:          512,\n    autoHideMenuBar: true,\n    resizable:       false,\n    minimizable:     false,\n    show:            false,\n    icon:            path.join(paths.resources, 'icons', 'logo-square-512.png'),\n    parent:          getWindow('main') ?? undefined,\n    webPreferences:  {\n      devTools:         !app.isPackaged,\n      nodeIntegration:  true,\n      contextIsolation: false,\n    },\n  });\n\n  if (!Shortcuts.isRegistered(window)) {\n    Shortcuts.register(\n      window,\n      [{\n        key:      '?',\n        meta:     true,\n        platform: 'darwin',\n      }, {\n        key:      'F1',\n        platform: ['win32', 'linux'],\n      }],\n      async() => {\n        Help.preferences.openUrl(await getVersion());\n      },\n      'preferences help',\n    );\n\n    Shortcuts.register(\n      window,\n      { key: 'Escape' },\n      () => {\n        window.close();\n      },\n      'Close preferences dialog',\n    );\n\n    preferencesNavItems.forEach(({ name }, index) => {\n      Shortcuts.register(\n        window,\n        {\n          ...CommandOrControl,\n          key: index + 1,\n        },\n        () => window.webContents.send('route', { name }),\n        `switch preferences tabs ${ name }`,\n      );\n    });\n\n    Shortcuts.register(\n      window,\n      {\n        ...CommandOrControl,\n        key: ']',\n      },\n      () => window.webContents.send('route', { direction: 'forward' }),\n      'switch preferences tabs by cycle [forward]',\n    );\n\n    Shortcuts.register(\n      window,\n      {\n        ...CommandOrControl,\n        key: '[',\n      },\n      () => window.webContents.send('route', { direction: 'back' }),\n      'switch preferences tabs by cycle [back]',\n    );\n  }\n\n  window.webContents.on('ipc-message', (_event, channel) => {\n    if (channel === 'preferences/load') {\n      window.show();\n    }\n  });\n\n  window.on('close', (event) => {\n    if (!isDirty || (process.env.RD_TEST ?? '').includes('e2e')) {\n      return;\n    }\n\n    const cancelPosition = 1;\n\n    const result = dialog.showMessageBoxSync(\n      window,\n      {\n        title:    'Rancher Desktop - Close Preferences',\n        type:     'warning',\n        message:  'Close preferences without applying?',\n        detail:   'There are preferences with changes that have not been applied. All unsaved preferences will be lost.',\n        cancelId: cancelPosition,\n        buttons:  [\n          'Discard changes',\n          'Cancel',\n        ],\n      });\n\n    if (result === cancelPosition) {\n      event.preventDefault();\n    }\n  });\n\n  app.dock?.show();\n}\n\nexport function preferencesSetDirtyFlag(dirtyFlag: boolean) {\n  isDirty = dirtyFlag;\n}\n"
  },
  {
    "path": "resources/k3s-versions.json",
    "content": "{\n  \"cacheVersion\": 2,\n  \"channels\": {\n    \"latest\": \"1.35.2\",\n    \"stable\": \"1.34.5\",\n    \"v1.25\": \"1.25.16\",\n    \"v1.26\": \"1.26.15\",\n    \"v1.27\": \"1.27.16\",\n    \"v1.28\": \"1.28.15\",\n    \"v1.29\": \"1.29.15\",\n    \"v1.30\": \"1.30.14\",\n    \"v1.31\": \"1.31.14\",\n    \"v1.32\": \"1.32.13\",\n    \"v1.33\": \"1.33.9\",\n    \"v1.34\": \"1.34.5\",\n    \"v1.35\": \"1.35.2\"\n  },\n  \"versions\": [\n    \"v1.25.3+k3s1\",\n    \"v1.25.4+k3s1\",\n    \"v1.25.5+k3s2\",\n    \"v1.25.6+k3s1\",\n    \"v1.25.7+k3s1\",\n    \"v1.25.8+k3s1\",\n    \"v1.25.9+k3s1\",\n    \"v1.25.10+k3s1\",\n    \"v1.25.11+k3s1\",\n    \"v1.25.12+k3s1\",\n    \"v1.25.13+k3s1\",\n    \"v1.25.14+k3s1\",\n    \"v1.25.15+k3s2\",\n    \"v1.25.16+k3s4\",\n    \"v1.26.0+k3s2\",\n    \"v1.26.1+k3s1\",\n    \"v1.26.2+k3s1\",\n    \"v1.26.3+k3s1\",\n    \"v1.26.4+k3s1\",\n    \"v1.26.5+k3s1\",\n    \"v1.26.6+k3s1\",\n    \"v1.26.7+k3s1\",\n    \"v1.26.8+k3s1\",\n    \"v1.26.9+k3s1\",\n    \"v1.26.10+k3s2\",\n    \"v1.26.11+k3s2\",\n    \"v1.26.12+k3s1\",\n    \"v1.26.13+k3s2\",\n    \"v1.26.14+k3s1\",\n    \"v1.26.15+k3s1\",\n    \"v1.27.1+k3s1\",\n    \"v1.27.2+k3s1\",\n    \"v1.27.3+k3s1\",\n    \"v1.27.4+k3s1\",\n    \"v1.27.5+k3s1\",\n    \"v1.27.6+k3s1\",\n    \"v1.27.7+k3s2\",\n    \"v1.27.8+k3s2\",\n    \"v1.27.9+k3s1\",\n    \"v1.27.10+k3s2\",\n    \"v1.27.11+k3s1\",\n    \"v1.27.12+k3s1\",\n    \"v1.27.13+k3s1\",\n    \"v1.27.14+k3s1\",\n    \"v1.27.15+k3s2\",\n    \"v1.27.16+k3s1\",\n    \"v1.28.1+k3s1\",\n    \"v1.28.2+k3s1\",\n    \"v1.28.3+k3s2\",\n    \"v1.28.4+k3s2\",\n    \"v1.28.5+k3s1\",\n    \"v1.28.6+k3s2\",\n    \"v1.28.7+k3s1\",\n    \"v1.28.8+k3s1\",\n    \"v1.28.9+k3s1\",\n    \"v1.28.10+k3s1\",\n    \"v1.28.11+k3s2\",\n    \"v1.28.12+k3s1\",\n    \"v1.28.13+k3s1\",\n    \"v1.28.14+k3s1\",\n    \"v1.28.15+k3s1\",\n    \"v1.29.0+k3s1\",\n    \"v1.29.1+k3s2\",\n    \"v1.29.2+k3s1\",\n    \"v1.29.3+k3s1\",\n    \"v1.29.4+k3s1\",\n    \"v1.29.5+k3s1\",\n    \"v1.29.6+k3s2\",\n    \"v1.29.7+k3s1\",\n    \"v1.29.8+k3s1\",\n    \"v1.29.9+k3s1\",\n    \"v1.29.10+k3s1\",\n    \"v1.29.11+k3s1\",\n    \"v1.29.12+k3s1\",\n    \"v1.29.13+k3s1\",\n    \"v1.29.14+k3s1\",\n    \"v1.29.15+k3s1\",\n    \"v1.30.0+k3s1\",\n    \"v1.30.1+k3s1\",\n    \"v1.30.2+k3s2\",\n    \"v1.30.3+k3s1\",\n    \"v1.30.4+k3s1\",\n    \"v1.30.5+k3s1\",\n    \"v1.30.6+k3s1\",\n    \"v1.30.7+k3s1\",\n    \"v1.30.8+k3s1\",\n    \"v1.30.9+k3s1\",\n    \"v1.30.10+k3s1\",\n    \"v1.30.11+k3s1\",\n    \"v1.30.12+k3s1\",\n    \"v1.30.13+k3s1\",\n    \"v1.30.14+k3s2\",\n    \"v1.31.0+k3s1\",\n    \"v1.31.1+k3s1\",\n    \"v1.31.2+k3s1\",\n    \"v1.31.3+k3s1\",\n    \"v1.31.4+k3s1\",\n    \"v1.31.5+k3s1\",\n    \"v1.31.6+k3s1\",\n    \"v1.31.7+k3s1\",\n    \"v1.31.8+k3s1\",\n    \"v1.31.9+k3s1\",\n    \"v1.31.10+k3s1\",\n    \"v1.31.11+k3s1\",\n    \"v1.31.12+k3s1\",\n    \"v1.31.13+k3s1\",\n    \"v1.31.14+k3s1\",\n    \"v1.32.0+k3s1\",\n    \"v1.32.1+k3s1\",\n    \"v1.32.2+k3s1\",\n    \"v1.32.3+k3s1\",\n    \"v1.32.4+k3s1\",\n    \"v1.32.5+k3s1\",\n    \"v1.32.6+k3s1\",\n    \"v1.32.7+k3s1\",\n    \"v1.32.8+k3s1\",\n    \"v1.32.9+k3s1\",\n    \"v1.32.10+k3s1\",\n    \"v1.32.11+k3s3\",\n    \"v1.32.12+k3s1\",\n    \"v1.32.13+k3s1\",\n    \"v1.33.0+k3s1\",\n    \"v1.33.1+k3s1\",\n    \"v1.33.2+k3s1\",\n    \"v1.33.3+k3s1\",\n    \"v1.33.4+k3s1\",\n    \"v1.33.5+k3s1\",\n    \"v1.33.6+k3s1\",\n    \"v1.33.7+k3s3\",\n    \"v1.33.8+k3s1\",\n    \"v1.33.9+k3s1\",\n    \"v1.34.1+k3s1\",\n    \"v1.34.2+k3s1\",\n    \"v1.34.3+k3s3\",\n    \"v1.34.4+k3s1\",\n    \"v1.34.5+k3s1\",\n    \"v1.35.0+k3s3\",\n    \"v1.35.1+k3s1\",\n    \"v1.35.2+k3s1\"\n  ]\n}\n"
  },
  {
    "path": "resources/setup-spin",
    "content": "#!/bin/sh\n# This script uses sh instead of bash to be compatible with as many distros as possible.\nset -u\n\n# The script is located in the Rancher Desktop resources/ directory.\nresources_dir=$(dirname \"$0\")\n\n# We run setup-spin in the rancher-desktop distro to setup spin on the Win32 host.\nif [ \"${WSL_DISTRO_NAME:-}\" = \"rancher-desktop\" ]; then\n  spin=\"${resources_dir}/win32/bin/spin.exe\"\nelif [ \"$(uname)\" = \"Linux\" ]; then\n  spin=\"${resources_dir}/linux/bin/spin\"\nelse\n  spin=\"${resources_dir}/darwin/bin/spin\"\nfi\n\nif [ ! -x \"$spin\" ]; then\n  echo \"Cannot execute '${spin}' (or does not exist)\"\n  exit 1\nfi\n\nif [ \"${WSL_DISTRO_NAME:-}\" = \"rancher-desktop\" ]; then\n  echo \"Waiting for github.com to become resolvable\"\n  for _ in $(seq 30); do\n    curl --head --silent http://github.com >/dev/null\n    rc=$?; test $rc -ne 0 && echo \"curl exit status is $rc\"\n    if [ $rc -ne 6 ]; then\n      break\n    fi\n    sleep 2\n  done\nfi\n\ninstall_templates() {\n  repo=$1\n\n  echo \"Installing ${repo} templates from tag ${SPIN_TEMPLATES_TAG}\"\n  url=\"https://github.com/spinframework/${repo}/archive/refs/tags/${SPIN_TEMPLATES_TAG}.tar.gz\"\n  if ! \"$spin\" templates install --update --tar \"$url\"; then\n    echo \"Install failed, falling back to main branch\"\n    url=\"https://github.com/spinframework/${repo}/archive/refs/heads/main.tar.gz\"\n    \"$spin\" templates install --update --tar \"$url\"\n  fi\n}\n\ninstall_plugin() {\n  plugin=$1\n  version=$2\n  echo \"Installing plugin ${plugin} version ${version}\"\n  \"$spin\" plugins uninstall \"$plugin\" || true\n  # `spin plugins install` requires `git`; however, we can provide the URL of a\n  # remote manifest instead.\n  local url_base=\"https://github.com/spinframework/spin-plugins/raw/refs/heads/main/manifests/${plugin}\"\n  local url=\"${url_base}/${plugin}@${version}.json\"\n  if ! curl --head --silent --fail \"${url}\" &>/dev/null; then\n    echo \"Plugin not available at ${url}, installing unversioned instead.\"\n    url=\"${url_base}/${plugin}.json\"\n  fi\n  \"$spin\" plugins install --yes --url \"${url}\"\n  rc=$?; test $rc -ne 0 && echo \"Exit status is $rc\"\n}\n\ninstall_templates spin\ninstall_templates spin-python-sdk\ninstall_templates spin-js-sdk\n\ninstall_plugin kube \"${KUBE_PLUGIN_VERSION:-0.3.1}\"\n\necho \"'${spin}' setup complete\"\n"
  },
  {
    "path": "screenshots/README.md",
    "content": "# screenshots\n\nThis is the e2e task that automatically collects screenshots of the UI.\n\n\n## Overview\n\nIt runs as any other `Playwright` e2e test and uses a proper utility for each platform to\ncollect screenshots of the main sections of the UI. Screenshots are either produced in\nlight & dark mode.\n\n\n## Prerequisites\n\n### macOS\n\n`screencapture` is required. It is part of the OS, so doesn't need to be installed\nseparately. See https://ss64.com/osx/screencapture.html\n\n`GetWindowID` is also required, and can be installed running `brew install\nsmokris/getwindowid/getwindowid`.\n\nIf you're experiencing any issues like `screencapture: no file specified` or\n`could not create image from window` while running the screenshots script, it's most\nlikely related to your privacy settings. Try to enable the 'Terminal' option at:\nSystem Preferences -> Security & Privacy -> Privacy -> Screen Recording.\n\n### Windows\n\nWe have custom scripting, but we must have PowerShell available in the `PATH`\n(as `powershell`, not `pwsh`).  This should come as part of Windows.\n\nPlease note that we crop a full-screen screenshot, so any overlapping windows\nwill be visible.\n\n### Linux\n\n`xwininfo` and `GraphicsMagick` are required.  The former may be in the `x11-utils` package.\n\n## Running\n\nFirst, install dependencies with:\n\n```\nyarn\n```\n\nMake sure you have run a \"Factory Reset\" before capturing screenshots, so they show the\ndefault settings and not your current configuration.\n\nOn macOS and Linux, after the \"Factory Reset\" run the app once manually (`yarn dev`)\nto disable admin access. Otherwise, the `screenshots` script will hang when the password\nprompt comes up.\n\nThen, capture screenshots in both dark & light mode:\n\n```\nyarn screenshots\n```\n\nLight mode only:\n\n```\nyarn screenshots:light\n```\n\nDark mode only:\n\n```\nyarn screenshots:dark\n```\n\n\n## Environment Variables\n\n- RD_MOCK_VERSION\n\n  Customize the app version, this is useful for populating documentation before release\n  activities are finalized.\n  ```\n  export RD_MOCK_VERSION=1.0.0; yarn screenshots\n  ```\n\n## Output\n\nThe directory where the screenshots are saved:\n\n  ```\n  screenshots/output\n  ```\n"
  },
  {
    "path": "screenshots/Screenshots.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { expect } from '@playwright/test';\nimport which from 'which';\n\nimport { NavPage } from '../e2e/pages/nav-page';\nimport { PreferencesPage } from '../e2e/pages/preferences';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { Log } from '@pkg/utils/logging';\n\nimport type { Page } from '@playwright/test';\n\ninterface ScreenshotsOptions {\n  directory: string;\n  log:       Log;\n}\n\nexport class Screenshots {\n  // used by Mac api\n  private appBundleTitle = 'Electron';\n\n  protected windowTitle = '';\n  private static screenshotIndex = 0;\n  readonly page:      Page;\n  readonly directory: string;\n  readonly log:       Log;\n\n  constructor(page: Page, opt: ScreenshotsOptions) {\n    this.page = page;\n    const { directory, log } = opt;\n\n    this.directory = path.resolve(import.meta.dirname, 'output', os.platform(), directory);\n    this.log = log;\n  }\n\n  protected buildPath(title: string): string {\n    return path.resolve(this.directory, `${ Screenshots.screenshotIndex++ }_${ title }.png`);\n  }\n\n  protected async createScreenshotsDirectory() {\n    if (!this.directory) {\n      return;\n    }\n\n    await fs.promises.mkdir(\n      this.directory,\n      { recursive: true },\n    );\n  }\n\n  protected async screenshot(title: string, includeAll = false) {\n    const outPath = this.buildPath(title);\n\n    try {\n      switch (process.platform) {\n      case 'darwin':\n        await this.screenshotDarwin(outPath, includeAll);\n        break;\n      case 'win32':\n        await this.screenshotWindows(outPath, includeAll);\n        break;\n      default:\n        await this.screenshotLinux(outPath, includeAll);\n      }\n    } catch (e) {\n      console.error('Failed to take screenshot', { error: e });\n      process.exit(1);\n    }\n  }\n\n  protected async screenshotDarwin(outPath: string, includeAll: boolean) {\n    const { stdout: windowId, stderr } = await spawnFile('GetWindowID', [this.appBundleTitle, this.windowTitle], { stdio: 'pipe' });\n\n    if (!windowId) {\n      throw new Error(`Failed to find window ID for ${ this.windowTitle }: ${ stderr || '(no stderr)' }`);\n    }\n    const args = [...(includeAll ? [] : ['-a']), '-o', '-l', windowId.trim(), outPath];\n\n    await spawnFile('screencapture', args, { stdio: this.log });\n  }\n\n  protected async screenshotWindows(outPath: string, includeAll: boolean) {\n    const script = path.resolve(import.meta.dirname, 'screenshot.ps1');\n    const args = ['-ExecutionPolicy', 'Bypass', script, '-FilePath', outPath, '-Title', `'${ this.windowTitle }'`];\n\n    if (!includeAll) {\n      args.push('-Foreground');\n    }\n    await spawnFile('powershell.exe', args, { stdio: this.log });\n  }\n\n  protected async screenshotLinux(outPath: string, includeAll: boolean) {\n    // Find the target window; note that this is a child window of the window\n    // frame, so we can't use it directly.\n    let windowId;\n    let { stdout } = await spawnFile('xwininfo', ['-name', this.windowTitle, '-tree'], { stdio: 'pipe' });\n\n    // Walk up the parents of the current window, until the parent is the root window.\n    while (true) {\n      this.log.log(stdout);\n      ([, windowId] = /xwininfo: Window id: (0x[0-9a-f]+)/i.exec(stdout) ?? []);\n      const [, parentId, rest] = /Parent window id: (0x[0-9a-f]+)(.*)/i.exec(stdout) ?? [];\n\n      if (!parentId || rest.includes('(the root window)')) {\n        break;\n      }\n      ({ stdout } = await spawnFile('xwininfo', ['-id', parentId, '-tree'], { stdio: 'pipe' }));\n    }\n    if (!windowId) {\n      throw new Error(`Failed to find window ID for ${ this.windowTitle }`);\n    }\n    // If `gm` is available, use `gm import`; otherwise, use `import`.\n    const args = ['-window', windowId, outPath];\n\n    if (await (which('gm', { nothrow: true }))) {\n      await spawnFile('gm', ['import', ...args], { stdio: this.log });\n    } else {\n      await spawnFile('import', args, { stdio: this.log });\n    }\n  }\n}\n\nexport class MainWindowScreenshots extends Screenshots {\n  constructor(page: Page, opt: ScreenshotsOptions) {\n    super(page, opt);\n    this.windowTitle = 'Rancher Desktop';\n  }\n\n  async take(tabName: Parameters<NavPage['navigateTo']>[0], navPage?: NavPage, timeout?: number, includeAll?: boolean): Promise<void>;\n  async take(screenshotName: string, includeAll?: boolean): Promise<void>;\n  async take(name: string, navPageOrIncludeAll?: NavPage | boolean, timeout = 200, includeAll = false) {\n    let navPage: NavPage | undefined;\n\n    if (typeof navPageOrIncludeAll === 'boolean') {\n      includeAll = navPageOrIncludeAll;\n    } else {\n      navPage = navPageOrIncludeAll;\n    }\n    if (navPage) {\n      await navPage.navigateTo(name as Parameters<NavPage['navigateTo']>[0]);\n      await this.page.waitForTimeout(timeout);\n    }\n\n    await this.createScreenshotsDirectory();\n    await this.screenshot(name, includeAll);\n  }\n}\n\nexport class PreferencesScreenshots extends Screenshots {\n  readonly preferencePage: PreferencesPage;\n\n  constructor(page: Page, preferencePage: PreferencesPage, opt: ScreenshotsOptions) {\n    super(page, opt);\n    this.preferencePage = preferencePage;\n    this.windowTitle = 'Rancher Desktop - Preferences';\n  }\n\n  async take(tabName: string, subTabName?: string) {\n    const tab = (this.preferencePage as any)[tabName];\n\n    await tab.nav.click();\n    await expect(tab.nav).toHaveClass('preferences-nav-item active');\n    const path = subTabName ? `${ tabName }_${ subTabName }` : tabName;\n\n    await this.createScreenshotsDirectory();\n    await this.screenshot(path);\n  }\n}\n\n// If needed, set the screen resolution in CI.\nawait (async function() {\n  if (!process.env.CI) {\n    return;\n  }\n  switch (process.platform) {\n  case 'win32': {\n    const script = path.resolve(import.meta.dirname, 'set-display-resolution.ps1');\n    await spawnFile(\n      'powershell.exe',\n      ['-ExecutionPolicy', 'Bypass', script],\n      { stdio: 'inherit' });\n  }\n  }\n})();\n"
  },
  {
    "path": "screenshots/playwright-config.ts",
    "content": "import childProcess from 'child_process';\nimport os from 'os';\nimport * as path from 'path';\n\nimport { defineConfig } from '@playwright/test';\n\nconst ci = !!process.env.CI;\nconst timeScale = ci ? 2 : 1;\n\nprocess.env.RD_MOCK_FOR_SCREENSHOTS = 'true';\n\nconst config = defineConfig({\n  testDir:       path.join(import.meta.dirname, '..', 'screenshots'),\n  outputDir:     path.join(import.meta.dirname, '..', 'e2e', 'reports'),\n  timeout:       10 * 60 * 1000 * timeScale,\n  globalTimeout: 30 * 60 * 1000 * timeScale,\n  workers:       1,\n  reporter:      'list',\n  retries:       ci ? 2 : 0,\n  use:           {\n    colorScheme: process.env.THEME === 'dark' ? 'dark' : 'light',\n    trace:       {\n      mode:        'on-all-retries',\n      screenshots: true,\n    },\n  },\n});\n\nif (os.platform() === 'darwin') {\n  childProcess.execSync(`osascript -e 'tell app \"System Events\" to tell appearance preferences to set dark mode to ${ config.use?.colorScheme === 'dark' }'`);\n}\n\nif (os.platform() === 'win32') {\n  const mode = config.use?.colorScheme === 'dark' ? '0' : '1';\n\n  childProcess.execSync(`reg add HKCU\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\Themes\\\\Personalize /v AppsUseLightTheme /t REG_DWORD /f /d ${ mode }`);\n}\n\nexport default config;\n"
  },
  {
    "path": "screenshots/screenshot.ps1",
    "content": "<#\n.SYNOPSIS\n  Take a screenshot of the active window.\n.DESCRIPTION\n  Take a screenshot of the active window and output it to a PNG file.\n.PARAMETER FilePath\n  The name of the file to write to.  The file will be a PNG.\n.PARAMETER Foreground\n  If set, make the window foreground or otherwise visible first.\n.PARAMETER Title\n  The title of the window to capture; defaults to the active window.\n#>\nParam(\n  [Parameter(\n    Mandatory = $true\n  )][string]$FilePath,\n  [switch]$Foreground,\n  [string]$Title\n)\n\n# We need to call Win32 APIs; PowerShell natively understands .NET only, so we\n# need to write some C#...\n$cSharpSource = @'\nusing System;\nusing System.Drawing;\nusing System.Runtime.InteropServices;\n\nnamespace Screenshot {\n  public class Screenshot {\n    public void Take(string filePath, string title, bool foreground) {\n      Image img;\n      if (title != \"\") {\n        img = CaptureWindowWithTitle(title, foreground);\n      } else {\n        img = CaptureActiveWindow();\n      }\n      img.Save(filePath, System.Drawing.Imaging.ImageFormat.Png);\n    }\n\n    public Image CaptureActiveWindow() {\n      return CaptureWindow(User32.GetForegroundWindow());\n    }\n\n    public Image CaptureWindowWithTitle(string title, bool foreground) {\n      IntPtr hwnd = User32.FindWindow(null, title);\n      if (!foreground) {\n        return CaptureWindow(hwnd);\n      }\n      if (User32.SetForegroundWindow(hwnd) != 0) {\n        return CaptureWindow(hwnd);\n      }\n      // Failed to set the window as foreground; make it topmost instead.\n      Int32 styles = User32.GetWindowLong(hwnd, User32.GWL_EXSTYLE);\n      User32.SetWindowPos(hwnd, (IntPtr)(User32.HWND_TOPMOST), 0, 0, 0, 0,\n        User32.SWP_NOSIZE | User32.SWP_NOMOVE);\n      Image image = CaptureWindow(hwnd);\n      if ((styles & User32.WS_EX_TOPMOST) == 0) {\n        User32.SetWindowPos(hwnd, (IntPtr)(User32.HWND_NOTOPMOST), 0, 0, 0, 0,\n          User32.SWP_NOSIZE | User32.SWP_NOMOVE);\n      }\n      return image;\n    }\n\n    public Image CaptureWindow(IntPtr hwnd) {\n      // Rancher Desktop (Electron/Chromium) uses accelerated (OpenGL/DirectX)\n      // rendering, so using BitBlt and related functions based on DCs will just\n      // emit a fully black image.  Instead, we need to take a screenshot of the\n      // whole screen, cropped to the area we need.\n\n      // Determine the bounds of the source window, excluding shadows.\n      // GetWindowRect() now includes shadows, so that's not useful.\n      DWMAPI.RECT rect = new DWMAPI.RECT();\n      Int32 hr = DWMAPI.DwmGetWindowAttribute(\n        hwnd,\n        DWMAPI.DWMWA_EXTENDED_FRAME_BOUNDS,\n        out rect,\n        Marshal.SizeOf(typeof(DWMAPI.RECT)));\n      if (hr != 0) {\n        throw new InvalidOperationException(\n          String.Format(\"Failed to get window size: {0:x}\", hr));\n      }\n\n      // Create a new bitmap with the desired size.\n      Size size = new Size(rect.right - rect.left, rect.bottom - rect.top);\n      Bitmap bitmap = new Bitmap(size.Width, size.Height);\n\n      // Get a \"graphics\" object that can help copy the image.\n      Graphics graphics = Graphics.FromImage(bitmap);\n\n      // Do the actual copying.\n      graphics.CopyFromScreen(rect.left, rect.top, 0, 0, size);\n\n      return bitmap;\n    }\n\n    private class User32 {\n      public const Int32 GWL_EXSTYLE = -20;\n      public const Int32 HWND_TOPMOST = -1;\n      public const Int32 HWND_NOTOPMOST = -2;\n      public const UInt32 SWP_NOSIZE = 0x0001;\n      public const UInt32 SWP_NOMOVE = 0x0002;\n      public const Int32 WS_EX_TOPMOST = 0x00000008;\n\n      [DllImport(\"user32.dll\", CharSet = CharSet.Unicode)]\n      public static extern IntPtr FindWindow(\n        String windowClass,\n        String windowTitle);\n      [DllImport(\"user32.dll\")]\n      public static extern IntPtr GetForegroundWindow();\n      [DllImport(\"user32.dll\")]\n      public static extern Int32 GetWindowLong(IntPtr hwnd, Int32 index);\n      [DllImport(\"user32.dll\")]\n      public static extern Int32 SetForegroundWindow(IntPtr hwnd);\n      [DllImport(\"user32.dll\")]\n      public static extern Int32 SetWindowPos(\n        IntPtr hwnd,\n        IntPtr hwndInsertAfter,\n        Int32 x, Int32 y,\n        Int32 cx, Int32 cy,\n        UInt32 flags);\n    }\n\n    private class DWMAPI {\n      [StructLayout(LayoutKind.Sequential)]\n      public struct RECT\n      {\n          public int left;\n          public int top;\n          public int right;\n          public int bottom;\n      }\n\n      public const Int32 DWMWA_EXTENDED_FRAME_BOUNDS = 9;\n\n      [DllImport(\"dwmapi.dll\")]\n      public static extern Int32 DwmGetWindowAttribute(\n        IntPtr hwnd,\n        Int32 attribute,\n        out RECT pvAttribute,\n        int cbAttribute);\n    }\n  }\n}\n'@\n\nAdd-Type $cSharpSource -ReferencedAssemblies 'System.Drawing'\n\n(New-Object Screenshot.Screenshot).Take($FilePath, $Title, $Foreground)\n"
  },
  {
    "path": "screenshots/screenshots.e2e.spec.ts",
    "content": "import os from 'os';\nimport path from 'path';\n\nimport { test, expect, _electron } from '@playwright/test';\n\nimport { MainWindowScreenshots, PreferencesScreenshots } from './Screenshots';\nimport { containersList } from './test-data/containers';\nimport { imagesList } from './test-data/images';\nimport { lockedSettings } from './test-data/preferences';\nimport { snapshotsList } from './test-data/snapshots';\nimport { volumesList } from './test-data/volumes';\nimport { NavPage } from '../e2e/pages/nav-page';\nimport { PreferencesPage } from '../e2e/pages/preferences';\nimport { clearUserProfile } from '../e2e/utils/ProfileUtils';\nimport {\n  createDefaultSettings, setUserProfile, retry, teardown, tool, startRancherDesktop, reportAsset,\n} from '../e2e/utils/TestUtils';\n\nimport { ContainerEngine, CURRENT_SETTINGS_VERSION } from '@pkg/config/settings';\nimport { Log } from '@pkg/utils/logging';\nimport { ContainerLogsPage } from 'e2e/pages/container-logs-page';\n\nimport type { ElectronApplication, Page } from '@playwright/test';\n\nconst isWin = os.platform() === 'win32';\nconst isMac = os.platform() === 'darwin';\nlet console: Log;\n\ntest.describe.serial('Main App Test', () => {\n  let electronApp: ElectronApplication;\n  let page: Page;\n  let navPage: NavPage;\n  let screenshot: MainWindowScreenshots;\n  const afterCheckedTimeout = 200;\n\n  test.beforeAll(async({ colorScheme }, testInfo) => {\n    createDefaultSettings({\n      application:     { updater: { enabled: false } },\n      containerEngine: {\n        allowedImages: { enabled: false, patterns: ['rancher/example'] },\n        name:          ContainerEngine.CONTAINERD,\n      },\n      diagnostics: { showMuted: true, mutedChecks: { MOCK_CHECKER: true } },\n    });\n\n    await setUserProfile(\n      { version: 11 as typeof CURRENT_SETTINGS_VERSION, containerEngine: { allowedImages: { enabled: true, patterns: [] } } },\n      {},\n    );\n\n    electronApp = await startRancherDesktop(testInfo, { mock: false });\n    console = new Log(path.basename(import.meta.filename, '.ts'), reportAsset(testInfo, 'log'));\n\n    page = await electronApp.firstWindow();\n    navPage = new NavPage(page);\n    screenshot = new MainWindowScreenshots(page, { directory: `${ colorScheme }/main`, log: console });\n\n    await page.emulateMedia({ colorScheme });\n    await (await electronApp.browserWindow(page)).evaluate(browserWindow => {\n      // Ensure the window is of the correct size, and near the top left corner\n      // in case the screen is too small.  But it needs to be lower than the\n      // macOS menu bar.\n      browserWindow.setBounds({ x: 64, y: 64, width: 1024, height: 768 });\n    });\n\n    await navPage.progressBecomesReady();\n\n    await page.waitForTimeout(2500);\n\n    await retry(async() => {\n      await tool('rdctl', 'extension', 'install', 'splatform/epinio-docker-desktop');\n    }, { tries: 5 });\n    await retry(async() => {\n      await tool('rdctl', 'extension', 'install', 'docker/logs-explorer-extension');\n    }, { tries: 5 });\n\n    const navExtension = page.locator('[data-test=\"extension-nav-epinio\"]');\n\n    await expect(navExtension).toBeVisible({ timeout: 30000 });\n  });\n\n  test.afterAll(async({ colorScheme }, testInfo) => {\n    await clearUserProfile();\n    await tool('rdctl', 'extension', 'uninstall', 'splatform/epinio-docker-desktop');\n    await tool('rdctl', 'extension', 'uninstall', 'docker/logs-explorer-extension');\n\n    return teardown(electronApp, testInfo);\n  });\n\n  test.describe('Main Page', () => {\n    test('General Page', async({ colorScheme }) => {\n      await screenshot.take('General', navPage);\n    });\n\n    test('Containers Page', async() => {\n      // Override the containers before the Vuex state is loaded.\n      await navPage.page.exposeFunction('listContainersMock', (options?: any) => {\n        return Promise.resolve(containersList);\n      });\n      await navPage.page.evaluate(() => {\n        const { ddClient, listContainersMock } = window as any;\n        ddClient.docker._listContainers = ddClient.docker.listContainers;\n        ddClient.docker.listContainers = listContainersMock;\n      });\n\n      try {\n        const containersPage = await navPage.navigateTo('Containers');\n\n        await expect(containersPage.page.getByRole('row')).toHaveCount(11);\n        await screenshot.take('Containers');\n      } finally {\n        await navPage.page.evaluate(() => {\n          const { ddClient } = window as any;\n          ddClient.docker.listContainers = ddClient.docker._listContainers;\n          delete ddClient.docker._listContainers;\n        });\n      }\n    });\n\n    test('Container Logs Page', async({ colorScheme }) => {\n      const containersPage = await navPage.navigateTo('Containers');\n\n      await containersPage.waitForTableToLoad();\n      await expect(containersPage.page.getByRole('row')).toHaveCount(11);\n\n      await containersPage.page.evaluate(() => {\n        const { ddClient } = window as any;\n        ddClient.docker.cli._exec = ddClient.docker.cli.exec;\n        ddClient.docker.cli.exec = (command, args, options) => {\n          if (command === 'logs') {\n            setTimeout(() => {\n              const sampleLogs = [\n                '2025-01-15T10:30:15.123456789Z PostgreSQL Database directory appears to contain a database; Skipping initialization',\n                '2025-01-15T10:30:15.234567890Z LOG:  starting PostgreSQL 15.5 on x86_64-pc-linux-gnu',\n                '2025-01-15T10:30:15.345678901Z LOG:  listening on IPv4 address \"0.0.0.0\", port 5432',\n                '2025-01-15T10:30:15.456789012Z LOG:  listening on Unix socket \"/var/run/postgresql/.s.PGSQL.5432\"',\n                '2025-01-15T10:30:15.567890123Z LOG:  database system was shut down at 2025-01-15 10:28:45 UTC',\n                '2025-01-15T10:30:15.678901234Z LOG:  database system is ready to accept connections',\n                '2025-01-15T10:31:20.789012345Z LOG:  checkpoint starting: time',\n                '2025-01-15T10:32:25.890123456Z LOG:  checkpoint complete: wrote 42 buffers (0.3%); 0 WAL file(s) added, 0 removed, 0 recycled',\n                '2025-01-15T10:35:30.901234567Z LOG:  received smart shutdown request',\n                '2025-01-15T10:35:30.912345678Z LOG:  database system is shut down',\n              ];\n\n              for (const log of sampleLogs) {\n                options.stream.onOutput({ stdout: log + '\\r\\n' });\n              }\n            }, 100);\n\n            return { close: () => {} };\n          }\n          return ddClient.docker.cli._exec(command, args, options);\n        };\n      });\n\n      try {\n        const containerId = containersList[0].Id;\n\n        await containersPage.waitForContainerToAppear(containerId);\n        await containersPage.viewContainerInfo(containerId);\n\n        await containersPage.page.waitForURL('**/containers/info/**');\n\n        const containerLogsPage = new ContainerLogsPage(containersPage.page);\n        await containerLogsPage.waitForLogsToLoad();\n        await expect(containerLogsPage.containerInfo).toBeVisible();\n        await expect(containerLogsPage.terminal).toBeVisible();\n        await expect(containerLogsPage.loadingIndicator).not.toBeVisible();\n\n        await screenshot.take('Container-Logs');\n      } finally {\n        await containersPage.page.evaluate(() => {\n          const { ddClient } = window as any;\n          if (ddClient.docker.cli._exec) {\n            ddClient.docker.cli.exec = ddClient.docker.cli._exec;\n            delete ddClient.docker.cli._exec;\n          }\n        });\n      }\n    });\n\n    test('PortForwarding Page', async({ colorScheme }) => {\n      const portForwardingPage = await navPage.navigateTo('PortForwarding');\n\n      await expect(portForwardingPage.page.getByRole('row')).toHaveCount(4);\n      await screenshot.take('PortForwarding', navPage);\n    });\n\n    test('Images Page', async({ colorScheme }) => {\n      await navPage.page.exposeFunction('imagesListMock', () => {\n        return imagesList;\n      });\n\n      const imagesPage = await navPage.navigateTo('Images');\n\n      await expect(imagesPage.rows).toBeVisible();\n      await screenshot.take('Images');\n    });\n\n    test('Volumes Page', async({ colorScheme }) => {\n      // Override the volumes before Vuex state is loaded.\n      await navPage.page.exposeFunction('listVolumesMock', (options?: any) => {\n        return volumesList;\n      });\n      await navPage.page.evaluate(() => {\n        const { ddClient, listVolumesMock } = window as any;\n        ddClient.docker._rdListVolumes = ddClient.docker.rdListVolumes;\n        ddClient.docker.rdListVolumes = listVolumesMock;\n      });\n\n      try {\n        const volumesPage = await navPage.navigateTo('Volumes');\n\n        await expect(volumesPage.page.locator('.volumesTable')).toBeVisible();\n        await expect(volumesPage.page.getByRole('row')).toHaveCount(7);\n        await screenshot.take('Volumes');\n      } finally {\n        await navPage.page.evaluate(() => {\n          const { ddClient } = window as any;\n          ddClient.docker.rdListVolumes = ddClient.docker._rdListVolumes;\n          delete ddClient.docker._rdListVolumes;\n        });\n      }\n    });\n\n    test('Troubleshooting Page', async({ colorScheme }) => {\n      await screenshot.take('Troubleshooting', navPage);\n    });\n\n    test('Diagnostics Page', async({ colorScheme }) => {\n      const diagnosticsPage = await navPage.navigateTo('Diagnostics');\n\n      // show diagnostics badge\n      await expect(diagnosticsPage.diagnostics).toBeVisible();\n      await diagnosticsPage.checkerRows('MOCK_CHECKER').muteButton.click();\n      // wait for the red bullet to appear on the Diagnostics page label\n      await page.waitForTimeout(1000);\n\n      await screenshot.take('Diagnostics');\n    });\n\n    test('Snapshots Page', async({ colorScheme }) => {\n      const snapshotsPage = await navPage.navigateTo('Snapshots');\n\n      await expect(snapshotsPage.snapshotsPage).toBeVisible();\n      // Wait for create button to be actively visible\n      await expect(snapshotsPage.createSnapshotButton).toBeVisible();\n      await screenshot.take('Snapshots-Empty');\n\n      await snapshotsPage.createSnapshotButton.click();\n      // Wait for create button to disappear\n      await expect(snapshotsPage.createSnapshotButton).not.toBeVisible();\n      await expect(snapshotsPage.createSnapshotNameInput).toBeVisible();\n      await expect(snapshotsPage.createSnapshotDescInput).toBeVisible();\n      await snapshotsPage.createSnapshotNameInput.fill('Snapshot 1');\n      await snapshotsPage.createSnapshotDescInput.fill('Snapshot 1 description');\n      await screenshot.take('Snapshot-Create');\n\n      await page.route(/^.*\\/snapshots/, async(route) => {\n        await route.fulfill(snapshotsList);\n      });\n      await navPage.navigateTo('Snapshots');\n      await expect(snapshotsPage.snapshotsPage).toBeVisible();\n      await screenshot.take('Snapshots-List');\n    });\n\n    test('Extensions Page', async({ colorScheme }) => {\n      const extensionsPage = await navPage.navigateTo('Extensions');\n\n      await expect(extensionsPage.cardEpinio).toBeVisible({ timeout: 30_000 });\n      await screenshot.take('Extensions');\n\n      await extensionsPage.tabInstalled.click();\n\n      // Should have the heading, Epinio, and Logs Explorer.\n      await expect(extensionsPage.page.getByRole('row')).toHaveCount(3);\n      await screenshot.take('Extensions-Installed');\n\n      await extensionsPage.tabCatalog.click();\n    });\n  });\n\n  test.describe('Preferences Page', () => {\n    let prefScreenshot: PreferencesScreenshots;\n    let preferencesPage: Page;\n    let e2ePreferences: PreferencesPage;\n\n    test.beforeAll(async({ colorScheme }) => {\n      await navPage.preferencesButton.click();\n      await electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n      preferencesPage = electronApp.windows()[1];\n      await preferencesPage.emulateMedia({ colorScheme });\n      e2ePreferences = new PreferencesPage(preferencesPage);\n      prefScreenshot = new PreferencesScreenshots(preferencesPage, e2ePreferences, { directory: `${ colorScheme }/preferences`, log: console });\n    });\n\n    test.afterAll(async({ colorScheme }) => {\n      await preferencesPage.close({ runBeforeUnload: true });\n    });\n\n    test.describe('Application Page', () => {\n      test('General Tab', async({ colorScheme }) => {\n        // enable Apply button\n        await e2ePreferences.application.automaticUpdatesCheckbox.click();\n        await preferencesPage.waitForTimeout(200);\n\n        await prefScreenshot.take('application', 'tabGeneral');\n      });\n\n      test('Behavior Tab', async() => {\n        await e2ePreferences.application.nav.click();\n        await e2ePreferences.application.tabBehavior.click();\n        await expect(e2ePreferences.application.autoStart).toBeVisible();\n        await prefScreenshot.take('application', 'tabBehavior');\n      });\n\n      test('Environment Tab', async() => {\n        test.skip( isWin, 'Linux & Mac only test');\n\n        await e2ePreferences.application.nav.click();\n        await e2ePreferences.application.tabEnvironment.click();\n        await expect(e2ePreferences.application.pathManagement).toBeVisible();\n        await prefScreenshot.take('application', 'tabEnvironment');\n      });\n    });\n\n    test.describe('WSL Page', () => {\n      test.skip( !isWin, 'Windows only test');\n\n      test('Network Tab', async() => {\n        await e2ePreferences.wsl.nav.click();\n        await prefScreenshot.take('wsl', 'tabNetwork');\n      });\n\n      test('Integrations Tab', async() => {\n        await e2ePreferences.wsl.tabIntegrations.click();\n        await expect(e2ePreferences.wsl.wslIntegrations).toBeVisible();\n        await prefScreenshot.take('wsl', 'tabIntegrations');\n      });\n\n      test('Proxy Tab', async() => {\n        await e2ePreferences.wsl.tabProxy.click();\n        await expect(e2ePreferences.wsl.addressTitle).toBeVisible();\n        await prefScreenshot.take('wsl', 'tabProxy');\n      });\n    });\n\n    test.describe('Virtual Machine Page', () => {\n      test.skip(isWin, 'Linux & Mac only tests');\n\n      test('Hardware Tab', async() => {\n        await e2ePreferences.virtualMachine.nav.click();\n        await expect(e2ePreferences.virtualMachine.memory).toBeVisible();\n        await prefScreenshot.take('virtualMachine', 'tabHardware');\n      });\n\n      test('VolumesTab', async() => {\n        await e2ePreferences.virtualMachine.tabVolumes.click();\n        await expect(e2ePreferences.virtualMachine.mountType).toBeVisible();\n        await prefScreenshot.take('virtualMachine', 'tabVolumes');\n\n        await e2ePreferences.virtualMachine.ninep.click();\n        await expect(e2ePreferences.virtualMachine.ninep).toBeChecked();\n        await page.waitForTimeout(afterCheckedTimeout);\n        await prefScreenshot.take('virtualMachine', 'tabVolumes_9P');\n      });\n\n      test.describe('Mac only tests', () => {\n        test.skip(!isMac, 'Mac only test');\n\n        test('EmulationTab', async() => {\n          await e2ePreferences.virtualMachine.tabEmulation.click();\n          await expect(e2ePreferences.virtualMachine.vmType).toBeVisible();\n          await prefScreenshot.take('virtualMachine', 'tabEmulation');\n\n          // If applicable, switch to VZ so we can use virtiofs.\n          if (await e2ePreferences.virtualMachine.vz.isEnabled()) {\n            await page.waitForTimeout(afterCheckedTimeout);\n            await expect(e2ePreferences.virtualMachine.vz).toBeVisible();\n            await e2ePreferences.virtualMachine.vz.click({ position: { x: 10, y: 10 } });\n            await expect(e2ePreferences.virtualMachine.vz).toBeChecked();\n          }\n        });\n\n        test('VolumesTab-virtiofs', async() => {\n          await e2ePreferences.virtualMachine.tabVolumes.click();\n          if (await e2ePreferences.virtualMachine.virtiofs.isEnabled()) {\n            await e2ePreferences.virtualMachine.virtiofs.click();\n            await expect(e2ePreferences.virtualMachine.virtiofs).toBeChecked();\n            await page.waitForTimeout(afterCheckedTimeout);\n            await prefScreenshot.take('virtualMachine', 'tabVolumes_virtiofs');\n          }\n        });\n\n        test('EmulationTab-vz', async() => {\n          await e2ePreferences.virtualMachine.tabEmulation.click();\n          if (await e2ePreferences.virtualMachine.vz.isEnabled()) {\n            await prefScreenshot.take('virtualMachine', 'tabEmulation_vz');\n          }\n        });\n      });\n    });\n\n    test.describe('Container Engine Page', () => {\n      test('GeneralTab', async() => {\n        await prefScreenshot.take('containerEngine', 'tabGeneral');\n      });\n\n      test('AllowedImagesTab', async() => {\n        await e2ePreferences.containerEngine.nav.click();\n        await e2ePreferences.containerEngine.tabAllowedImages.click();\n        await expect(e2ePreferences.containerEngine.allowedImages).toBeVisible();\n        await e2ePreferences.containerEngine.allowedImagesCheckbox.click();\n        await page.waitForTimeout(afterCheckedTimeout);\n\n        await prefScreenshot.take('containerEngine', 'tabAllowedImages');\n      });\n    });\n\n    test('Kubernetes Page', async() => {\n      await prefScreenshot.take('kubernetes');\n    });\n  });\n\n  test.describe('Preferences Page, locked fields', () => {\n    // ToDo, locked fields tooltips are not captured on Windows.\n\n    let prefScreenshot: PreferencesScreenshots;\n    let prefPage: Page;\n    let preferencesPage: Page;\n    let e2ePreferences: PreferencesPage;\n\n    test.beforeAll(async({ colorScheme }) => {\n      await navPage.preferencesButton.click();\n\n      prefPage = await electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n      // Mock locked Fields API response\n      await prefPage.route(/^.*\\/settings\\/locked/, async(route) => {\n        await route.fulfill(lockedSettings);\n      });\n\n      preferencesPage = electronApp.windows()[1];\n      await preferencesPage.emulateMedia({ colorScheme });\n      e2ePreferences = new PreferencesPage(preferencesPage);\n      prefScreenshot = new PreferencesScreenshots(preferencesPage, e2ePreferences,\n        { directory: `${ colorScheme }/preferences`, log: console });\n    });\n\n    test.afterAll(async({ colorScheme }) => {\n      await preferencesPage.close({ runBeforeUnload: true });\n    });\n\n    test('Allowed Images - locked fields', async() => {\n      await e2ePreferences.containerEngine.nav.click();\n      await e2ePreferences.containerEngine.tabAllowedImages.click();\n      await expect(e2ePreferences.containerEngine.allowedImages).toBeVisible();\n\n      await e2ePreferences.containerEngine.enabledLockedField.hover();\n      await preferencesPage.waitForTimeout(250);\n      await prefScreenshot.take('containerEngine', 'tabAllowedImages_lockedFields');\n    });\n\n    test('Kubernetes - locked fields', async() => {\n      await e2ePreferences.kubernetes.nav.click();\n      await expect(e2ePreferences.kubernetes.kubernetesVersionLockedFields).toBeVisible();\n\n      await e2ePreferences.kubernetes.kubernetesVersionLockedFields.hover();\n      await preferencesPage.waitForTimeout(250);\n      await prefScreenshot.take('kubernetes', 'lockedFields');\n    });\n  });\n\n  test('Intro Image', async({ colorScheme }) => {\n    await navPage.navigateTo('General');\n    const bounds = await navPage.page.evaluate(() => {\n      window.resizeTo(1024, 768);\n\n      return {\n        top: window.screenTop, left: window.screenLeft, width: window.outerWidth, height: window.outerHeight,\n      };\n    });\n\n    await navPage.preferencesButton.click();\n    await electronApp.waitForEvent('window', page => /preferences/i.test(page.url()));\n    const preferencesPage = electronApp.windows()[1];\n\n    await preferencesPage.evaluate((bounds) => {\n      const {\n        top, left, width, height,\n      } = bounds;\n\n      window.moveTo(left + (width - window.outerWidth) / 2, top + (height - window.outerHeight) / 2);\n    }, bounds);\n\n    try {\n      await preferencesPage.emulateMedia({ colorScheme });\n      await preferencesPage.waitForTimeout(250);\n      await preferencesPage.bringToFront();\n      await screenshot.take('intro', true);\n    } finally {\n      preferencesPage.close({ runBeforeUnload: true });\n    }\n  });\n});\n"
  },
  {
    "path": "screenshots/set-display-resolution.ps1",
    "content": "<#\n.SYNOPSIS\n  Set the display resolution on the current monitor to a pre-set size.\n.DESCRIPTION\n  Set the current display to at least the desired size, for use with CI where\n  the display is smaller than expected.\n#>\n\nParam(\n  # The minimum width to set; return an error if this is not supported.\n  [UInt32]$MinimumWidth = 1440,\n  # The minimum height to set; return an error if this is not supported.\n  [UInt32]$MinimumHeight = 900,\n  # The minimum bits per pixel to set; return an error if this is not supported.\n  [UInt32]$MinimumBitsPerPixel = 32,\n  # If set, actually change the resolution.\n  [PSDefaultValue(Help='True, if running in CI; otherwise false')]\n  [switch]$ChangeResolution = !!$ENV:CI\n)\n\n$cSharpSource = @'\n\nusing System;\nusing System.Runtime.InteropServices;\n\n[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Auto)]\ninternal struct DEVMODE {\n  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]\n  public string dmDeviceName;\n  public UInt16 dmSpecVersion;\n  public UInt16 dmDriverVersion;\n  public UInt16 dmSize;\n  public UInt16 dmDriverExtra;\n  public UInt32 dmFields;\n\n  public Int32 dmPositionX;\n  public Int32 dmPositionY;\n  public UInt32 dmDisplayOrientation;\n  public UInt32 dmDisplayFixedOutput;\n\n  public short dmColor;\n  public short dmDuplex;\n  public short dmVerticalResolution;\n  public short dmTTOption;\n  public short dmCollate;\n  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]\n  public string dmFormName;\n  public UInt16 dmLogPixels;\n  public UInt32 dmBitsPerPixel;\n  public UInt32 dmPixelsWidth;\n  public UInt32 dmPixelsHeight;\n  public UInt32 dmDisplayFlags;\n  public UInt32 dmDisplayFrequency;\n}\n\ninternal class User32 {\n  public const UInt32 ENUM_CURRENT_SETTINGS = unchecked((uint)-1);\n  [DllImport(\"user32.dll\", SetLastError = true, CharSet = CharSet.Unicode)]\n  public static extern Int32 EnumDisplaySettings(\n    String deviceName,\n    UInt32 modeNum,\n    ref DEVMODE devMode);\n\n  public const Int32 DISP_CHANGE_SUCCESSFUL = 0;\n  [DllImport(\"user32.dll\", CharSet = CharSet.Unicode)]\n  public static extern Int32 ChangeDisplaySettings(\n    ref DEVMODE devMode,\n    UInt32 flags);\n}\n\npublic class DisplayResolution {\n  static public void SetResolution(bool changeResolution, UInt32 MinWidth, UInt32 MinHeight, UInt32 MinBitsPerPixel) {\n    DEVMODE dm = new DEVMODE(), bestMode = new DEVMODE();\n    ulong bestSize = 0;\n    dm.dmDeviceName = new String(new char[32]);\n    dm.dmFormName = new String(new char[32]);\n    dm.dmSize = (UInt16)Marshal.SizeOf(dm);\n\n    var rv = User32.EnumDisplaySettings(null, User32.ENUM_CURRENT_SETTINGS, ref dm);\n    if (rv == 0) {\n      int error = Marshal.GetLastWin32Error();\n      throw new InvalidOperationException(\n        String.Format(\"Failed to get current display settings: {0:x}\", error));\n    }\n\n    Console.WriteLine(String.Format(\n      \"Current display is {0}x{1} ({2})\",\n      dm.dmPixelsWidth, dm.dmPixelsHeight, dm.dmBitsPerPixel));\n\n    for (UInt32 i = 0; ; i++) {\n      rv = User32.EnumDisplaySettings(null, i, ref dm);\n      if (rv == 0) {\n        break;\n      }\n      Console.WriteLine(String.Format(\n        \"#{0,3} {1,6}x{2,-6} ({3})\", i, dm.dmPixelsWidth, dm.dmPixelsHeight, dm.dmBitsPerPixel));\n      if (dm.dmPixelsWidth >= MinWidth && dm.dmPixelsHeight >= MinHeight && dm.dmBitsPerPixel >= MinBitsPerPixel) {\n        if (dm.dmPixelsWidth * dm.dmPixelsHeight > bestSize) {\n          bestSize = dm.dmPixelsWidth * dm.dmPixelsHeight;\n          bestMode = dm;\n        }\n      }\n    }\n\n    if (bestSize < 1) {\n      throw new NotSupportedException(\"Desired resolution is not found\");\n    }\n    Console.WriteLine(String.Format(\n      \"Picking resolution: {0}x{1} ({2})\",\n      bestMode.dmPixelsWidth, bestMode.dmPixelsHeight, bestMode.dmBitsPerPixel));\n    if (changeResolution) {\n      rv = User32.ChangeDisplaySettings(ref bestMode, 0);\n      if (rv != User32.DISP_CHANGE_SUCCESSFUL) {\n        throw new InvalidOperationException(\n          String.Format(\"Failed to change resolution: {0}\", rv));\n      }\n    }\n\n    rv = User32.EnumDisplaySettings(null, User32.ENUM_CURRENT_SETTINGS, ref dm);\n    if (rv == 0) {\n      int error = Marshal.GetLastWin32Error();\n      throw new InvalidOperationException(\n        String.Format(\"Failed to get modified display settings: {0:x}\", error));\n    }\n\n    Console.WriteLine(String.Format(\n      \"Modified display is {0}x{1}\",\n      dm.dmPixelsWidth, dm.dmPixelsHeight));\n  }\n}\n\n'@\n\nAdd-Type $cSharpSource\n[DisplayResolution]::SetResolution($ChangeResolution, $MinimumWidth, $MinimumHeight, $MinimumBitsPerPixel)\n"
  },
  {
    "path": "screenshots/test-data/containers.ts",
    "content": "export const containersList = [{\n  Image:   'postgres:15',\n  Command: '\"docker-entrypoint.sh postgres\"',\n  Status:  'Up About 22 minutes ago',\n  Id:      'b253b86ddaca501c0f542564d086b7535ed015faa323f0f8df8fccc38c0c8ee0',\n  ImageID: 'sha256:7317fa7ddf4f0870b999784a8ff3f5d8f180fa43e0b894394cc3d8f3aa6cdbd9',\n  Mounts:  [\n    {\n      Type:        'volume',\n      Name:        'desktop_penpot_postgres_v15',\n      Source:      '/var/lib/docker/volumes/desktop_penpot_postgres_v15/_data',\n      Destination: '/var/lib/postgresql/data',\n      Driver:      'local',\n      Mode:        'z',\n      RW:          true,\n      Propagation: '',\n    },\n  ],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'desktop_penpot',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'always',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove:   false,\n    VolumeDriver: '',\n    VolumesFrom:  null,\n    ConsoleSize:  [\n      0,\n      0,\n    ],\n    Mounts: [\n      {\n        Type:          'volume',\n        Source:        'desktop_penpot_postgres_v15',\n        Target:        '/var/lib/postgresql/data',\n        VolumeOptions: {},\n      },\n    ],\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      { '5432/tcp': null },\n  Labels:     {\n    'com.docker.compose.config-hash':          '6bc84b873e5a54c963a2a5beb0468a9c0739073acccf25087690737d7d620b65',\n    'com.docker.compose.container-number':     '1',\n    'com.docker.compose.depends_on':           '',\n    'com.docker.compose.image':                'sha256:7317fa7ddf4f0870b999784a8ff3f5d8f180fa43e0b894394cc3d8f3aa6cdbd9',\n    'com.docker.compose.oneoff':               'False',\n    'com.docker.compose.project':              'web-compose',\n    'com.docker.compose.project.config_files': '/Users/USER/Desktop/docker-compose.yaml',\n    'com.docker.compose.project.working_dir':  '/Users/USER/Desktop',\n    'com.docker.compose.service':              'penpot-postgres',\n    'com.docker.compose.version':              '2.17.3',\n  },\n  State: 'running',\n  Names: [\n    'desktop-penpot-postgres-1',\n  ],\n  Created:          null,\n  state:            'running',\n  containerName:    'desktop-penpot-postgres-1',\n  started:          'Up About 22 minutes ago',\n  imageName:        'postgres:15',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:   'redis:7',\n  Command: '\"docker-entrypoint.sh redis-server\"',\n  Status:  'Up About a minute',\n  Id:      '68c028de950c6fae763ec4dfd0d5f2574feca88c3480ba9db94c7cf3e43a0f23',\n  ImageID: 'sha256:c4645622ca3919b60b2d3a377c438b9d5de65cf76a63ca4c025733909db8c9ef',\n  Mounts:  [\n    {\n      Type:        'volume',\n      Name:        '2014788e23e024376e8e9897b9371608b24f0d5ef55642b22d941ff59edf7faa',\n      Source:      '/var/lib/docker/volumes/2014788e23e024376e8e9897b9371608b24f0d5ef55642b22d941ff59edf7faa/_data',\n      Destination: '/data',\n      Driver:      'local',\n      Mode:        '',\n      RW:          true,\n      Propagation: '',\n    },\n  ],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'desktop_penpot',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'always',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove:   false,\n    VolumeDriver: '',\n    VolumesFrom:  null,\n    ConsoleSize:  [\n      0,\n      0,\n    ],\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      { '6379/tcp': null },\n  Labels:     {\n    'com.docker.compose.config-hash':          '360cdded21f67d032e99d8a69c9e662fa23a17dc7ee6701547a659a3b76e13f5',\n    'com.docker.compose.container-number':     '1',\n    'com.docker.compose.depends_on':           '',\n    'com.docker.compose.image':                'sha256:c4645622ca3919b60b2d3a377c438b9d5de65cf76a63ca4c025733909db8c9ef',\n    'com.docker.compose.oneoff':               'False',\n    'com.docker.compose.project':              'web-compose',\n    'com.docker.compose.project.config_files': '/Users/USER/Desktop/docker-compose.yaml',\n    'com.docker.compose.project.working_dir':  '/Users/USER/Desktop',\n    'com.docker.compose.service':              'penpot-redis',\n    'com.docker.compose.version':              '2.17.3',\n  },\n  State: 'running',\n  Names: [\n    'desktop-penpot-redis-1',\n  ],\n  Created:          null,\n  state:            'running',\n  containerName:    'desktop-penpot-redis-1',\n  started:          'Up About 2 hours ago',\n  imageName:        'redis:7',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:      'sj26/mailcatcher:latest',\n  Command:    '\"mailcatcher --foreground --ip 0.0.0.0\"',\n  Status:     'Up About 2 hours ago',\n  Id:         '0ce3c8e5eea2493472e57730ae52298e8d26231ca93bfb6e20120af2626b4e0a',\n  ImageID:    'sha256:d06b19d398e73bce5c61f91c2564085972c83e698dbfc0b968efea0ae0f86413',\n  Mounts:     [],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:  'desktop_penpot',\n    PortBindings: {\n      '1080/tcp': [\n        {\n          HostIp:   '',\n          HostPort: '1080',\n        },\n      ],\n    },\n    RestartPolicy: {\n      Name:              'always',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove:   false,\n    VolumeDriver: '',\n    VolumesFrom:  null,\n    ConsoleSize:  [\n      0,\n      0,\n    ],\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      {\n    '1025/tcp': null,\n    '1080/tcp': [\n      {\n        HostIp:   '0.0.0.0',\n        HostPort: '1080',\n      },\n      {\n        HostIp:   '::',\n        HostPort: '1080',\n      },\n    ],\n  },\n  Labels: {\n    'com.docker.compose.config-hash':          '60eae7f8e5567664c487ca61224cb2b196f1a33d719968da8fee5badc5a5087b',\n    'com.docker.compose.container-number':     '1',\n    'com.docker.compose.depends_on':           '',\n    'com.docker.compose.image':                'sha256:d06b19d398e73bce5c61f91c2564085972c83e698dbfc0b968efea0ae0f86413',\n    'com.docker.compose.oneoff':               'False',\n    'com.docker.compose.project':              'web-compose',\n    'com.docker.compose.project.config_files': '/Users/USER/Desktop/docker-compose.yaml',\n    'com.docker.compose.project.working_dir':  '/Users/USER/Desktop',\n    'com.docker.compose.service':              'penpot-mailcatch',\n    'com.docker.compose.version':              '2.17.3',\n  },\n  State: 'running',\n  Names: [\n    'desktop-penpot-mailcatch-1',\n  ],\n  Created:          null,\n  state:            'running',\n  containerName:    'desktop-penpot-mailcatch-1',\n  started:          'Up About a minute',\n  imageName:        'sj26/mailcatcher:latest',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:      'busybox',\n  Command:    '\"--name bb\"',\n  Status:     'Created',\n  Id:         'be8a7ea2bb7e8d714171f54d91e8ae322ae418a0fc36fbbadbdcc055c51185c0',\n  ImageID:    'sha256:fc9db2894f4e4b8c296b8c9dab7e18a6e78de700d21bc0cfaf5c78484226db9c',\n  Mounts:     [],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'default',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'no',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove:   false,\n    VolumeDriver: '',\n    VolumesFrom:  null,\n    ConsoleSize:  [\n      22,\n      96,\n    ],\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      {},\n  Labels:     {},\n  State:      'created',\n  Names:      [\n    'sad_lovelace',\n  ],\n  Created:          null,\n  state:            'created',\n  containerName:    'sad_lovelace',\n  started:          '',\n  imageName:        'busybox',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:   'redis',\n  Command: '\"docker-entrypoint.sh --name r1\"',\n  Status:  'Exited (1) 6 days ago',\n  Id:      'de3f19652d8d05582c6995d6cb2f7b9e55a7cdc0dfc70a383f9a5769b81ae440',\n  ImageID: 'sha256:db32f19a80e6724015fc6a6f4c99731a7d4f88809a6e227313f19e1cde872734',\n  Mounts:  [\n    {\n      Type:        'volume',\n      Name:        '808bbbe0bd45dbfda7ef439e41010f8ff4f18b1e0d21c76fa06583c49a5bf1ca',\n      Source:      '/var/lib/docker/volumes/808bbbe0bd45dbfda7ef439e41010f8ff4f18b1e0d21c76fa06583c49a5bf1ca/_data',\n      Destination: '/data',\n      Driver:      'local',\n      Mode:        '',\n      RW:          true,\n      Propagation: '',\n    },\n  ],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'default',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'no',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove:   false,\n    VolumeDriver: '',\n    VolumesFrom:  null,\n    ConsoleSize:  [\n      22,\n      116,\n    ],\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      {},\n  Labels:     {},\n  State:      'exited',\n  Names:      [\n    'affectionate_mcnulty',\n  ],\n  Created:          null,\n  state:            'exited',\n  containerName:    'affectionate_mcnulty',\n  started:          '',\n  imageName:        'redis',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:      'nginx:1.21-alpine',\n  Command:    '\"/docker-entrypoint.sh nginx -g \\'daemon off;\\'\"',\n  Status:     'Up 3 hours',\n  Id:         'k8s_nginx_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  ImageID:    'sha256:a6eb2a334a9f2e5c9d8e7f012345678901234567890abcdef1234567890abcdef',\n  Mounts:     [],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'none',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'no',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove: false,\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      {},\n  Labels:     {\n    'io.kubernetes.container.name': 'nginx',\n    'io.kubernetes.pod.name':        'nginx-deployment-5d5b7c79d6-abcde',\n    'io.kubernetes.pod.namespace':   'default',\n    'io.kubernetes.pod.uid':         '12345678-1234-1234-1234-123456789abc',\n    app:                            'nginx',\n    'pod-template-hash':             '5d5b7c79d6',\n  },\n  State: 'running',\n  Names: [\n    'k8s_nginx_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  ],\n  Created:          null,\n  state:            'running',\n  containerName:    'k8s_nginx_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  started:          'Up 3 hours',\n  imageName:        'nginx:1.21-alpine',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n},\n{\n  Image:      'registry.k8s.io/pause:3.9',\n  Command:    '\"/pause\"',\n  Status:     'Up 3 hours',\n  Id:         'k8s_POD_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  ImageID:    'sha256:e6f1816883972d4be47bd48879a08919b96afcd344132622e4d444987919323c',\n  Mounts:     [],\n  HostConfig: {\n    Binds:           null,\n    ContainerIDFile: '',\n    LogConfig:       {\n      Type:   'json-file',\n      Config: {},\n    },\n    NetworkMode:   'container:1234567890abcdef',\n    PortBindings:  {},\n    RestartPolicy: {\n      Name:              'no',\n      MaximumRetryCount: 0,\n    },\n    AutoRemove: false,\n  },\n  SizeRootFs: -1,\n  SizeRw:     -1,\n  Ports:      { '80/tcp': null },\n  Labels:     {\n    'io.kubernetes.container.name': 'POD',\n    'io.kubernetes.pod.name':        'nginx-deployment-5d5b7c79d6-abcde',\n    'io.kubernetes.pod.namespace':   'default',\n    'io.kubernetes.pod.uid':         '12345678-1234-1234-1234-123456789abc',\n  },\n  State: 'running',\n  Names: [\n    'k8s_POD_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  ],\n  Created:          null,\n  state:            'running',\n  containerName:    'k8s_POD_nginx-deployment-5d5b7c79d6-abcde_default_12345678-1234-1234-1234-123456789abc_0',\n  started:          'Up 3 hours',\n  imageName:        'registry.k8s.io/pause:3.9',\n  availableActions: [\n    {\n      label:      'Stop',\n      action:     'stopContainer',\n      enabled:    true,\n      bulkable:   true,\n      bulkAction: 'stopContainers',\n    },\n    {\n      label:      'Start',\n      action:     'startContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'startContainer',\n    },\n    {\n      label:      'Delete',\n      action:     'deleteContainer',\n      enabled:    false,\n      bulkable:   true,\n      bulkAction: 'deleteContainers',\n    },\n  ],\n}] as const;\n"
  },
  {
    "path": "screenshots/test-data/images.ts",
    "content": "export const imagesList = [\n  {\n    imageName: 'nginx',\n    tag:       'latest',\n    imageID:   '553f64aecdc3',\n    size:      '185.5MB',\n    digest:    'sha256:553f64aecdc31b5bf944521731cd70e35da4faed96b2b7548a3d8e2598c52a42',\n  },\n  {\n    imageName: 'rancher/k3s',\n    tag:       'latest',\n    imageID:   '5e0707cfd123',\n    size:      '233.5MB',\n    digest:    'sha256:5e0707cfd1239b358ef73f3254bc3eadc027dd30cd5ec6ca41e29e47652a1b8c',\n  },\n  {\n    imageName: 'rancher/rancher',\n    tag:       'latest',\n    imageID:   '5a26e0918b42',\n    size:      '1.723GB',\n    digest:    'sha256:5a26e0918b425bd91e9be0678e39d7be08f3f023de42622008894aad7db10080',\n  },\n  {\n    imageName: 'registry.opensuse.org/opensuse/leap',\n    tag:       'latest',\n    imageID:   '999adf320e40',\n    size:      '144.4MB',\n    digest:    'sha256:999adf320e40662dc96119a14f07459af9959a081d10ccab7c405257030ab96b',\n  },\n];\n"
  },
  {
    "path": "screenshots/test-data/preferences.ts",
    "content": "export const lockedSettings = {\n  body: JSON.stringify({\n    containerEngine: {\n      allowedImages: {\n        enabled:  true,\n        patterns: true,\n      },\n    },\n    kubernetes: { version: true },\n  }),\n  status:  200,\n  headers: {},\n};\n"
  },
  {
    "path": "screenshots/test-data/snapshots.ts",
    "content": "import dayjs from 'dayjs';\n\nexport const snapshotsList = {\n  body: JSON.stringify([{\n    name:    'Snapshot 1',\n    created: dayjs(new Date(), 'YYYY-MM-DD_HH_mm_ss').subtract(5, 'minute'),\n  }, {\n    name:    'Snapshot 2',\n    created: dayjs(new Date(), 'YYYY-MM-DD_HH_mm_ss'),\n  }]),\n  status:  200,\n  headers: {},\n};\n"
  },
  {
    "path": "screenshots/test-data/volumes.ts",
    "content": "export const volumesList = new Promise((resolve) => {\n  resolve([\n    {\n      Name:       'desktop_penpot_postgres_v15',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/desktop_penpot_postgres_v15/_data',\n      CreatedAt:  '2025-01-15T10:30:00Z',\n      Labels:     {\n        'com.docker.compose.project': 'desktop',\n        'com.docker.compose.service': 'penpot-postgres',\n        'com.docker.compose.version': '2.17.3',\n      },\n      Scope:   'local',\n      Options: null,\n    },\n    {\n      Name:       'redis-data',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/redis-data/_data',\n      CreatedAt:  '2025-01-12T14:22:00Z',\n      Labels:     {},\n      Scope:      'local',\n      Options:    null,\n    },\n    {\n      Name:       'nginx-config',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/nginx-config/_data',\n      CreatedAt:  '2025-01-10T09:15:00Z',\n      Labels:     {\n        app:         'web-server',\n        environment: 'production',\n      },\n      Scope:   'local',\n      Options: null,\n    },\n    {\n      Name:       'mongodb-storage',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/mongodb-storage/_data',\n      CreatedAt:  '2025-01-08T16:45:00Z',\n      Labels:     {\n        database: 'mongodb',\n        version:  '7.0',\n      },\n      Scope:   'local',\n      Options: null,\n    },\n    {\n      Name:       'app-logs',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/app-logs/_data',\n      CreatedAt:  '2025-01-05T11:30:00Z',\n      Labels:     {\n        purpose:   'logging',\n        retention: '30-days',\n      },\n      Scope:   'local',\n      Options: null,\n    },\n    {\n      Name:       'backup-volume',\n      Driver:     'local',\n      Mountpoint: '/var/lib/docker/volumes/backup-volume/_data',\n      CreatedAt:  '2024-12-28T08:00:00Z',\n      Labels:     {\n        backup:   'daily',\n        critical: 'true',\n      },\n      Scope:   'local',\n      Options: null,\n    },\n  ]);\n});\n"
  },
  {
    "path": "scripts/assets/extension-data.yaml",
    "content": "# This file contains the extension versions we list in the marketplace.\n# This is processed by `yarn generate:extension-data` to produce\n# pkg/rancher-desktop/assets/extension-data.yaml\n\n# Each top level key is the extension image + version.\n# They may have properties; the current known properties are:\n# - `containerd_compatible`: defaults to true.\n# - `logo`: Overrides the logo URL.\n# - `github_repo`: GitHub repository, used for version updates.\n\nrancher/application-collection-extension:0.5.2:\n  github_repo: rancherlabs/application-collection-extension\nghcr.io/rancher-sandbox/rancher-desktop-rdx-ai-workbench:0.2.0:\n  github_repo: rancher-sandbox/rancher-desktop-rdx-ai-workbench\nsplatform/epinio-docker-desktop:0.1.3:\n  github_repo: epinio/extension-docker-desktop\njulianb90/tachometer:0.1.1: {}\ndocker/logs-explorer-extension:0.2.5: {}\nprakhar1989/dive-in:0.0.8:\n  containerd_compatible: false\njoycelin79/newman-extension:0.0.7: {}\ndocker/resource-usage-extension:1.0.3: {}\nanchore/docker-desktop-extension:0.5.1:\n  containerd_compatible: false\nignatandrei/blockly-automation:0.0.7: {}\ndocker/disk-usage-extension:0.2.8:\n  containerd_compatible: false\nharpooncorp/harpoon-ext:0.0.6: {}\nvklokun/docker-desktop-extension:0.1.1: {}\ncaretdev/intersystems-extension:0.1.7: {}\n"
  },
  {
    "path": "scripts/assets/options.go.templ",
    "content": "// Code generated by running `yarn postinstall` DO NOT EDIT.\n//\n// To rebuild this file manually, run\n// node scripts/ts-wrapper.js scripts/generateCliCode.ts pkg/rancher-desktop/assets/specs/command-api.yaml \\\n//     src/go/rdctl/pkg/options/generated/options.go\n\n/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage options\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strings\"\n\t\"strconv\"\n\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/pflag\"\n)\n\n/**\n * The two types `ServerSettingsForJSON` and `serverSettings` both reflect the settings type in the\n * backend (as defined in `config/settings.ts`), but have different uses.\n *\n * As its name implies, the `ServerSettingsForJSON` is used to generate a backend for the `rdctl set`\n * subcommand. Only fields that are explicitly changed by the user should be inserted into the JSON\n * payload, so we use pointers that are by default nil. This way we don't inadvertently change backend\n * settings to their default values.\n *\n * The problem with using pointers in a struct rather than non-pointer types is that we can't use\n * them with the golang `cmd.Flags().XVar` functions (where `X` is one of `Bool`, `String`, or `Int`).\n * We use the non-pointer fields in the `serverSettings` structure to hold them.\n *\n * See how the two structs are used in the `UpdateFieldsForJSON` function.\n */\n\ntype ServerSettingsForJSON struct {\n\t<%- linesForJSON %>\n}\n\n\ntype serverSettings struct {\n\t<%- linesWithoutJSON %>\n}\n\nconst CURRENT_SETTINGS_VERSION = <%- settingsVersion %>\nvar specifiedSettings serverSettings\n\n/**\n * When an enum array is given with an option,\n * check that a specified value for that option is in its `allowedValues` list.\n */\nfunc enumStringCheck(option string, specified string, allowedValues []string) error {\n\tnumVals := len(allowedValues)\n\tsingleQuotedAllowedValues := make([]string, numVals)\n\tfor i, val := range allowedValues {\n\t\tif specified == val {\n\t\t\treturn nil\n\t\t}\n\t\tsingleQuotedAllowedValues[i] = fmt.Sprintf(\"'%s'\", val)\n\t}\n\tvar allowedString string\n\tif numVals == 1 {\n\t\tallowedString = singleQuotedAllowedValues[0]\n\t} else if numVals == 2 {\n\t\tallowedString = strings.Join(singleQuotedAllowedValues, \" or \")\n\t} else {\n\t\tfirstPart := strings.Join(singleQuotedAllowedValues[:numVals - 1], \", \")\n\t\tallowedString = fmt.Sprintf(\"%s, or %s\", firstPart,  singleQuotedAllowedValues[numVals - 1])\n\t}\n\treturn fmt.Errorf(`invalid value for option %s: %q; must be %s`, option, specified, allowedString)\n}\n\nfunc qualifiedPlatformName() string {\n\tif runtime.GOOS == \"darwin\" {\n\t\treturn \"macOS\"\n\t}\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \"Windows\"\n\t}\n\tif runtime.GOOS != \"linux\" {\n\t\treturn runtime.GOOS\n\t}\n\t// Special case if it's running in a WSL partition, return \"Linux in a WSL partition\"\n\tcontents, err := os.ReadFile(\"/proc/version\")\n\tif err != nil {\n\t\treturn \"Linux\"\n\t}\n\tptn, err := regexp.Compile(`^Linux version \\S+?-microsoft-standard-WSL2`)\n\tif err != nil {\n\t\treturn \"Linux\"\n\t}\n\tmatch := ptn.Match(contents)\n\tif match && err == nil {\n\t\treturn \"Linux in a WSL partition\"\n\t}\n\treturn \"Linux\"\n}\n\nfunc UpdateCommonStartAndSetCommands(cmd *cobra.Command) {\n\t<%_ for (const flag of commandFlags) {\n      if (flag.flagType === 'Array') {\n        continue;\n      }\n\t\t\tconst kebabPropertyName = kebabCase(flag.propertyName); _%>\n\t\tcmd.Flags().<%- flag.flagType %>Var(&specifiedSettings.<%- flag.capitalizedName %>, \"<%- kebabPropertyName %>\", <%- flag.defaultValue %>, \"<%- flag.usageNote %>\")\n\t\t<%_ if (flag.aliasFor || flag.notAvailable) { _%>\n\t\tcmd.Flags().MarkHidden(\"<%- kebabPropertyName %>\")\n\t\t<%_ } _%>\n\t<%_ } _%>\n}\n\nfunc UpdateFieldsForJSON(flags *pflag.FlagSet) (*ServerSettingsForJSON, error) {\n\tvar specifiedSettingsForJSON ServerSettingsForJSON\n\tchangedSomething := false\n\t<%_ for (const flag of commandFlags) {\n\tconst kebabPropertyName = kebabCase(flag.propertyName); _%>\n\t\tif flags.Changed(\"<%- kebabPropertyName %>\") {\n\t\t\t<%_ if (flag.notAvailable) { _%>\n\t\t\t\treturn nil, fmt.Errorf(`option --<%- kebabPropertyName %> is not available on %s`, qualifiedPlatformName())\n\t\t\t<%_ } else { _%>\n\t\t\t\t<%_ if (flag.enums) { _%>\n\t\t\t\t\tif err := enumStringCheck(\"--<%- kebabPropertyName %>\", specifiedSettings.<%- flag.capitalizedName %>, <%- flag.enums %>) ; err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t<%_ } _%>\n\t\t\t\tspecifiedSettingsForJSON.<%- flag.capitalizedName %> = &specifiedSettings.<%- flag.capitalizedName %>\n\t\t\t\tchangedSomething = true\n\t\t\t<%_ } _%>\n\t\t}\n\t<% } %>\n\tif changedSomething {\n\t\tspecifiedSettings.Version = <%- settingsVersion %>\n\t\tspecifiedSettingsForJSON.Version = &specifiedSettings.Version;\n\t\treturn &specifiedSettingsForJSON, nil\n\t}\n\treturn nil, nil\n}\n\nfunc GetCommandLineArgsForStartCommand(flags *pflag.FlagSet) ([]string, error) {\n\tvar commandLineArgs []string\n\t<%_ for (const flag of commandFlags) {\n\t\t\tconst kebabPropertyName = kebabCase(flag.propertyName);\n\t\t\t// The app and backend use dot-separated [Cc]amelCase names, not kebab-case, so pass in the camelCase name (a few lines down)\n\t\t\tconst actualPropertyName = flag.aliasFor || flag.propertyName;\n\t_%>\n\t\tif flags.Changed(\"<%- kebabPropertyName %>\") {\n\t\t\t<%_ if (flag.notAvailable) { _%>\n\t\t\t\treturn nil, fmt.Errorf(\"option --<%- kebabPropertyName %> is not available on %s\", qualifiedPlatformName())\n\t\t\t<% } else { _%>\n\t\t\t\t<%_ if (flag.enums) { _%>\n\t\t\t\tif err := enumStringCheck(\"--<%- kebabPropertyName %>\", specifiedSettings.<%- flag.capitalizedName %>, <%- flag.enums %>) ; err != nil {\n\t\t\t\t\treturn commandLineArgs, err\n\t\t\t\t}\n\t\t\t\t<%_ } _%>\n\t\t\t\tcommandLineArgs = append(commandLineArgs, \"--<%- actualPropertyName %>\"<%- flag.valuePart %>)\n\t\t\t\t<%_ } _%>\n\t\t}\n\t<% } %>\n\treturn commandLineArgs, nil\n}\n"
  },
  {
    "path": "scripts/build.ts",
    "content": "/**\n * This script builds the javascript, without packaging it up for use.  This is\n * mostly useful for more comprehensive checking.\n */\n\n'use strict';\n\nimport * as fs from 'fs/promises';\nimport * as os from 'os';\nimport * as path from 'path';\n\nimport buildUtils from './lib/build-utils';\n\nimport { simpleSpawn } from 'scripts/simple_process';\n\nclass Builder {\n  async cleanup() {\n    console.log('Removing previous builds...');\n    const dirs = [\n      path.resolve(buildUtils.rendererSrcDir, 'dist'),\n      path.resolve(buildUtils.distDir),\n    ];\n    const options = {\n      force: true, maxRetries: 3, recursive: true,\n    };\n\n    await Promise.all(dirs.map(dir => fs.rm(dir, options)));\n\n    if (/^win/i.test(os.platform())) {\n      // On Windows, virus scanners (e.g. the default Windows Defender) like to\n      // hold files open upon deletion(!?) and delay the deletion for a second\n      // or two.  Wait for those directories to actually be gone before\n      // continuing.\n      const waitForDelete = async(dir: string) => {\n        while (true) {\n          try {\n            await fs.stat(dir);\n            await buildUtils.sleep(500);\n          } catch (error: any) {\n            if (error?.code === 'ENOENT') {\n              return;\n            }\n            throw error;\n          }\n        }\n      };\n\n      await Promise.all(dirs.map(waitForDelete));\n    }\n  }\n\n  async buildRenderer() {\n    process.env.VUE_CLI_SERVICE_CONFIG_PATH = 'pkg/rancher-desktop/vue.config.mjs';\n    await simpleSpawn(\n      process.execPath,\n      [\n        '--stack-size=16384',\n        'node_modules/@vue/cli-service/bin/vue-cli-service.js',\n        'build',\n        '--skip-plugins',\n        'eslint',\n      ],\n    );\n  }\n\n  async build() {\n    console.log('Building...');\n    buildUtils.isDevelopment = false;\n    await this.buildRenderer();\n    await buildUtils.buildPreload();\n    await buildUtils.buildMain();\n  }\n\n  async run() {\n    await this.cleanup();\n    await this.build();\n  }\n}\n\n(new Builder()).run().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/check-api-schema.ts",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This script checks the command API schema against the settings file to ensure\n * that we have documented all settings.\n *\n * @note This does not check that we declare enums.\n */\n\nimport fs from 'fs';\n\nimport yaml from 'yaml';\n\nimport { defaultSettings } from '@pkg/config/settings';\nimport { RecursiveReadonly } from '@pkg/utils/typeUtils';\n\nconst schemaPath = 'pkg/rancher-desktop/assets/specs/command-api.yaml';\n\ninterface schemaObject {\n  type:                  'object';\n  properties?:           Record<string, schemaNode>;\n  additionalProperties?: boolean;\n}\ninterface schemaString {\n  type: 'string';\n}\ninterface schemaInteger {\n  type: 'integer';\n}\ninterface schemaBoolean {\n  type: 'boolean';\n}\ninterface schemaArray {\n  type:  'array';\n  items: schemaNode;\n}\ninterface schemaMissing {\n  // This is not a real schema type; it's a stand-in for a missing property.\n  type: '<missing>';\n}\ntype schemaNode = schemaObject | schemaString | schemaInteger | schemaBoolean | schemaArray | schemaMissing;\n\nconst blacklistedPaths = [\n  'version',\n];\n\nfunction makePath(pathParts: string[]): string {\n  switch (pathParts.length) {\n  case 0: return '';\n  case 1: return pathParts[0];\n  }\n\n  const copyParts = [...pathParts];\n  const lastItem = copyParts.pop() ?? '';\n  const lastSeparator = lastItem.startsWith('[') ? '' : '.';\n\n  return `${ copyParts.join('.') }${ lastSeparator }${ lastItem }`;\n}\n\nfunction checkObject(setting: RecursiveReadonly<any>, schema: schemaNode, path: string[] = [], allowMissing = false): string[] {\n  const errors: string[] = [];\n  const pathString = makePath(path);\n\n  function logTypeError(desiredType: schemaNode['type']) {\n    errors.push(`${ pathString } has incorrect type \"${ schema.type }\", should be \"${ desiredType }\"`);\n  }\n\n  if (blacklistedPaths.includes(pathString)) {\n    if (schema.type !== '<missing>') {\n      errors.push(`${ pathString } should not be in the schema`);\n    }\n\n    return errors;\n  }\n\n  if (schema.type === '<missing>') {\n    if (!allowMissing) {\n      errors.push(`${ pathString } is missing in the schema`);\n    }\n\n    return errors;\n  }\n\n  switch (typeof setting) {\n  case 'object': {\n    if (schema.type !== 'object') {\n      if (schema.type === 'array') {\n        if (!Array.isArray(setting)) {\n          logTypeError('object');\n        } else {\n          // check the types of the array's children\n          for (let i = 0; i < setting.length; i++) {\n            errors.push(...checkObject(setting[i], schema.items, path.concat([`[${ i }]`])));\n          }\n        }\n      } else {\n        logTypeError('object');\n      }\n      break;\n    }\n    const schemaProps = schema.properties ?? {} as Record<string, schemaNode>;\n\n    for (const prop in setting) {\n      const propSchema = schemaProps[prop] ?? { type: '<missing>' };\n\n      errors.push(...checkObject(setting[prop], propSchema, path.concat(prop), schema.additionalProperties));\n    }\n    for (const prop in schemaProps) {\n      if (!(prop in setting)) {\n        errors.push(`${ path.concat(prop).join('.') } not found in Settings`);\n      }\n    }\n    break;\n  }\n  case 'boolean':\n    if (schema.type !== 'boolean') {\n      logTypeError('boolean');\n    }\n    break;\n  case 'number':\n    if (schema.type !== 'integer') {\n      logTypeError('integer');\n    }\n    break;\n  case 'string':\n    if (schema.type !== 'string') {\n      logTypeError('string');\n    }\n    break;\n  default:\n    errors.push(`${ pathString } has object of unknown type ${ typeof setting }`);\n  }\n\n  return errors;\n}\n\n(async function() {\n  const schema = yaml.parse(await fs.promises.readFile(schemaPath, { encoding: 'utf-8' }));\n  const errors = checkObject(defaultSettings, schema.components.schemas.preferences);\n\n  if (errors.length > 0) {\n    console.error(`Preferences schema in ${ schemaPath } contains errors:`);\n    for (const error of errors) {\n      console.error(`  ${ error }`);\n    }\n    process.exit(1);\n  }\n  console.log(`Preferences schema ${ schemaPath } appears to be up to date.`);\n})();\n"
  },
  {
    "path": "scripts/dependencies/go-source.ts",
    "content": "import path from 'path';\n\nimport { Dependency, DownloadContext } from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\ninterface GoDependencyOptions {\n  /**\n   * The output file name, relative to the platform-specific resources directory.\n   * If this does not contain any directory separators ('/'), it is assumed to\n   * be a directory name (defaults to `bin`) and the leaf name of the source\n   * path is appended as the executable name.\n   */\n  outputPath: string;\n  /**\n   * Additional environment for the go compiler; e.g. for GOARCH overrides.\n   */\n  env?:       NodeJS.ProcessEnv;\n\n  /**\n   * The version string to be stamped into the binary at build time.\n   * This is typically used with `-ldflags=\"-X ...\"` to embed version information.\n   * Example: `1.18.1`.\n   */\n  version?: string;\n\n  /**\n   * The Go module path, typically as defined in `go.mod`. This should match the\n   * import path of the module (e.g., `github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper`).\n   */\n  modulePath?: string;\n}\n\n/**\n * GoDependency represents a golang binary that is built from the local source\n * code.\n */\nexport class GoDependency implements Dependency {\n  /**\n   * Construct a new GoDependency.\n   * @param sourcePath The path to be compiled, relative to .../src/go\n   * @param options Additional configuration option; if a string is given, this\n   * is the outputPath option, defaulting to `bin`.\n   */\n  constructor(sourcePath: string, options: string | GoDependencyOptions = 'bin') {\n    this.sourcePath = sourcePath;\n    this.options = typeof options === 'string' ? { outputPath: options } : options;\n  }\n\n  get name(): string {\n    if (this.options.outputPath.includes('/')) {\n      return path.basename(this.options.outputPath);\n    }\n\n    return path.basename(this.sourcePath);\n  }\n\n  sourcePath: string;\n  options:    GoDependencyOptions;\n\n  async download(context: DownloadContext): Promise<void> {\n    // Rather than actually downloading anything, this builds the source code.\n    const sourceDir = path.join(process.cwd(), 'src', 'go', this.sourcePath);\n    const outFile = this.outFile(context);\n\n    const ldFlags: string[] = ['-s', '-w'];\n\n    if (this.options.version && this.options.modulePath) {\n      ldFlags.push(`-X ${ this.options.modulePath }/pkg/version.Version=${ this.options.version }`);\n    }\n\n    const buildArgs: string[] = ['build', '-ldflags', ldFlags.join(' '), '-o', outFile, '.'];\n\n    const env = this.environment(context);\n\n    console.log(`Building go utility \\x1B[1;33;40m${ this.name }\\x1B[0m [${ env.GOOS }/${ env.GOARCH }] from ${ sourceDir } to ${ outFile }...`);\n    await simpleSpawn('go', buildArgs, {\n      cwd: sourceDir,\n      env,\n    });\n  }\n\n  environment(context: DownloadContext): NodeJS.ProcessEnv {\n    return {\n      ...process.env,\n      GOOS:   context.goPlatform,\n      GOARCH: context.isM1 ? 'arm64' : 'amd64',\n      ...this.options.env ?? {},\n    };\n  }\n\n  outFile(context: DownloadContext): string {\n    const suffix = context.platform === 'win32' ? '.exe' : '';\n    let outputPath = `${ this.options.outputPath }${ suffix }`;\n\n    if (!this.options.outputPath.includes('/')) {\n      outputPath = `${ this.options.outputPath }/${ this.name }${ suffix }`;\n    }\n\n    return path.join(context.resourcesDir, context.platform, outputPath);\n  }\n}\n\nexport class RDCtl extends GoDependency {\n  constructor(version: string) {\n    super('rdctl', {\n      outputPath: 'bin',\n      modulePath: 'github.com/rancher-sandbox/rancher-desktop/src/go/rdctl',\n      version,\n    });\n  }\n\n  dependencies(context: DownloadContext): string[] {\n    if (context.dependencyPlatform === 'wsl') {\n      // For the WSL copy depend on the Windows one to generate code\n      return ['rdctl:win32'];\n    }\n\n    return [];\n  }\n\n  override async download(context: DownloadContext): Promise<void> {\n    // For WSL, don't re-generate the code; the win32 copy did it.\n    if (context.dependencyPlatform !== 'wsl') {\n      await simpleSpawn('node', ['scripts/ts-wrapper.js',\n        'scripts/generateCliCode.ts',\n        'pkg/rancher-desktop/assets/specs/command-api.yaml',\n        'src/go/rdctl/pkg/options/generated/options.go']);\n    }\n    await super.download(context);\n  }\n}\n\nexport class WSLHelper extends GoDependency {\n  constructor(version: string) {\n    super('wsl-helper', {\n      outputPath: 'internal',\n      env:        { CGO_ENABLED: '0' },\n      modulePath: 'github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper',\n      version,\n    });\n  }\n\n  dependencies(context: DownloadContext): string[] {\n    return ['mobyOpenAPISpec:win32'];\n  }\n}\n\nexport class NerdctlStub extends GoDependency {\n  constructor() {\n    super('nerdctl-stub');\n  }\n\n  override outFile(context: DownloadContext) {\n    // nerdctl-stub is the actual nerdctl binary to be run on linux;\n    // there is also a `nerdctl` wrapper in the same directory to make it\n    // easier to handle permissions for Linux-in-WSL.\n    const leafName = context.platform === 'win32' ? 'nerdctl.exe' : 'nerdctl-stub';\n\n    return path.join(context.resourcesDir, context.platform, 'bin', leafName);\n  }\n}\n\nexport class SpinStub extends GoDependency {\n  constructor() {\n    super('spin-stub');\n  }\n\n  override outFile(context: DownloadContext) {\n    // spin-stub is only used on Windows\n    return path.join(context.resourcesDir, context.platform, 'bin', 'spin.exe');\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/lima.ts",
    "content": "// This downloads the resources related to Lima.\n\nimport childProcess from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport yaml from 'yaml';\n\nimport { download, downloadTarGZ, getResource } from '../lib/download';\n\nimport {\n  AlpineLimaISOVersion,\n  DEP_VERSIONS_PATH,\n  DependencyVersions,\n  DownloadContext,\n  findChecksum,\n  getOctokit,\n  GitHubDependency,\n  GitHubRelease,\n  GlobalDependency,\n} from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\nexport class Lima extends GlobalDependency(GitHubDependency) {\n  readonly name = 'lima';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-lima';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseUrl = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download`;\n    let platform: string = context.platform;\n\n    if (platform === 'darwin') {\n      platform = `macos-15.${ process.env.M1 ? 'arm64' : 'amd64' }`;\n    } else {\n      platform = `linux.${ process.env.M1 ? 'arm64' : 'amd64' }`;\n    }\n\n    const url = `${ baseUrl }/v${ context.versions.lima }/lima.${ platform }.tar.gz`;\n    const expectedChecksum = (await getResource(`${ url }.sha512sum`)).split(/\\s+/)[0];\n    const limaDir = path.join(context.resourcesDir, context.platform, 'lima');\n    const tarPath = path.join(context.resourcesDir, context.platform, `lima.${ platform }.v${ context.versions.lima }.tgz`);\n\n    await download(url, tarPath, {\n      expectedChecksum,\n      checksumAlgorithm: 'sha512',\n      access:            fs.constants.W_OK,\n    });\n    await fs.promises.mkdir(limaDir, { recursive: true });\n\n    const child = childProcess.spawn('/usr/bin/tar', ['-xf', tarPath],\n      { cwd: limaDir, stdio: 'inherit' });\n\n    await new Promise<void>((resolve, reject) => {\n      child.on('exit', (code, signal) => {\n        if (code === 0) {\n          resolve();\n        } else {\n          reject(new Error(`Lima extract failed with ${ code || signal }`));\n        }\n      });\n    });\n  }\n}\n\nexport class Qemu extends GlobalDependency(GitHubDependency) {\n  readonly name = 'qemu';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-qemu';\n\n  async download(context: DownloadContext): Promise<void> {\n    // TODO: we don't have an arm64 version of QEMU for Linux yet.\n    if (context.platform === 'linux' && context.isM1) {\n      return;\n    }\n\n    const baseUrl = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download`;\n    const arch = context.isM1 ? 'aarch64' : 'x86_64';\n\n    const url = `${ baseUrl }/v${ context.versions.qemu }/qemu-${ context.versions.qemu }-${ context.platform }-${ arch }.tar.gz`;\n    const expectedChecksum = (await getResource(`${ url }.sha512sum`)).split(/\\s+/)[0];\n    const limaDir = path.join(context.resourcesDir, context.platform, 'lima');\n    const tarPath = path.join(context.resourcesDir, context.platform, `qemu.v${ context.versions.qemu }.tgz`);\n\n    await download(url, tarPath, {\n      expectedChecksum, checksumAlgorithm: 'sha512', access: fs.constants.W_OK,\n    });\n    await fs.promises.mkdir(limaDir, { recursive: true });\n\n    await simpleSpawn('/usr/bin/tar', ['-xf', tarPath], { cwd: limaDir });\n  }\n}\n\nexport class SocketVMNet extends GlobalDependency(GitHubDependency) {\n  readonly name = 'socketVMNet';\n  readonly githubOwner = 'lima-vm';\n  readonly githubRepo = 'socket_vmnet';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'arm64' : 'x86_64';\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.socketVMNet }`;\n    const archiveName = `socket_vmnet-${ context.versions.socketVMNet }-${ arch }.tar.gz`;\n    const expectedChecksum = await findChecksum(`${ baseURL }/SHA256SUMS`, archiveName);\n\n    await downloadTarGZ(`${ baseURL }/${ archiveName }`,\n      path.join(context.resourcesDir, context.platform, 'lima', 'socket_vmnet', 'bin', 'socket_vmnet'),\n      { expectedChecksum, entryName: './opt/socket_vmnet/bin/socket_vmnet' });\n  }\n}\n\nexport class AlpineLimaISO extends GlobalDependency(GitHubDependency) {\n  readonly name = 'alpineLimaISO';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'alpine-lima';\n  readonly releaseFilter = 'custom';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseUrl = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download`;\n    const edition = 'rd';\n    const version = context.versions.alpineLimaISO;\n    const arch = process.env.M1 ? 'aarch64' : 'x86_64';\n\n    const isoName = `alpine-lima-${ edition }-${ version.alpineVersion }-${ arch }.iso`;\n    const url = `${ baseUrl }/v${ version.isoVersion }/${ isoName }`;\n    const destPath = path.join(process.cwd(), 'resources', os.platform(), `alpine-lima-v${ version.isoVersion }-${ edition }-${ version.alpineVersion }.iso`);\n    const expectedChecksum = (await getResource(`${ url }.sha512sum`)).split(/\\s+/)[0];\n\n    await download(url, destPath, {\n      expectedChecksum, checksumAlgorithm: 'sha512', access: fs.constants.W_OK,\n    });\n  }\n\n  assembleAlpineLimaISOVersionFromGitHubRelease(release: GitHubRelease): AlpineLimaISOVersion {\n    const matchingAsset = release.assets.find((asset: { name: string }) => asset.name.includes('rd'));\n\n    if (!matchingAsset) {\n      throw new Error(`Could not find matching asset name in set ${ release.assets }`);\n    }\n    const nameMatch = matchingAsset.name.match(/alpine-lima-rd-([0-9]+\\.[0-9]+\\.[0-9])-.*/);\n\n    if (!nameMatch) {\n      throw new Error(`Failed to parse name \"${ matchingAsset.name }\"`);\n    }\n    const alpineVersion = nameMatch[1];\n\n    return {\n      isoVersion: release.tag_name.replace(/^v/, ''),\n      alpineVersion,\n    };\n  }\n\n  async getAvailableVersions(): Promise<AlpineLimaISOVersion[]> {\n    const response = await getOctokit().rest.repos.listReleases({ owner: this.githubOwner, repo: this.githubRepo });\n    const releases = response.data;\n\n    return await Promise.all(releases.map(this.assembleAlpineLimaISOVersionFromGitHubRelease));\n  }\n\n  versionToTagName(version: AlpineLimaISOVersion): string {\n    return `v${ version.isoVersion }`;\n  }\n\n  rcompareVersions(version1: AlpineLimaISOVersion, version2: AlpineLimaISOVersion): -1 | 0 | 1 {\n    return super.rcompareVersions(version1.isoVersion, version2.isoVersion);\n  }\n\n  async updateManifest(newVersion: string | AlpineLimaISOVersion): Promise<Set<string>> {\n    if (typeof newVersion === 'string') {\n      throw new TypeError(`AlpineLimaISO.updateManifest does not support string version ${ newVersion }`);\n    }\n\n    const depVersions: DependencyVersions = yaml.parse(await fs.promises.readFile(DEP_VERSIONS_PATH, 'utf8'));\n\n    depVersions.alpineLimaISO = newVersion;\n    await fs.promises.writeFile(DEP_VERSIONS_PATH, yaml.stringify(depVersions), 'utf-8');\n\n    return new Set([DEP_VERSIONS_PATH]);\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/moby-openapi.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport _ from 'lodash';\nimport yaml from 'yaml';\n\nimport { download } from '../lib/download';\n\nimport { DownloadContext, getOctokit, VersionedDependency, GlobalDependency } from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\n// This downloads the moby openAPI specification (for WSL-helper) and generates\n// ./src/go/wsl-helper/pkg/dockerproxy/models/...\nexport class MobyOpenAPISpec extends GlobalDependency(VersionedDependency) {\n  readonly name = 'mobyOpenAPISpec';\n  readonly githubOwner = 'moby';\n  readonly githubRepo = 'moby';\n  readonly releaseFilter = 'custom';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseUrl = `https://raw.githubusercontent.com/${ this.githubOwner }/${ this.githubRepo }/master/api/docs`;\n    const url = `${ baseUrl }/v${ context.versions.mobyOpenAPISpec }.yaml`;\n    const outPath = path.join(process.cwd(), 'src', 'go', 'wsl-helper', 'pkg', 'dockerproxy', 'swagger.yaml');\n    const modifiedPath = path.join(path.dirname(outPath), 'swagger-modified.yaml');\n\n    await download(url, outPath, { access: fs.constants.W_OK });\n\n    // We may need compatibility fixes from time to time as the upstream swagger\n    // configuration is manually maintained and needs fixups to work.\n    const contents = yaml.parse(await fs.promises.readFile(outPath, 'utf-8'), { intAsBigInt: true });\n\n    // go-swagger gets confused when multiple things have the same name; this\n    // collides with definitions.Config\n    if (contents.definitions?.Plugin?.properties?.Config?.['x-go-name'] === 'Config') {\n      contents.definitions.Plugin.properties.Config['x-go-name'] = 'PluginConfig';\n    }\n    // Same as above; various Plugin* things collide with the non-plugin versions.\n    for (const key of Object.keys(contents.definitions ?? {}).filter(k => /^Plugin./.test(k))) {\n      delete contents.definitions[key]?.['x-go-name'];\n    }\n\n    // Moby is starting to add `x-go-type` annotations to the spec; however,\n    // none of the types implement validation, and some types are not actually\n    // defined in the file.  Override them here.\n    (function checkTypes(obj: object, prefix = '') {\n      for (const [k, v] of Object.entries(obj)) {\n        if (k === 'x-go-type') {\n          const pkg = v.import?.package ?? '';\n          if (!pkg && /^[A-Z]/.test(v.type)) {\n            // If a type is exported but has no package, it's undefined.\n            console.log(`\\x1B[34;1m${ prefix }\\x1B[22m has invalid type \\x1B[1m${ v.type }\\x1B[22m, removing.\\x1B[0m`);\n            delete (obj as any)[k];\n          } else {\n            // For all other types, skip validation.\n            console.log(`\\x1B[34;1m${ prefix }\\x1B[22m has type \\x1B[1m${ pkg }.${ v.type }\\x1B[22m, disabling validation.\\x1B[0m`);\n            _.set(v, 'hints.noValidation', true);\n          }\n        } else if (_.isPlainObject(v)) {\n          checkTypes(v, `${ prefix }.${ k }`.replace(/^\\./, ''));\n        } else if (Array.isArray(v)) {\n          for (const [i, element] of Object.entries(v)) {\n            checkTypes(element, `${ prefix }[${ i }]`);\n          }\n        }\n      }\n    })(contents);\n\n    await fs.promises.writeFile(modifiedPath, yaml.stringify(contents), 'utf-8');\n\n    await simpleSpawn('go', ['generate', '-x', 'pkg/dockerproxy/generate.go'], { cwd: path.join(process.cwd(), 'src', 'go', 'wsl-helper') });\n    console.log('Moby API swagger models generated.');\n  }\n\n  async getAvailableVersions(): Promise<string[]> {\n    // get list of files in repo directory\n    const githubPath = 'api/docs';\n    const args = {\n      owner: this.githubOwner, repo: this.githubRepo, path: githubPath,\n    };\n    const response = await getOctokit().rest.repos.getContent(args);\n    const fileObjs = response.data as Partial<{ name: string }>[];\n    const allFiles = fileObjs.map(fileObj => fileObj.name);\n\n    // extract versions from file names and convert to valid semver format\n    const versions = [];\n\n    for (const fileName of allFiles) {\n      const match = fileName?.match(/^v([0-9]+\\.[0-9]+)\\.yaml$/);\n\n      if (match) {\n        // to compare with semver we need to add .0 onto the end\n        versions.push(match[1]);\n      }\n    }\n\n    return versions;\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/sudo-prompt.ts",
    "content": "import path from 'path';\n\nimport { Dependency, DownloadContext } from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\n/**\n * SudoPrompt represents the sudo-prompt.app applet used by sudo-prompt on macOS.\n */\nexport class SudoPrompt implements Dependency {\n  readonly name = 'sudo-prompt';\n\n  async download(_: DownloadContext): Promise<void> {\n    // Rather than actually downloading anything, this builds the source code.\n    const sourceDir = path.join(process.cwd(), 'src', 'sudo-prompt');\n\n    console.log(`Building sudo-prompt applet`);\n    await simpleSpawn('./build-sudo-prompt', [], { cwd: sourceDir });\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/tar-archives.ts",
    "content": "import crypto from 'crypto';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\n\nimport tar from 'tar-stream';\n\nimport { Dependency, DownloadContext } from 'scripts/lib/dependencies';\n\nexport class ExtensionProxyImage implements Dependency {\n  readonly name = 'rdx-proxy.tar';\n  dependencies(context: DownloadContext) {\n    return [`extension-proxy:linux`];\n  }\n\n  async download(context: DownloadContext): Promise<void> {\n    // Build the extension proxy image.\n    const workDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rd-build-rdx-pf-'));\n\n    try {\n      const executablePath = path.join(context.resourcesDir, 'linux', 'staging', 'extension-proxy');\n      const layerPath = path.join(workDir, 'layer.tar');\n      const imagePath = path.join(context.resourcesDir, 'rdx-proxy.tar');\n\n      console.log('Building RDX proxying image...');\n\n      // Build the layer tarball\n      // tar streams don't implement piping to multiple writers, and stream.Duplex\n      // can't deal with it either; so we need to fully write out the file, then\n      // calculate the hash as a separate step.\n      const layer = tar.pack();\n      const layerOutput = layer.pipe(fs.createWriteStream(layerPath));\n      const executableStats = await fs.promises.stat(executablePath);\n\n      await stream.promises.finished(\n        fs.createReadStream(executablePath)\n          .pipe(layer.entry({\n            name:  path.basename(executablePath),\n            mode:  0o755,\n            type:  'file',\n            mtime: new Date(0),\n            size:  executableStats.size,\n          })));\n      layer.finalize();\n      await stream.promises.finished(layerOutput);\n\n      // calculate the hash\n      const layerReader = fs.createReadStream(layerPath);\n      const layerHasher = layerReader.pipe(crypto.createHash('sha256'));\n\n      await stream.promises.finished(layerReader);\n\n      // Build the image tarball\n      const layerHash = layerHasher.digest().toString('hex');\n      const image = tar.pack();\n      const imageStream = fs.createWriteStream(imagePath);\n      const imageWritten = stream.promises.finished(imageStream);\n\n      image.pipe(imageStream);\n      const addEntry = (name: string, input: Buffer | stream.Readable, size?: number) => {\n        if (Buffer.isBuffer(input)) {\n          size = input.length;\n          input = stream.Readable.from(input);\n        }\n\n        return stream.promises.finished((input).pipe(image.entry({\n          name,\n          size,\n          type:  'file',\n          mtime: new Date(0),\n        })));\n      };\n\n      image.entry({ name: layerHash, type: 'directory' });\n      await addEntry(`${ layerHash }/VERSION`, Buffer.from('1.0'));\n      await addEntry(`${ layerHash }/layer.tar`, fs.createReadStream(layerPath), layerOutput.bytesWritten);\n      await addEntry(`${ layerHash }/json`, Buffer.from(JSON.stringify({\n        id:     layerHash,\n        config: {\n          ExposedPorts: { '80/tcp': {} },\n          WorkingDir:   '/',\n          Entrypoint:   [`/${ path.basename(executablePath) }`],\n        },\n      })));\n      await addEntry(`${ layerHash }.json`, Buffer.from(JSON.stringify({\n        architecture: context.isM1 ? 'arm64' : 'amd64',\n        config:       {\n          ExposedPorts: { '80/tcp': {} },\n          Entrypoint:   [`/${ path.basename(executablePath) }`],\n          WorkingDir:   '/',\n        },\n        history: [],\n        os:      'linux',\n        rootfs:  {\n          type:     'layers',\n          diff_ids: [`sha256:${ layerHash }`],\n        },\n      })));\n      await addEntry('manifest.json', Buffer.from(JSON.stringify([\n        {\n          Config:   `${ layerHash }.json`,\n          RepoTags: ['ghcr.io/rancher-sandbox/rancher-desktop/rdx-proxy:latest'],\n          Layers:   [`${ layerHash }/layer.tar`],\n        },\n      ])));\n      image.finalize();\n      await imageWritten;\n      console.log('Built RDX port proxy image');\n    } finally {\n      await fs.promises.rm(workDir, { recursive: true });\n    }\n  }\n}\n\nexport class WSLDistroImage implements Dependency {\n  readonly name = 'WSLDistroImage';\n  dependencies(context: DownloadContext): string[] {\n    return [\n      'WSLDistro:win32',\n      'guestagent:linux',\n      'vm-switch:linux',\n      'network-setup:linux',\n      'wsl-proxy:linux',\n      'trivy:linux',\n    ];\n  }\n\n  async download(context: DownloadContext): Promise<void> {\n    const tarName = `distro-${ context.versions.WSLDistro }.tar`;\n    const pristinePath = path.join(context.resourcesDir, context.platform, 'staging', tarName);\n    const pristineFile = fs.createReadStream(pristinePath);\n    const extractor = tar.extract();\n    const destPath = path.join(context.resourcesDir, context.platform, tarName);\n    const destFile = fs.createWriteStream(destPath);\n    const packer = tar.pack();\n\n    console.log('Building WSLDistro image...');\n\n    // Copy the pristine tar file to the destination.\n    packer.pipe(destFile);\n    extractor.on('entry', (header, stream, callback) => {\n      stream.pipe(packer.entry(header, callback));\n    });\n    await stream.promises.finished(pristineFile.pipe(extractor));\n\n    async function addFile(fromPath: string, name: string, options: Omit<tar.Headers, 'name' | 'size'> = {}) {\n      const { size } = await fs.promises.stat(fromPath);\n      const inputFile = fs.createReadStream(fromPath);\n\n      console.log(`WSL Distro: Adding ${ fromPath } to ${ name }...`);\n      await stream.promises.finished(inputFile.pipe(packer.entry({\n        name,\n        size,\n        mode:  0o755,\n        type:  'file',\n        mtime: new Date(0),\n        ...options,\n      })));\n    }\n\n    // Add extra files.\n    const extraFiles = {\n      'linux/staging/guestagent':    'usr/local/bin/rancher-desktop-guestagent',\n      'linux/staging/vm-switch':     'usr/local/bin/vm-switch',\n      'linux/staging/network-setup': 'usr/local/bin/network-setup',\n      'linux/staging/wsl-proxy':     'usr/local/bin/wsl-proxy',\n      'linux/staging/trivy':         'usr/local/bin/trivy',\n    };\n\n    await Promise.all(Object.entries(extraFiles).map(([src, dest]) => {\n      return addFile(path.join(context.resourcesDir, ...src.split('/')), dest);\n    }));\n\n    // Finish the archive.\n    packer.finalize();\n    await stream.promises.finished(packer as any);\n    console.log('Built WSLDistro image.');\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/tools.ts",
    "content": "import fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport {\n  ArchiveDownloadOptions,\n  download,\n  DownloadOptions,\n  downloadTarGZ,\n  downloadZip,\n  getResource,\n} from '../lib/download';\n\nimport {\n  DownloadContext,\n  findChecksum,\n  getPublishedReleaseTagNames,\n  GitHubDependency,\n  GlobalDependency,\n} from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\nfunction exeName(context: DownloadContext, name: string) {\n  const onWindows = context.platform === 'win32';\n\n  return `${ name }${ onWindows ? '.exe' : '' }`;\n}\n\nexport class KuberlrAndKubectl extends GlobalDependency(GitHubDependency) {\n  readonly name = 'kuberlr';\n  readonly githubOwner = 'flavio';\n  readonly githubRepo = 'kuberlr';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const kuberlrPath = await this.downloadKuberlr(context, context.versions.kuberlr, arch);\n\n    await this.bindKubectlToKuberlr(kuberlrPath, path.join(context.binDir, exeName(context, 'kubectl')));\n  }\n\n  async downloadKuberlr(context: DownloadContext, version: string, arch: 'amd64' | 'arm64'): Promise<string> {\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ version }`;\n    const platformDir = `kuberlr_${ version }_${ context.goPlatform }_${ arch }`;\n    const archiveName = platformDir + (context.goPlatform.startsWith('win') ? '.zip' : '.tar.gz');\n    const expectedChecksum = await findChecksum(`${ baseURL }/checksums.txt`, archiveName);\n    const binName = exeName(context, 'kuberlr');\n    const options: ArchiveDownloadOptions = {\n      expectedChecksum,\n      entryName: `${ platformDir }/${ binName }`,\n    };\n    const downloadFunc = context.platform.startsWith('win') ? downloadZip : downloadTarGZ;\n\n    return await downloadFunc(`${ baseURL }/${ archiveName }`, path.join(context.binDir, binName), options);\n  }\n\n  /**\n   * Desired: on Windows, .../bin/kubectl.exe is a copy of .../bin/kuberlr.exe\n   *          elsewhere: .../bin/kubectl is a symlink to .../bin/kuberlr\n   */\n  async bindKubectlToKuberlr(kuberlrPath: string, binKubectlPath: string): Promise<void> {\n    if (os.platform().startsWith('win')) {\n      await fs.promises.copyFile(kuberlrPath, binKubectlPath);\n\n      return;\n    }\n    try {\n      const binKubectlStat = await fs.promises.lstat(binKubectlPath);\n\n      if (binKubectlStat.isSymbolicLink()) {\n        const actualTarget = await fs.promises.readlink(binKubectlPath);\n\n        if (actualTarget === 'kuberlr') {\n          // The link is already there\n          return;\n        } else {\n          console.log(`Deleting symlink ${ binKubectlPath } unexpectedly pointing to ${ actualTarget }`);\n        }\n      }\n      await fs.promises.rm(binKubectlPath);\n    } catch (_) {\n      // .../bin/kubectl doesn't exist, so there's nothing to clean up\n    }\n    await fs.promises.symlink('kuberlr', binKubectlPath);\n  }\n}\n\nexport class Helm extends GlobalDependency(GitHubDependency) {\n  readonly name = 'helm';\n  readonly githubOwner = 'helm';\n  readonly githubRepo = 'helm';\n\n  async download(context: DownloadContext): Promise<void> {\n    // Download Helm. It is a tar.gz file that needs to be expanded and file moved.\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const helmURL = `https://get.helm.sh/helm-v${ context.versions.helm }-${ context.goPlatform }-${ arch }.tar.gz`;\n\n    await downloadTarGZ(helmURL, path.join(context.binDir, exeName(context, 'helm')), {\n      expectedChecksum: (await getResource(`${ helmURL }.sha256sum`)).split(/\\s+/, 1)[0],\n      entryName:        `${ context.goPlatform }-${ arch }/${ exeName(context, 'helm') }`,\n    });\n  }\n}\n\nexport class DockerCLI extends GlobalDependency(GitHubDependency) {\n  readonly name = 'dockerCLI';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-docker-cli';\n\n  async download(context: DownloadContext): Promise<void> {\n    const dockerPlatform = context.dependencyPlatform === 'wsl' ? 'wsl' : context.goPlatform;\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.dockerCLI }`;\n    const executableName = exeName(context, `docker-${ dockerPlatform }-${ arch }`);\n    const dockerURL = `${ baseURL }/${ executableName }`;\n    const destPath = path.join(context.binDir, exeName(context, 'docker'));\n    const expectedChecksum = await findChecksum(`${ baseURL }/sha256sum.txt`, executableName);\n    const codesign = process.platform === 'darwin';\n\n    await download(dockerURL, destPath, { expectedChecksum, codesign });\n  }\n}\n\nexport class DockerBuildx extends GlobalDependency(GitHubDependency) {\n  readonly name = 'dockerBuildx';\n  readonly githubOwner = 'docker';\n  readonly githubRepo = 'buildx';\n\n  async download(context: DownloadContext): Promise<void> {\n    // Download the Docker-Buildx Plug-In\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.dockerBuildx }`;\n    const executableName = exeName(context, `buildx-v${ context.versions.dockerBuildx }.${ context.goPlatform }-${ arch }`);\n    const dockerBuildxURL = `${ baseURL }/${ executableName }`;\n    const dockerBuildxPath = path.join(context.dockerPluginsDir, exeName(context, 'docker-buildx'));\n    const options: DownloadOptions = {};\n\n    // No checksums available on the docker/buildx site for darwin builds\n    // https://github.com/docker/buildx/issues/945\n    if (context.goPlatform !== 'darwin') {\n      options.expectedChecksum = await findChecksum(`${ baseURL }/checksums.txt`, executableName);\n    }\n    await download(dockerBuildxURL, dockerBuildxPath, options);\n  }\n}\n\nexport class DockerCompose extends GlobalDependency(GitHubDependency) {\n  readonly name = 'dockerCompose';\n  readonly githubOwner = 'docker';\n  readonly githubRepo = 'compose';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseUrl = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.dockerCompose }`;\n    const arch = context.isM1 ? 'aarch64' : 'x86_64';\n    const executableName = exeName(context, `docker-compose-${ context.goPlatform }-${ arch }`);\n    const url = `${ baseUrl }/${ executableName }`;\n    const destPath = path.join(context.dockerPluginsDir, exeName(context, 'docker-compose'));\n    const expectedChecksum = await findChecksum(`${ url }.sha256`, executableName);\n\n    await download(url, destPath, { expectedChecksum });\n  }\n}\n\nexport class GoLangCILint extends GlobalDependency(GitHubDependency) {\n  readonly name = 'golangci-lint';\n  readonly githubOwner = 'golangci';\n  readonly githubRepo = 'golangci-lint';\n\n  download(context: DownloadContext): Promise<void> {\n    // We don't actually download anything; when we invoke the linter, we just\n    // use `go run` with the appropriate package.\n    return Promise.resolve();\n  }\n}\n\nexport class CheckSpelling extends GlobalDependency(GitHubDependency) {\n  readonly name = 'check-spelling';\n  readonly githubOwner = 'check-spelling';\n  readonly githubRepo = 'check-spelling';\n\n  download(context: DownloadContext): Promise<void> {\n    // We don't download anything there; `scripts/spelling.sh` does the cloning.\n    return Promise.resolve();\n  }\n}\n\nexport class Trivy extends GlobalDependency(GitHubDependency) {\n  readonly name = 'trivy';\n  readonly githubOwner = 'aquasecurity';\n  readonly githubRepo = 'trivy';\n\n  async download(context: DownloadContext): Promise<void> {\n    // Download Trivy\n    // Always run this in the VM, so download the *LINUX* version into internalDir\n    // and move it over to the wsl/lima partition at runtime.\n    // Sample URLs:\n    // https://github.com/aquasecurity/trivy/releases/download/v0.18.3/trivy_0.18.3_checksums.txt\n    // https://github.com/aquasecurity/trivy/releases/download/v0.18.3/trivy_0.18.3_macOS-64bit.tar.gz\n\n    const versionWithV = `v${ context.versions.trivy }`;\n    const trivyURLBase = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases`;\n    const trivyOS = context.isM1 ? 'Linux-ARM64' : 'Linux-64bit';\n    const trivyBasename = `trivy_${ context.versions.trivy }_${ trivyOS }`;\n    const trivyURL = `${ trivyURLBase }/download/${ versionWithV }/${ trivyBasename }.tar.gz`;\n    const checksumURL = `${ trivyURLBase }/download/${ versionWithV }/trivy_${ context.versions.trivy }_checksums.txt`;\n    const trivySHA = await findChecksum(checksumURL, `${ trivyBasename }.tar.gz`);\n    const trivyDir = context.dependencyPlatform === 'wsl' ? 'staging' : 'internal';\n    const trivyPath = path.join(context.resourcesDir, 'linux', trivyDir, 'trivy');\n\n    // trivy.tgz files are top-level tarballs - not wrapped in a labelled directory :(\n    await downloadTarGZ(trivyURL, trivyPath, { expectedChecksum: trivySHA });\n  }\n}\n\nexport class Steve extends GlobalDependency(GitHubDependency) {\n  readonly name = 'steve';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-steve';\n  readonly releaseFilter = 'published-pre';\n\n  async download(context: DownloadContext): Promise<void> {\n    const steveURLBase = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.steve }`;\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const steveExecutable = `steve-${ context.goPlatform }-${ arch }`;\n    const steveURL = `${ steveURLBase }/${ steveExecutable }.tar.gz`;\n    const stevePath = path.join(context.internalDir, exeName(context, 'steve'));\n    const steveSHA = await findChecksum(`${ steveURL }.sha512sum`, `${ steveExecutable }.tar.gz`);\n\n    await downloadTarGZ(\n      steveURL,\n      stevePath,\n      {\n        expectedChecksum:  steveSHA,\n        checksumAlgorithm: 'sha512',\n      });\n  }\n}\n\nexport class RancherDashboard extends GlobalDependency(GitHubDependency) {\n  readonly name = 'rancherDashboard';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-dashboard';\n  readonly releaseFilter = 'custom';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseURL = `https://github.com/rancher-sandbox/${ this.githubRepo }/releases/download/desktop-v${ context.versions.rancherDashboard }`;\n    const executableName = 'rancher-dashboard-desktop-embed';\n    const url = `${ baseURL }/${ executableName }.tar.gz`;\n    const destPath = path.join(context.resourcesDir, 'rancher-dashboard.tgz');\n    const expectedChecksum = await findChecksum(`${ url }.sha512sum`, `${ executableName }.tar.gz`);\n    const rancherDashboardDir = path.join(context.resourcesDir, 'rancher-dashboard');\n\n    if (fs.existsSync(rancherDashboardDir)) {\n      console.log(`${ rancherDashboardDir } already exists, not re-downloading.`);\n\n      return;\n    }\n\n    await download(\n      url,\n      destPath,\n      {\n        expectedChecksum,\n        checksumAlgorithm: 'sha512',\n        access:            fs.constants.W_OK,\n      });\n\n    await fs.promises.mkdir(rancherDashboardDir, { recursive: true });\n\n    const args = ['tar', '-xf', destPath];\n\n    if (os.platform().startsWith('win')) {\n      // On Windows, force use the bundled bsdtar.\n      // We may find GNU tar on the path, which looks at the Windows-style path\n      // and considers C:\\Temp to be a reference to a remote host named `C`.\n      const systemRoot = process.env.SystemRoot;\n\n      if (!systemRoot) {\n        throw new Error('Could not find system root');\n      }\n      args[0] = path.join(systemRoot, 'system32', 'tar.exe');\n    }\n\n    console.log('Extracting rancher dashboard...');\n    await simpleSpawn(args[0], args.slice(1), {\n      cwd:   rancherDashboardDir,\n      stdio: ['ignore', 'inherit', 'inherit'],\n    });\n\n    await fs.promises.rm(destPath, { recursive: true, maxRetries: 10 });\n  }\n\n  async getAvailableVersions(): Promise<string[]> {\n    const tagNames = await getPublishedReleaseTagNames(this.githubOwner, this.githubRepo, 'published');\n\n    return tagNames.map((tagName: string) => tagName.replace(/^desktop-v/, ''));\n  }\n\n  versionToTagName(version: string): string {\n    return `desktop-v${ version }`;\n  }\n}\n\nexport class DockerProvidedCredHelpers extends GlobalDependency(GitHubDependency) {\n  readonly name = 'dockerProvidedCredentialHelpers';\n  readonly githubOwner = 'docker';\n  readonly githubRepo = 'docker-credential-helpers';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const version = context.versions.dockerProvidedCredentialHelpers;\n    const credHelperNames = {\n      linux:  ['docker-credential-secretservice', 'docker-credential-pass'],\n      darwin: ['docker-credential-osxkeychain'],\n      win32:  ['docker-credential-wincred'],\n    }[context.platform];\n    const promises = [];\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ version }`;\n\n    for (const baseName of credHelperNames) {\n      const fullBaseName = `${ baseName }-v${ version }.${ context.goPlatform }-${ arch }`;\n      const fullBinName = context.platform.startsWith('win') ? `${ fullBaseName }.exe` : fullBaseName;\n      const sourceURL = `${ baseURL }/${ fullBinName }`;\n      const expectedChecksum = await findChecksum(`${ baseURL }/checksums.txt`, fullBinName);\n      const binName = context.platform.startsWith('win') ? `${ baseName }.exe` : baseName;\n      const destPath = path.join(context.binDir, binName);\n      // starting with the 0.7.0 the upstream releases have a broken ad-hoc signature\n      const codesign = context.platform === 'darwin';\n\n      promises.push(download(sourceURL, destPath, { expectedChecksum, codesign } ));\n    }\n\n    await Promise.all(promises);\n  }\n}\n\nexport class ECRCredHelper extends GlobalDependency(GitHubDependency) {\n  readonly name = 'ECRCredentialHelper';\n  readonly githubOwner = 'awslabs';\n  readonly githubRepo = 'amazon-ecr-credential-helper';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'arm64' : 'amd64';\n    const ecrLoginPlatform = context.platform.startsWith('win') ? 'windows' : context.platform;\n    const baseName = 'docker-credential-ecr-login';\n    const baseUrl = 'https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com';\n    const binName = exeName(context, baseName);\n    const sourceUrl = `${ baseUrl }/${ context.versions.ECRCredentialHelper }/${ ecrLoginPlatform }-${ arch }/${ binName }`;\n    const destPath = path.join(context.binDir, binName);\n\n    return await download(sourceUrl, destPath);\n  }\n}\n\nexport class WasmShims extends GlobalDependency(GitHubDependency) {\n  readonly name = 'spinShim';\n  readonly githubOwner = 'spinframework';\n  readonly githubRepo = 'containerd-shim-spin';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'aarch64' : 'x86_64';\n    const base = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.spinShim }`;\n    const url = `${ base }/containerd-shim-spin-v2-linux-${ arch }.tar.gz`;\n    const destPath = path.join(context.resourcesDir, 'linux', 'internal', 'containerd-shim-spin-v2');\n\n    await downloadTarGZ(url, destPath);\n  }\n}\n\nexport class CertManager extends GlobalDependency(GitHubDependency) {\n  readonly name = 'certManager';\n  readonly githubOwner = 'cert-manager';\n  readonly githubRepo = 'cert-manager';\n\n  async download(context: DownloadContext): Promise<void> {\n    const base = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.certManager }`;\n    const filename = 'cert-manager.crds.yaml';\n\n    await download(`${ base }/${ filename }`, path.join(context.resourcesDir, filename));\n\n    const url = `https://charts.jetstack.io/charts/cert-manager-v${ context.versions.certManager }.tgz`;\n\n    await download(url, path.join(context.resourcesDir, 'cert-manager.tgz'));\n  }\n}\n\nexport class SpinOperator extends GlobalDependency(GitHubDependency) {\n  readonly name = 'spinOperator';\n  readonly githubOwner = 'spinframework';\n  readonly githubRepo = 'spin-operator';\n\n  async download(context: DownloadContext): Promise<void> {\n    const base = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.spinOperator }`;\n    let filename = 'spin-operator.crds.yaml';\n\n    await download(`${ base }/${ filename }`, path.join(context.resourcesDir, filename));\n\n    filename = `spin-operator-${ context.versions.spinOperator }.tgz`;\n    await download(`${ base }/${ filename }`, path.join(context.resourcesDir, 'spin-operator.tgz'));\n  }\n}\n\nexport class SpinCLI extends GlobalDependency(GitHubDependency) {\n  readonly name = 'spinCLI';\n  readonly githubOwner = 'spinframework';\n  readonly githubRepo = 'spin';\n\n  async download(context: DownloadContext): Promise<void> {\n    const arch = context.isM1 ? 'aarch64' : 'amd64';\n    const platform = {\n      darwin:  'macos',\n      linux:   'static-linux',\n      windows: 'windows',\n    }[context.goPlatform];\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download/v${ context.versions.spinCLI }`;\n    const archiveName = `spin-v${ context.versions.spinCLI }-${ platform }-${ arch }${ context.goPlatform.startsWith('win') ? '.zip' : '.tar.gz' }`;\n    const expectedChecksum = await findChecksum(`${ baseURL }/checksums-v${ context.versions.spinCLI }.txt`, archiveName);\n    const entryName = exeName(context, 'spin');\n    const options: ArchiveDownloadOptions = { expectedChecksum, entryName };\n    const downloadFunc = context.platform.startsWith('win') ? downloadZip : downloadTarGZ;\n\n    await downloadFunc(`${ baseURL }/${ archiveName }`, path.join(context.internalDir, entryName), options);\n  }\n}\n\nexport class SpinKubePlugin extends GlobalDependency(GitHubDependency) {\n  readonly name = 'spinKubePlugin';\n  readonly githubOwner = 'spinframework';\n  readonly githubRepo = 'spin-plugin-kube';\n\n  download(context: DownloadContext): Promise<void> {\n    // We don't download anything there; `resources/setup-spin` does the installation.\n    return Promise.resolve();\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/wix.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport semver from 'semver';\n\nimport { DownloadContext, GitHubDependency, GlobalDependency, getOctokit } from '../lib/dependencies';\nimport { download } from '../lib/download';\n\nimport { simpleSpawn } from 'scripts/simple_process';\n\n/**\n * Wix downloads the latest build of WiX3.\n */\nexport class Wix extends GlobalDependency(GitHubDependency) {\n  readonly name = 'wix';\n\n  // Wix4 is packaged really oddly (involves NuGet), and while there's a sketchy\n  // build in https://github.com/electron-userland/electron-builder-binaries, it's rather\n  // outdated (and has since-fixed bugs).\n  readonly githubOwner = 'wixtoolset';\n  readonly githubRepo = 'wix3';\n  readonly releaseFilter = 'custom';\n\n  async download(context: DownloadContext): Promise<void> {\n    // WiX doesn't appear to believe in checksum files...\n\n    const tagName = this.versionToTagName(context.versions.wix);\n    const version = semver.parse(context.versions.wix);\n\n    if (!version) {\n      throw new Error(`Could not parse WiX version ${ context.versions.wix }`);\n    }\n\n    const hostDir = path.join(context.resourcesDir, 'host');\n    const wixDir = path.join(hostDir, 'wix');\n    const archivePath = path.join(hostDir, `${ tagName }.zip`);\n    // The archive name never includes the patch version.\n    const archiveName = `wix${ version.major }${ version.minor }-binaries.zip`;\n    const url = `https://github.com/wixtoolset/wix3/releases/download/${ tagName }/${ archiveName }`;\n\n    await fs.promises.mkdir(wixDir, { recursive: true });\n    await download(url, archivePath);\n    await simpleSpawn('unzip', ['-q', '-o', archivePath, '-d', wixDir], { cwd: wixDir });\n  }\n\n  versionToTagName(versionString: string): string {\n    const version = semver.parse(versionString);\n\n    if (!version) {\n      throw new Error(`Could not parse WiX version ${ versionString }`);\n    }\n\n    return `wix${ version.major }${ version.minor }${ version.patch || '' }rtm`;\n  }\n\n  async getAvailableVersions(): Promise<string[]> {\n    // WiX tag names are `wix${ major }${ minor }${ patch if not zero }rtm` with\n    // no separation between fields; so we have to dig the version number out\n    // of the release title instead.\n    const { data: releases } = await getOctokit().rest.repos.listReleases({ owner: this.githubOwner, repo: this.githubRepo });\n    const publishedReleases = releases.filter(release => release.published_at);\n    const versions = publishedReleases.map(r => (/^WiX Toolset (v\\d+\\.\\d+\\.\\d+)/.exec(r.name ?? '') ?? [])[1]);\n\n    return versions.filter(version => version);\n  }\n}\n"
  },
  {
    "path": "scripts/dependencies/wsl.ts",
    "content": "import fs from 'fs';\nimport path from 'path';\n\nimport { download } from '../lib/download';\nimport { simpleSpawn } from '../simple_process';\n\nimport { DownloadContext, GitHubDependency, GlobalDependency } from 'scripts/lib/dependencies';\n\nexport class Moproxy extends GlobalDependency(GitHubDependency) {\n  readonly name = 'moproxy';\n  readonly githubOwner = 'sorz';\n  readonly githubRepo = 'moproxy';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseURL = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download`;\n    const binName = `moproxy_${ context.versions.moproxy }_linux_x86_64_musl.bin`;\n    const archiveName = `${ binName }.xz`;\n    const moproxyURL = `${ baseURL }/v${ context.versions.moproxy }/${ archiveName }`;\n    const archivePath = path.join(context.internalDir, archiveName);\n    const moproxyPath = path.join(context.internalDir, 'moproxy');\n\n    await download(\n      moproxyURL,\n      archivePath,\n      { access: fs.constants.W_OK });\n\n    // moproxy uses xz with no tar wrapper; just decompress it manually.\n    await simpleSpawn('7z', ['e', archivePath], { cwd: context.internalDir });\n    await fs.promises.rename(path.join(context.internalDir, binName), moproxyPath);\n    await fs.promises.rm(archivePath);\n  }\n}\n\nexport class WSLDistro extends GlobalDependency(GitHubDependency) {\n  readonly name = 'WSLDistro';\n  readonly githubOwner = 'rancher-sandbox';\n  readonly githubRepo = 'rancher-desktop-wsl-distro';\n\n  async download(context: DownloadContext): Promise<void> {\n    const baseUrl = `https://github.com/${ this.githubOwner }/${ this.githubRepo }/releases/download`;\n    const tarName = `distro-${ context.versions.WSLDistro }.tar`;\n    const url = `${ baseUrl }/v${ context.versions.WSLDistro }/${ tarName }`;\n    const destPath = path.join(context.resourcesDir, context.platform, 'staging', tarName);\n\n    await download(url, destPath, { access: fs.constants.W_OK });\n  }\n}\n"
  },
  {
    "path": "scripts/dev.ts",
    "content": "/**\n * This script runs the application for development.\n */\n\n'use strict';\n\nimport childProcess from 'node:child_process';\nimport events from 'node:events';\nimport fs from 'node:fs';\nimport https from 'node:https';\nimport path from 'node:path';\nimport util from 'node:util';\n\nimport psTree from 'ps-tree';\n\nimport buildUtils from './lib/build-utils';\n\ninterface RendererEnv {\n  home:   string;\n  agent?: https.Agent | undefined;\n}\n\nclass DevRunner extends events.EventEmitter {\n  emitError(message: string, error: any) {\n    let combinedMessage = message;\n\n    if (error?.message) {\n      combinedMessage += `: ${ error.message }`;\n    }\n    const newError: Error & { code?: number } = new Error(combinedMessage);\n\n    newError.code = error?.code;\n    if (error?.stack) {\n      newError.stack += `\\nCaused by: ${ error.stack }`;\n    }\n    this.emit('error', newError);\n  }\n\n  readonly rendererPort = 8888;\n\n  /**\n   * Spawn a child process, set up to emit errors on unexpected exit.\n   * @param title The title of the process to show in messages.\n   * @param command The executable to run.\n   * @param args Any arguments to the executable.\n   * @returns The new child process.\n   */\n  spawn(title: string, command: string, ...args: string[]): childProcess.ChildProcess {\n    const promise = buildUtils.spawn(command, ...args);\n\n    promise\n      .then(() => this.exit())\n      .catch(error => this.emitError(`${ title } error`, error));\n\n    return promise.child;\n  }\n\n  /**\n   * Gets information about the renderer based on the environment variable\n   * RD_ENV_PLUGINS_DEV. For plugins development.\n   */\n  rendererEnv(): RendererEnv {\n    if (process.env.RD_ENV_PLUGINS_DEV) {\n      return {\n        home:  'https://localhost:8888/home',\n        agent: new https.Agent({ rejectUnauthorized: false }),\n      };\n    }\n\n    return { home: 'http://localhost:8888' };\n  }\n\n  #mainProcess: childProcess.ChildProcess | null = null;\n  async startMainProcess() {\n    console.info('Main process: starting...');\n    try {\n      const electronPath = await fs.promises.readFile('node_modules/electron/path.txt', 'utf-8');\n      const argv = process.argv.slice(2);\n\n      await buildUtils.buildMain();\n\n      this.#mainProcess = this.spawn(\n        'Main process',\n        path.join('node_modules/electron/dist', electronPath),\n        ...argv.filter(x => x.startsWith('--inspect')),\n        buildUtils.rootDir,\n        this.rendererPort.toString(),\n        '## Rancher Desktop Command Line Marker ##',\n        ...argv.filter(x => !x.startsWith('--inspect')),\n      );\n      this.#mainProcess.on('exit', (code: number, signal: string) => {\n        if (code === 201) {\n          console.log('Another instance of Rancher Desktop is already running');\n        } else if (code > 0) {\n          console.log(`Rancher Desktop: main process exited with status ${ code }`);\n        } else if (signal) {\n          console.log(`Rancher Desktop: main process exited with signal ${ signal }`);\n        }\n      });\n    } catch (err) {\n      console.log(`Failure in startMainProcess: ${ err }`);\n    }\n  }\n\n  #rendererProcess: null | childProcess.ChildProcess = null;\n  /**\n   * Start the renderer process.  The function should return once the renderer\n   * is ready for connections, and the renderer process should continue after\n   * the function returns.\n   */\n  async startRendererProcess(): Promise<void> {\n    await buildUtils.buildPreload();\n    let started = false;\n    let startError: Error | undefined;\n\n    console.info('Renderer process: starting...');\n    process.env.VUE_CLI_SERVICE_CONFIG_PATH = 'pkg/rancher-desktop/vue.config.mjs';\n\n    this.#rendererProcess = this.spawn(\n      'Renderer process',\n      process.execPath,\n      '--stack-size=16384',\n      'node_modules/@vue/cli-service/bin/vue-cli-service.js',\n      'serve',\n      '--host',\n      'localhost',\n      '--port',\n      this.rendererPort.toString(),\n      '--skip-plugins',\n      'eslint',\n    );\n\n    // Listen for the 'exit' event of the child process and resolve or reject the Promise accordingly.\n    this.#rendererProcess.on('exit', (code, _signal) => {\n      if (!started) {\n        startError = new Error(`Renderer process exited prematurely with code ${ code }`);\n      } else if (code !== 0) {\n        this.emit('error', new Error(`Renderer process exited with code ${ code }`));\n      }\n    });\n\n    // Wait for the renderer to finish, so that vue-cli output doesn't\n    // clobber debugging output.\n    const rendererEnv = this.rendererEnv();\n\n    const maxRetries = 30;\n    const retryInterval = 1000;\n\n    for (let retryCount = 0; retryCount < maxRetries;) {\n      if (startError) {\n        throw startError;\n      }\n      try {\n        const response = await fetch(rendererEnv.home);\n\n        if (response.ok && !startError) {\n          console.info('Renderer process: dev server started');\n          break;\n        }\n      } catch (ex) {\n        /* nothing */\n      }\n      if (startError) {\n        throw startError;\n      }\n      // Retry if response is not okay or fetch throws an error.\n      if (++retryCount < maxRetries) {\n        await util.promisify(setTimeout)(retryInterval);\n      } else {\n        throw new Error(`Renderer process: failed to connect`);\n      }\n    }\n\n    started = true;\n  }\n\n  /**\n   * Kill child processes associated with the given parent PID.\n   * @param parentPID - Parent PID whose child processes need to be terminated.\n   */\n  killChildProcesses(parentPID: number) {\n    psTree(parentPID, (err: Error | null, children: readonly psTree.PS[]) => {\n      if (err) {\n        console.error(`Error getting child processes with PID ${ parentPID }:`, { err });\n      } else {\n        children.forEach((child: psTree.PS) => {\n          try {\n            process.kill(Number(child.PID));\n          } catch (error: any) {\n            if (error.code === 'ESRCH') {\n              console.log(`Child process with PID ${ child.PID } not found.`);\n            } else {\n              console.error(`Error killing child process with PID ${ child.PID }: ${ error.message }`);\n            }\n          }\n        });\n      }\n    });\n  }\n\n  exit() {\n    // Terminate the renderer process if it exists\n    if (this.#rendererProcess) {\n      this.#rendererProcess.kill();\n\n      if (this.#rendererProcess.pid) {\n        this.killChildProcesses(this.#rendererProcess.pid);\n      }\n\n      // Set to null in the event that exit() invokes multiple times\n      this.#rendererProcess = null;\n    }\n\n    this.#mainProcess?.kill();\n  }\n\n  async run() {\n    process.env.NODE_ENV = 'development';\n    try {\n      await this.startRendererProcess();\n      await this.startMainProcess();\n\n      await new Promise((resolve, reject) => {\n        this.on('error', reject);\n      });\n    } catch (err: any) {\n      if (typeof err === 'string' && err.includes('Main process error: Process exited with code 201')) {\n        // do nothing\n      } else {\n        console.error(err);\n      }\n    } finally {\n      this.exit();\n    }\n  }\n}\n\n(new DevRunner()).run().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/docker-cli-monitor.ts",
    "content": "// This script creates GitHub issues when new releases of the Docker CLI are\n// found.\n\n// Environment:\n//   GITHUB_CREATE_TOKEN: GitHub authorization token for creating an issue.\n//     Must be a PAT/app token and have `issues:write` permissions.\n//   GITHUB_TOKEN: GitHub authorization token for closing an issue.\n//     Must have `issues:write` permissions.\n\nimport path from 'path';\n\nimport semver from 'semver';\n\nimport { readDependencyVersions, getOctokit, RancherDesktopRepository, IssueOrPullRequest } from 'scripts/lib/dependencies';\n\nconst GITHUB_OWNER = process.env.GITHUB_REPOSITORY?.split('/')[0] || 'rancher-sandbox';\nconst GITHUB_REPO = process.env.GITHUB_REPOSITORY?.split('/')[1] || 'rancher-desktop';\nconst DOCKER_CLI_OWNER = process.env.DOCKER_CLI_OWNER || 'docker';\nconst DOCKER_CLI_REPO = process.env.DOCKER_CLI_REPO || 'cli';\nconst TAG_REGEX = /^v[0-9]+\\.[0-9]+\\.[0-9]+$/;\nconst SCRIPT_NAME = 'docker-cli-monitor';\nconst TITLE_PREFIX = `${ SCRIPT_NAME }: make rancher-desktop-docker-cli release for version`;\nconst mainRepo = new RancherDesktopRepository(GITHUB_OWNER, GITHUB_REPO);\n\nasync function getLatestDockerCliVersion(): Promise<string> {\n  const result = await getOctokit().rest.repos.listTags({\n    owner: DOCKER_CLI_OWNER, repo: DOCKER_CLI_REPO, per_page: 100,\n  });\n  const tags = result.data;\n  const fullReleaseTags = tags.filter(tag => TAG_REGEX.test(tag.name));\n\n  if (fullReleaseTags.length === 0) {\n    throw new Error('Failed to find any valid tags');\n  }\n  fullReleaseTags.sort((previous, current) => {\n    const previousWithoutV = previous.name.replace('v', '');\n    const currentWithoutV = current.name.replace('v', '');\n\n    return semver.rcompare(previousWithoutV, currentWithoutV, { loose: true });\n  });\n  const latestTag = fullReleaseTags[0];\n\n  return latestTag.name;\n}\n\nasync function getDockerCliIssues(): Promise<IssueOrPullRequest[]> {\n  const query = `type:issue repo:${ GITHUB_OWNER }/${ GITHUB_REPO } sort:updated in:title \"${ TITLE_PREFIX }\"`;\n  const result = await getOctokit().rest.search.issuesAndPullRequests({ q: query });\n\n  return result.data.items;\n}\n\nasync function checkDockerCli(): Promise<void> {\n  const latestTagName = await getLatestDockerCliVersion();\n  const latestVersion = latestTagName.replace('v', '');\n\n  console.log(`Latest version: ${ latestVersion }`);\n\n  const depVersionsPath = path.join('pkg', 'rancher-desktop', 'assets', 'dependencies.yaml');\n  const dependencyVersions = await readDependencyVersions(depVersionsPath);\n\n  console.log(`Current version: ${ dependencyVersions.dockerCLI }`);\n\n  if (latestVersion === dependencyVersions.dockerCLI) {\n    return;\n  }\n\n  const issues = await getDockerCliIssues();\n  let issueFound = false;\n\n  await Promise.all(issues.map(async(issue) => {\n    if (issue.title.endsWith(` ${ latestTagName }`)) {\n      issueFound = true;\n      if (issue.state === 'closed') {\n        await mainRepo.reopenIssue(issue, process.env.GITHUB_CREATE_TOKEN);\n      }\n    } else if (issue.state === 'open') {\n      await mainRepo.closeIssue(issue);\n    }\n  }));\n  if (!issueFound) {\n    const title = `${ TITLE_PREFIX } ${ latestTagName }`;\n    const body = `The Docker CLI monitor has detected a new release of docker/cli. ` +\n      `Please make a corresponding release in rancher-desktop-docker-cli to keep it up to date in Rancher Desktop.`;\n\n    await mainRepo.createIssue(title, body, process.env.GITHUB_CREATE_TOKEN);\n  }\n}\n\ncheckDockerCli().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/e2e.ts",
    "content": "/**\n * This script runs the end-to-end tests.\n */\n\n'use strict';\n\nimport childProcess from 'child_process';\nimport events from 'events';\nimport util from 'util';\n\nimport buildUtils from './lib/build-utils';\n\nimport * as settings from '@pkg/config/settings';\nimport { readDeploymentProfiles } from '@pkg/main/deploymentProfiles';\n\nconst sleep = util.promisify(setTimeout);\n\nclass E2ETestRunner extends events.EventEmitter {\n  emitError(message: string, error: any) {\n    let combinedMessage = message;\n\n    if (error?.message) {\n      combinedMessage += `: ${ error.message }`;\n    }\n    const newError: Error & { code?: number } = new Error(combinedMessage);\n\n    newError.code = error?.code;\n    if (error?.stack) {\n      newError.stack += `\\nCaused by: ${ error.stack }`;\n    }\n    this.emit('error', newError);\n  }\n\n  get rendererPort() {\n    return 8888;\n  }\n\n  /**\n   * Spawn a child process, set up to emit errors on unexpected exit.\n   * @param title The title of the process to show in messages.\n   * @param command The executable to run.\n   * @param  args Any arguments to the executable.\n   * @returns The new child process.\n   */\n  spawn(title: string, command: string, ...args: string[]): childProcess.ChildProcess {\n    const promise = buildUtils.spawn(command, ...args);\n\n    promise\n      .then(() => this.exit())\n      .catch(error => this.emitError(`${ title } error`, error));\n\n    return promise.child;\n  }\n\n  exit() {\n    this.#testProcess?.kill();\n  }\n\n  #testProcess: null | childProcess.ChildProcess = null;\n  startTestProcess(): Promise<void> {\n    const args = processArgsForPlaywright(process.argv);\n    const spawnArgs = ['node_modules/@playwright/test/cli.js', 'test', '--config=e2e/config/playwright-config.ts'];\n\n    this.#testProcess = this.spawn('Test process', 'node', ...spawnArgs, ...args);\n\n    return new Promise((resolve, reject) => {\n      this.#testProcess?.on('exit', (code: number, signal: string) => {\n        if (code === 201) {\n          console.log('Another instance of Rancher Desktop is already running');\n          resolve();\n        } else if (code > 0) {\n          console.log(`Rancher Desktop: main process exited with status ${ code }`);\n          reject(code);\n        } else if (signal) {\n          console.log(`Rancher Desktop: main process exited with signal ${ signal }`);\n          reject(signal);\n        } else {\n          resolve(process.exit());\n        }\n      });\n    });\n  }\n\n  /**\n   * Start the renderer process.\n   */\n  buildRenderer(): Promise<void> {\n    process.env.VUE_CLI_SERVICE_CONFIG_PATH = 'pkg/rancher-desktop/vue.config.mjs';\n\n    return buildUtils.spawn(\n      process.execPath,\n      '--stack-size=16384',\n      'node_modules/@vue/cli-service/bin/vue-cli-service.js',\n      'build',\n      '--skip-plugins',\n      'eslint',\n    );\n  }\n\n  async run() {\n    try {\n      if (!process.env.RD_TEST_ALLOW_PROFILE) {\n        let deploymentProfiles: settings.DeploymentProfileType = { defaults: {}, locked: {} };\n\n        try {\n          deploymentProfiles = await readDeploymentProfiles();\n        } catch {}\n        if (Object.keys(deploymentProfiles.defaults).length > 0 || Object.keys(deploymentProfiles.locked).length > 0) {\n          throw new Error([\"Trying to run e2e tests with existing deployment profiles isn't supported.\",\n            'Set environment variable RD_TEST_ALLOW_PROFILE=true to override this check',\n          ].join('\\n'));\n        }\n      }\n      process.env.RD_TEST = process.env.npm_lifecycle_event || 'e2e'; // May include \"screenshots\"\n\n      // Start the renderer process and wait for it to complete the build.\n      await this.buildRenderer();\n\n      await buildUtils.wait(\n        () => buildUtils.buildMain(),\n        () => buildUtils.buildPreload(),\n      );\n      await isCiOrDevelopmentTimeout();\n      await this.startTestProcess();\n    } finally {\n      this.exit();\n    }\n  }\n}\n\n(new E2ETestRunner()).run().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n\nfunction isCiOrDevelopmentTimeout() {\n  const ciTimeout = 40000;\n  const devTimeout = 20000;\n\n  if (process.env.CI) {\n    console.log(`ENV Detected CI:${ process.env.CI } - Setting up Loading timeout: ${ ciTimeout }ms`);\n\n    return sleep(ciTimeout);\n  } else {\n    console.log(`ENV Detected non-CI:${ process.env.NODE_ENV } - Setting up Loading timeout: ${ devTimeout }ms`);\n\n    return sleep(devTimeout);\n  }\n}\n\n// Convert any single backslash into two, but leave pairs of backslashes alone.\nfunction escapeUnescapedBackslashes(s: string): string {\n  return s.replace(/\\\\(?:.|$)/g, m => m === '\\\\\\\\' ? m : `\\\\${ m }`);\n}\n\n/**\n * The first 2 args are internal for yarn/npm and shouldn't be passed to playwright. Same with `--serial`.\n * Now playwright treats paths as regexes, meaning that unescaped backslashes will normally be treated\n * as meta-regex-characters and will be unlikely to match files. This wasn't an issue in the NPM world,\n * because on Windows npm escaped each backslash: `.\\e2e\\foo.spec.ts` showed up as .\\\\e2e\\\\foo.spec.ts`.\n * But Yarn doesn't escape the backslashes, so we need to escape them ourselves.\n *\n * I filed an upstream bug on Playwright, but they closed it due to the claim that paths are actually\n * regexes: https://github.com/microsoft/playwright/issues/24408#issuecomment-1652146685 . This is so\n * you can specify a command like `npx playwright foot head` and run any tests that match the terms\n * `foot` or `head` but skip, for example, `thin-waist.spec.ts`.\n *\n * I don't think it's worth writing a bug against yarn on this. For whatever reason, the paths were\n * escaped in the npm world but not yarn, and we can just allow both forms.\n *\n * The code assumes that anything starting with a '-' doesn't need escaping (because we don't invoke\n * this script with any such options)\n *\n * @param args\n */\nfunction processArgsForPlaywright(args: string[]): string[] {\n  args = process.argv.slice(2).filter(x => x !== '--serial');\n  if (process.platform !== 'win32') {\n    return args;\n  }\n\n  return args.map((s) => {\n    return s.startsWith('-') ? s : escapeUnescapedBackslashes(s);\n  });\n}\n"
  },
  {
    "path": "scripts/extension-data.ts",
    "content": "/**\n * This file generates pkg/rancher-desktop/assets/extension-data.yaml\n *\n * Usage: `yarn generate:extension-data`\n */\n\nimport { generateExtensionMarketplaceData } from './lib/extension-data';\n\ngenerateExtensionMarketplaceData().catch((ex) => {\n  console.error(ex);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/generateCliCode.ts",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/**\n * This script generates the options module used for the rdctl `set` and `start` subcommands,\n * according to the preferences spec from pkg/rancher-desktop/assets/specs/command-api.yaml\n * Doing this avoids manually keeping these `rdctl` commands in sync with the supported settings.\n */\n\nimport { execFileSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\nimport ejs from 'ejs';\nimport yaml from 'yaml';\n\nimport { CURRENT_SETTINGS_VERSION } from '@pkg/config/settings';\n\ninterface commandFlagType {\n  /**\n   * The capitalized name of the final part of a dotted property name,\n   * like Flannel` in `kubernetes.options.flannel`. Capitalized names are used for\n   * exported golang struct fields.\n   */\n  capitalizedName: string;\n  /**\n   * The default value to enter as a string in the `cmd.Flags()...` statement.\n   * This is a string internally, but is written as the appropriate type for `cmd.Flags.TVar(...)`.\n   */\n  defaultValue:    string;\n  /**\n   * Capitalized name of the type given in command-api.yaml.\n   * This maps to go code like `cmdFlags.StringVar(...)`.\n   */\n  flagType:        goCmdFlagTypeName;\n  /**\n   * Lower-case name of a scalar golang type, like `string`.\n   */\n  lcTypeName?:     goTypeName;\n  /**\n   * A property name from the API spec, like `kubernetes`.\n   * Can be the name of a leaf or a compound object.\n   */\n  propertyName:    string;\n  /**\n   * Used to map a shortcut option (like `--container-engine` to the full name `--kubernetes.containerEngine`)\n   */\n  aliasFor:        string;\n  /**\n   * Some options can be specified with a limited set of possible values.\n   * This field carries those values as a string, to be inserted into the golang command definition.\n   */\n  enums:           string;\n  /**\n   * Used to insert an optional `x-rd-usage` field from a preference spec\n   * into the help text for the command in the generated go code.\n   */\n  usageNote?:      string;\n  /**\n   * Used to format the specified value for a command-line option depending on the value's golang type.\n   */\n  valuePart:       string;\n  /**\n   * The option is not available for the current platform\n   */\n  notAvailable:    boolean;\n}\n\ntype yamlObject = any;\n\ntype goTypeName = 'string' | 'bool' | 'int' | 'array';\ntype goCmdFlagTypeName = 'String' | 'Bool' | 'Int' | 'Array';\ntype typeValue = goTypeName | settingsTreeType | 'hash';\ninterface settingsTypeObject { type: typeValue }\ntype settingsTreeType = Record<string, settingsTypeObject>;\n\nfunction assert(predicate: boolean, error: string) {\n  if (!predicate) {\n    throw new Error(error);\n  }\n}\n\nconst digit = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine'];\n\nfunction capitalize(s: string) {\n  // Also turn a leading digit into a string so '9p' becomes 'NineP'\n  return s.replace(/^(\\d?)(.)(.*)/,\n    (_, maybeDigit, first, rest) => (digit[maybeDigit] || '') + first.toUpperCase() + rest);\n}\n\nfunction capitalizeParts(s: string) {\n  return s.split('.').map(capitalize).join('.');\n}\n\n/**\n * Replace each sequence of capital letters after a lower-case one with a \"-\"\n * followed by its lower-case conversion. Different from lodash.kebabCase,\n * which assumes the last upper-case letter in a sequence starts the next\n * inner word-part, and would convert `numberCPUs` to `number-cp-us`\n */\nfunction kebabCase(s: string) {\n  return s.replace(/(?<=[a-z])([A-Z]+)/g, m => `-${ m.toLowerCase() }`);\n}\n\nfunction lastName(s: string): string {\n  return s.split('.').pop() ?? '';\n}\n\nclass Generator {\n  constructor() {\n    this.commandFlags = [];\n    this.settingsTree = { version: { type: 'int' } };\n  }\n\n  commandFlags: commandFlagType[];\n  settingsTree: settingsTreeType;\n\n  protected async loadInput(inputFile: string): Promise<yamlObject> {\n    const contents = (await fs.promises.readFile(inputFile)).toString();\n\n    try {\n      return yaml.parse(contents);\n    } catch (e) {\n      console.error(`Can't parse input file ${ inputFile }\\n${ contents }\\n\\nError: ${ e }`, e);\n      throw (e);\n    }\n  }\n\n  protected processInput(obj: yamlObject, inputFile: string): void {\n    const preferences = obj?.components?.schemas?.preferences;\n\n    if (!preferences) {\n      throw new Error(`Can't find components.schemas.preferences in ${ inputFile }`);\n    }\n    assert(preferences.type === 'object', `Expected preferences.type = 'object', got ${ preferences.type }`);\n    assert(Object.keys(preferences.properties).length > 0, `Not a properties object: ${ preferences.properties }`);\n    for (const propertyName of Object.keys(preferences.properties)) {\n      this.walkProperty(propertyName, preferences.properties[propertyName], false, this.settingsTree);\n    }\n  }\n\n  protected async emitOutput(outputFile: string) {\n    const options = { rmWhitespace: false };\n    const templateFile = 'scripts/assets/options.go.templ';\n\n    const linesForJSON = this.collectServerSettingsForJSON(this.settingsTree, true, '');\n    const linesWithoutJSON = this.collectServerSettingsForJSON(this.settingsTree, false, '');\n    const data = {\n      commandFlags:     this.commandFlags,\n      linesForJSON:     linesForJSON.join('\\n'),\n      linesWithoutJSON: linesWithoutJSON.join('\\n'),\n      settingsVersion:  CURRENT_SETTINGS_VERSION,\n      kebabCase,\n    };\n    const renderedContent = await ejs.renderFile(templateFile, data, options);\n\n    if (!renderedContent) {\n      throw new Error('ejs.renderFile returned nothing');\n    }\n    if (outputFile === '-') {\n      console.log(renderedContent);\n\n      return;\n    }\n    await fs.promises.mkdir(path.dirname(outputFile), { recursive: true });\n    await fs.promises.writeFile(outputFile, renderedContent);\n  }\n\n  protected collectServerSettingsForJSON(settingsTree: settingsTreeType, includeJSONTag: boolean, indent: string): string[] {\n    return Object.keys(settingsTree).flatMap((propertyName) => {\n      return this.collectServerSettingsForJSONProperty(propertyName, settingsTree[propertyName], includeJSONTag, indent);\n    });\n  }\n\n  protected collectServerSettingsForJSONProperty(propertyName: string, typeWrapper: settingsTypeObject, includeJSONTag: boolean, indent: string): string[] {\n    if (typeof (typeWrapper.type) === 'object') {\n      const lines: string[] = [];\n\n      lines.push(`${ indent }${ capitalize(propertyName) } struct {`);\n      lines.push(...this.collectServerSettingsForJSON(typeWrapper.type, includeJSONTag, `${ indent }  `));\n      const lastLineParts = [indent, '}'];\n\n      if (includeJSONTag) {\n        lastLineParts.push(` \\`json:\"${ propertyName }\"\\``);\n      }\n      lines.push(lastLineParts.join(''));\n\n      return lines;\n    } else {\n      const onlyLineParts = [indent, capitalize(propertyName), ' '];\n\n      if (typeWrapper.type === 'array') {\n        onlyLineParts.push('[]string');\n      } else if (typeWrapper.type === 'hash') {\n        onlyLineParts.push('map[string]interface{}');\n      } else {\n        if (includeJSONTag) {\n          onlyLineParts.push('*');\n        }\n        onlyLineParts.push(typeWrapper.type);\n      }\n      if (includeJSONTag) {\n        onlyLineParts.push(`\\`json:\"${ propertyName },omitempty\"\\``);\n      }\n\n      return [onlyLineParts.join('')];\n    }\n  }\n\n  protected convertStringsToGolang(enums: string[] | undefined): string {\n    return !enums ? '' : `[]string{${ enums.map(s => JSON.stringify(s) ).join(', ') }}`;\n  }\n\n  protected getCommandLineArgValue(flagType: goCmdFlagTypeName, capitalizedName: string) {\n    switch (flagType) {\n    case 'Bool':\n      return `+\"=\"+strconv.FormatBool(specifiedSettings.${ capitalizedName })`;\n    case 'Int':\n      return `, strconv.Itoa(specifiedSettings.${ capitalizedName })`;\n    case 'String':\n      return `, specifiedSettings.${ capitalizedName }`;\n    case 'Array':\n      return '';\n    }\n  }\n\n  protected getFullUsageNote(usageNote: string, rawEnums: undefined | string[]): string {\n    const usageParts = [usageNote];\n\n    if (rawEnums) {\n      usageParts.push(`(allowed values: [${ rawEnums.join(', ' ) }])`);\n    }\n\n    return usageParts.join(' ').trim();\n  }\n\n  protected updateLeaf(propertyName: string,\n    capitalizedName: string,\n    lcTypeName: goTypeName,\n    flagType: goCmdFlagTypeName,\n    defaultValue: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType) {\n    const enums = this.convertStringsToGolang(preference.enum);\n    const usageNote = preference['x-rd-usage'] ?? '';\n    const newFlag: commandFlagType = {\n      capitalizedName,\n      defaultValue,\n      flagType,\n      propertyName,\n      enums,\n      aliasFor:  '',\n      valuePart: this.getCommandLineArgValue(flagType, capitalizedName),\n      notAvailable,\n    };\n\n    newFlag.usageNote = this.getFullUsageNote(usageNote, preference.enum);\n    settingsTree[lastName(propertyName)] = { type: lcTypeName };\n    this.commandFlags.push(newFlag);\n    for (const alias of preference['x-rd-aliases'] ?? []) {\n      this.commandFlags.push(Object.assign({}, newFlag, { propertyName: alias, aliasFor: propertyName }));\n    }\n  }\n\n  protected walkProperty(\n    propertyName: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType): void {\n    const platforms = preference['x-rd-platforms'] ?? [];\n\n    notAvailable ||= preference['x-rd-hidden'];\n    notAvailable ||= platforms.length > 0 && !platforms.includes(process.platform);\n    switch (preference.type) {\n    case 'object':\n      return this.walkPropertyObject(propertyName, preference, notAvailable, settingsTree);\n    case 'boolean':\n      return this.walkPropertyBoolean(propertyName, preference, notAvailable, settingsTree);\n    case 'string':\n      return this.walkPropertyString(propertyName, preference, notAvailable, settingsTree);\n    case 'integer':\n      return this.walkPropertyInteger(propertyName, preference, notAvailable, settingsTree);\n    case 'array':\n      // not yet available\n      return this.walkPropertyArray(propertyName, preference, settingsTree);\n    default:\n      throw new Error(`walkProperty: unexpected preference.type: '${ preference.type }'`);\n    }\n  }\n\n  protected walkPropertyArray(\n    propertyName: string,\n    preference: yamlObject,\n    settingsTree: settingsTreeType,\n  ): void {\n    this.updateLeaf(propertyName, capitalizeParts(propertyName),\n      'array', 'Array', 'nil',\n      preference,\n      true,\n      settingsTree);\n  }\n\n  protected walkPropertyBoolean(\n    propertyName: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType,\n  ): void {\n    this.updateLeaf(propertyName, capitalizeParts(propertyName),\n      'bool', 'Bool', 'false',\n      preference,\n      notAvailable,\n      settingsTree);\n  }\n\n  protected walkPropertyInteger(\n    propertyName: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType,\n  ): void {\n    this.updateLeaf(propertyName, capitalizeParts(propertyName),\n      'int', 'Int', '0',\n      preference,\n      notAvailable,\n      settingsTree);\n  }\n\n  protected walkPropertyObject(\n    propertyName: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType): void {\n    if (preference.additionalProperties) {\n      settingsTree[lastName(propertyName)] = { type: 'hash' };\n\n      return;\n    }\n    const properties = preference.properties;\n\n    assert(Object.keys(properties).length > 0, `Not a properties object: ${ properties }`);\n    const innerSetting: settingsTreeType = {};\n\n    for (const innerName in properties) {\n      this.walkProperty(`${ propertyName }.${ innerName }`, properties[innerName], notAvailable, innerSetting);\n    }\n\n    settingsTree[lastName(propertyName)] = { type: innerSetting };\n  }\n\n  protected walkPropertyString(\n    propertyName: string,\n    preference: yamlObject,\n    notAvailable: boolean,\n    settingsTree: settingsTreeType,\n  ): void {\n    this.updateLeaf(propertyName, capitalizeParts(propertyName),\n      'string', 'String', '\"\"',\n      preference,\n      notAvailable,\n      settingsTree);\n  }\n\n  async run(argv: string[]): Promise<void> {\n    if (argv.length < 1) {\n      throw new Error(`Not enough arguments: [${ argv.join(' ') }]; Usage: scriptFile inputFile [outputFile]`);\n    }\n    const obj = await this.loadInput(argv[0]);\n\n    this.processInput(obj, argv[0]);\n    await this.emitOutput(argv[1] ?? '-');\n    if (argv[1]) {\n      execFileSync('gofmt', ['-w', argv[1]]);\n    }\n  }\n}\n\nconst idx = process.argv.findIndex(node => node.endsWith('generateCliCode.ts'));\n\nif (idx === -1) {\n  console.error(\"Can't find generateCliCode.ts in argv \", process.argv);\n  process.exit(1);\n}\nconst args = process.argv.slice(idx + 1);\n\nif (args[args.length - 1] !== '-') {\n  console.log(`Generating ${ args[args.length - 1] }...`);\n}\n(new Generator()).run(args).catch((e) => {\n  console.error(e);\n  if (e.stderr) {\n    console.log(e.stderr.toString());\n  }\n  if (e.stdout) {\n    console.log(e.output.toString());\n  }\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/go-license-check.sh",
    "content": "#!/usr/bin/env bash\n\nset -o errexit -o nounset -o pipefail\n\ngo install github.com/google/go-licenses@v1.6.0\n\n# https://github.com/google/go-licenses/issues/244#issuecomment-1885198141\nexport GOTOOLCHAIN=local\n\nARGS=(\n    \"--include_tests\"\n    # We just copy the allowed licenses from the CNCF list:\n    # https://github.com/cncf/foundation/blob/main/allowed-third-party-license-policy.md#approved-licenses-for-allowlist\n    \"--allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-2-Clause-FreeBSD,BSD-3-Clause,MIT,ISC,Python-2.0,PostgreSQL,X11,Zlib\"\n    # skipping our own modules because we only have a LICENSE file in the root of the repo\n    \"--ignore=github.com/rancher-sandbox/rancher-desktop\"\n    # CNCF has approved several exceptions for MPL-2 licensed modules, including those below\n    # https://github.com/cncf/foundation/blob/1e80c35/license-exceptions/cncf-exceptions-2019-11-01.spdx\n    \"--ignore=github.com/hashicorp/hcl\"\n    \"--ignore=github.com/hashicorp/errwrap\"\n    \"--ignore=github.com/hashicorp/go-multierror\"\n)\n\nfind src/go -mindepth 1 -maxdepth 1 -type d | while IFS= read -r DIR; do\n    pushd \"$DIR\"\n    \"$(go env GOPATH)/bin/go-licenses\" check \"${ARGS[@]}\" ./...\n    popd >/dev/null\ndone\n"
  },
  {
    "path": "scripts/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/scripts\n\ngo 1.25.0\n\nrequire golang.org/x/mod v0.34.0\n"
  },
  {
    "path": "scripts/go.sum",
    "content": "golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\n"
  },
  {
    "path": "scripts/install-latest-ci.sh",
    "content": "#!/usr/bin/env bash\n\n# Download the latest CI build and install it.\n# NOTE On Linux, \"user\" installs to `~/opt/rancher-desktop`\n\nset -o errexit -o nounset -o pipefail\nset -o xtrace\n\n: \"${OWNER:=rancher-sandbox}\" # Repository owner to fetch from\n: \"${REPO:=rancher-desktop}\"  # Repository to fetch from\n: \"${BRANCH:=main}\"           # Branch to fetch from\n: \"${PR:=}\"                   # PR number to fetch from (overrides BRANCH)\n: \"${ID:=}\"                   # If set, use the specific Action run.\n: \"${WORKFLOW:=package.yaml}\" # Name of workflow that must have succeeded\n: \"${BATS_DIR:=${TMPDIR:-/tmp}/bats}\" # Directory to extract BATS tests to.\n: \"${INSTALL_MODE:=zip}\"      # One of `skip`, `zip`, or `installer`\n: \"${ZIP_NAME:=}\"             # If set, output the zip file name to this file.\n\n: \"${RD_LOCATION:=user}\"\n\nif ! [[ $RD_LOCATION =~ ^(system|user)$ ]]; then\n    echo \"RD_LOCATION must be either 'system' or 'user' (got '$RD_LOCATION')\" >&2\n    exit 1\nfi\nif ! [[ $INSTALL_MODE =~ ^(skip|zip|installer)$ ]]; then\n    echo \"INSTALL_MODE must be one of 'skip', 'zip', or 'installer' (got '$INSTALL_MODE')\" >&2\n    echo \"  skip:      Do not install at all\"\n    echo \"  zip:       Install from the zip file (default)\"\n    echo \"  installer: Install from the installer (or from zip file if not available)\"\n    exit 1\nfi\n\n: \"${TMPDIR:=/tmp}\" # If TMPDIR is unset, set it to something reasonable.\n\nget_platform() {\n    case \"$(uname -s)%%$(uname -r)\" in\n    Darwin*)\n        echo darwin;;\n    MINGW*|*-WSL2)\n        echo win32;;\n    *)\n        echo linux;;\n    esac\n}\n\n# Get the run ID and store it into the global environment variable $ID.\n# May also update $BRANCH for pull requests.\ndetermine_run_id() {\n    if [[ -n $ID ]]; then\n        return 0\n    fi\n    local args=(\n        --repo \"$OWNER/$REPO\"\n        run list\n        --status success\n        --workflow \"$WORKFLOW\"\n        --limit 1\n        --json databaseId\n        --jq '.[].databaseId'\n    )\n    if [[ -n $PR ]]; then\n        BRANCH=$(gh pr view --repo \"$OWNER/$REPO\" --json headRefName --jq .headRefName \"$PR\")\n        args+=(--event pull_request)\n    fi\n    if [[ -z $BRANCH ]]; then\n        echo \"Failed to find relevant branch to download from\" >&2\n        exit 1\n    fi\n    args+=(--branch \"$BRANCH\")\n    ID=$(gh \"${args[@]}\")\n    if [[ -z $ID ]]; then\n        echo \"Failed to find run ID to download from\" >&2\n        exit 1\n    fi\n}\n\nwslpath_from_win32_env() {\n    if [[ \"$(uname -s)\" =~ MINGW* ]]; then\n        # When running under WSL, the environment variables are set but to\n        # Windows-style paths; however, `cd` works with those.  Also, under\n        # MinGW the relevant variables are upper case.\n        local var=\"${1^^}\"\n        (\n            cd \"${!var}\"\n            pwd\n        )\n    else\n        # The cmd.exe _sometimes_ returns an empty string when invoked in a subshell\n        # wslpath \"$(cmd.exe /c \"echo %$1%\" 2>/dev/null)\" | tr -d \"\\r\"\n        # Let's see if powershell.exe avoids this issue\n        wslpath \"$(powershell.exe -Command \"Write-Output \\${Env:$1}\")\" | tr -d \"\\r\"\n    fi\n}\n\ninstall_application() {\n    local archive workdir\n\n    # While the artifact has a consistent name, the single file inside the\n    # artifact does not.  Create a temporary directory that `gh run download`\n    # will download into, so we can pick out the file that it creates.\n    workdir=$(mktemp -d \"$TMPDIR/rd-install.XXXXXXXXXX\")\n    if [[ -z \"$workdir\" || ! -d \"$workdir\" ]]; then\n        echo \"Failed to create temporary directory\" >&2\n        exit 1\n    fi\n    case \"$(get_platform)\" in\n    darwin)\n        ARCH=x86_64\n        if [ \"$(uname -m)\" = \"arm64\" ]; then\n            ARCH=aarch64\n        fi\n        archive=\"Rancher Desktop-mac.$ARCH.zip\"\n        ;;\n    win32)\n        case $INSTALL_MODE in\n        zip)\n            archive=\"Rancher Desktop-win.zip\"\n            ;;\n        installer)\n            archive=\"Rancher Desktop Setup.msi\"\n            ;;\n        esac\n        ;;\n    linux)\n        archive=\"Rancher Desktop-linux.zip\"\n        ;;\n    esac\n    gh run download --repo \"$OWNER/$REPO\" \"$ID\" --dir \"$workdir\" --name \"$archive\"\n\n    # `gh run download` extracts the artifact into the provided directory.\n    local zip=(\"$workdir\"/*)\n    if [[ \"${#zip[@]}\" -ne 1 ]]; then\n        echo \"Cannot find artifact from $archive\"\n        rm -rf \"$workdir\"\n        exit 1\n    fi\n    local zip_abspath=\"$TMPDIR/${zip[0]##*/}\"\n    mv \"${zip[0]}\" \"$zip_abspath\"\n    rm -rf \"$workdir\"\n\n    if [[ -n $ZIP_NAME ]]; then\n        echo \"${zip_abspath##*/}\" > \"$ZIP_NAME\"\n    fi\n\n    local dest\n\n    case \"$(get_platform)\" in\n    darwin)\n        # Extract from inner archive into /Applications\n        dest=\"/Applications\"\n        if [ \"$RD_LOCATION\" = \"user\" ]; then\n            dest=\"$HOME/$dest\"\n        fi\n\n        local app=\"Rancher Desktop.app\"\n        rm -rf \"${dest:?}/$app\"\n        unzip -o \"$zip_abspath\" \"$app/*\" -d \"$dest\" >/dev/null\n        ;;\n    win32)\n        case $INSTALL_MODE in\n        zip)\n            local app='Rancher Desktop'\n            case \"$RD_LOCATION\" in\n            system)\n                dest=\"$(wslpath_from_win32_env ProgramFiles)\";;\n            user)\n                dest=\"$(wslpath_from_win32_env LOCALAPPDATA)/Programs\";;\n            *)\n                printf \"Installing to %s is not supported on Windows.\\n\" \\\n                    \"$RD_LOCATION\" >&2\n                exit 1;;\n            esac\n            rm -rf \"${dest:?}/$app\"\n            # For some reason, the Windows archive doesn't put everything in a\n            # subdirectory like Linux & macOS do.\n            mkdir -p \"$dest/$app\"\n            unzip -o \"$zip_abspath\" -d \"$dest/$app\" >/dev/null\n            ;;\n        installer)\n            local allusers=1\n            local installer\n            installer=$(cygpath --windows \"$zip_abspath\")\n            case \"$RD_LOCATION\" in\n                system)\n                    ;;\n                user)\n                    allusers=0;;\n                *)\n                    printf \"Installing to %s is not supported on Windows.\\n\" \\\n                        \"$RD_LOCATION\" >&2\n                    exit 1;;\n            esac\n            MSYS2_ARG_CONV_EXCL='*' msiexec.exe \\\n                /i \"$installer\" /passive /norestart \\\n                ALLUSERS=$allusers WSLINSTALLED=1\n            # msiexec returns immediately and runs in the background; wait for that\n            # process to exit before continuing.\n            local deadline completed\n            deadline=$(( $(date +%s) + 10 * 60 ))\n            while [[ $(date +%s) -lt $deadline ]]; do\n                if MSYS2_ARG_CONV_EXCL='*' tasklist.exe /FI \"ImageName eq msiexec.exe\" | grep msiexec; then\n                    printf \"Waiting for msiexec to finish: %s/%s\\n\" \"$(date)\" \"$(date --date=\"@$deadline\")\"\n                    sleep 10\n                else\n                    completed=true\n                    break\n                fi\n            done\n            if [[ -z \"${completed:-}\" ]]; then\n                echo \"msiexec took too long to finish, aborting\" >&2\n                exit 1\n            fi\n            ;;\n        esac\n        ;;\n    linux)\n        case $RD_LOCATION in\n        system)\n            dest=\"/opt/rancher-desktop\"\n            sudo rm -rf \"${dest:?}\"\n            sudo unzip -o \"$zip_abspath\" -d \"$dest\" >/dev/null\n            sudo chmod 04755 \"${dest}/chrome-sandbox\"\n            ;;\n        user)\n            dest=\"$HOME/opt/rancher-desktop\"\n            mkdir -p \"${dest:?}\" # Ensure the parent directory exists.\n            rm -rf \"${dest:?}\"\n            unzip -o \"$zip_abspath\" -d \"$dest\" >/dev/null\n            sudo chown root:root \"${dest}/chrome-sandbox\"\n            sudo chmod 04755 \"${dest}/chrome-sandbox\"\n            ;;\n        esac\n        ;;\n    esac\n}\n\ndownload_bats() {\n    # Download the BATS archive; it's automatically extracted one level, i.e.\n    # the wrapper zip file.\n    rm -f \"$TMPDIR/bats.tar.gz\"\n    gh run download --repo \"$OWNER/$REPO\" \"$ID\" --dir \"$TMPDIR\" --name bats.tar.gz\n\n    # Unpack bats into $BATS_DIR\n    rm -rf \"$BATS_DIR\"\n    mkdir -p \"$BATS_DIR\"\n    # Windows tar doesn't like $BATS_DIR when it's a Windows-style path.\n    # So instead of using tar -C, enter that directory first.\n    (\n        cd \"$BATS_DIR\"\n        tar xfz \"$TMPDIR/bats.tar.gz\"\n    )\n}\n\ndetermine_run_id\n\nif [[ \"$INSTALL_MODE\" != \"skip\" ]]; then\n    install_application\nfi\ndownload_bats\n"
  },
  {
    "path": "scripts/k3s-versions.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"golang.org/x/mod/semver\"\n)\n\nconst (\n\t// golang.org/x/mod/semver *requires* a leading 'v' on versions, and will add missing minor/patch numbers.\n\tminimumVersion = \"v1.25.3\"\n\t// The K3s channels endpoint\n\tk3sChannelsEndpoint = \"https://update.k3s.io/v1-release/channels\"\n)\n\ntype Channels struct {\n\tData []Channel `json:\"data\"`\n}\ntype Channel struct {\n\tName   string `json:\"name\"`\n\tLatest string `json:\"latest\"`\n}\n\n// getK3sChannels returns a map of all non-prerelease channels, plus \"latest\" and \"stable\".\n// The values are the latest release for each channel.\nfunc getK3sChannels(ctx context.Context) (map[string]string, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, k3sChannelsEndpoint, http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create HTTP request: %w\", err)\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get k3s channels: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"update channel request failed with status: %s\", resp.Status)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response body for k3s update channel: %w\", err)\n\t}\n\n\tvar channels Channels\n\tif err := json.Unmarshal(body, &channels); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response from k3s update channel: %w\", err)\n\t}\n\n\tk3sChannels := make(map[string]string)\n\tfor _, channel := range channels.Data {\n\t\tswitch {\n\t\tcase channel.Name == \"latest\" || channel.Name == \"stable\":\n\t\t\t// process this channel.\n\t\tcase semver.Prerelease(channel.Latest) != \"\":\n\t\t\tcontinue\n\t\tcase semver.IsValid(channel.Latest) && semver.Compare(channel.Latest, minimumVersion) >= 0:\n\t\t\t// process this channel.\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\t\t// Turn \"v1.31.3+k3s1\" into \"1.31.3\"\n\t\tlatest := strings.TrimPrefix(channel.Latest, \"v\")\n\t\tlatest = strings.SplitN(latest, \"+\", 2)[0]\n\t\tk3sChannels[channel.Name] = latest\n\t}\n\n\treturn k3sChannels, nil\n}\n\ntype GithubRelease struct {\n\tTagName    string `json:\"tag_name\"`\n\tDraft      bool   `json:\"draft\"`\n\tPrerelease bool   `json:\"prerelease\"`\n}\n\n// getGithubReleasesPage fetches a single page of GitHub releases and returns a list\n// of all non-draft, non-prerelease releases above the minimumVersion.\nfunc getGithubReleasesPage(ctx context.Context, page int) ([]GithubRelease, error) {\n\turl := fmt.Sprintf(\"https://api.github.com/repos/k3s-io/k3s/releases?page=%d\", page)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request for %q: %w\", url, err)\n\t}\n\ttoken := os.Getenv(\"GH_TOKEN\")\n\tif token == \"\" {\n\t\ttoken = os.Getenv(\"GITHUB_TOKEN\")\n\t}\n\tif token != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"token \"+token)\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to make request for %q: %w\", url, err)\n\t}\n\tdefer resp.Body.Close()\n\tif resp.StatusCode != http.StatusOK {\n\t\t//nolint:revive // error-strings\n\t\treturn nil, fmt.Errorf(\"GitHub API request failed with status: %s\", resp.Status)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response body for %q: %w\", url, err)\n\t}\n\n\tvar releases []GithubRelease\n\tif err := json.Unmarshal(body, &releases); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response for %q: %w\", url, err)\n\t}\n\n\t// Filter desired releases here, so caller will stop requesting additional pages if there are\n\t// no more matches (heuristics, but releases are returned in reverse chronological order).\n\treleases = slices.DeleteFunc(releases, func(release GithubRelease) bool {\n\t\treturn release.Draft || release.Prerelease || semver.Compare(release.TagName, minimumVersion) < 0\n\t})\n\treturn releases, nil\n}\n\n// getGithubReleases returns a sorted list of all matching GitHub releases.\nfunc getGithubReleases(ctx context.Context) ([]string, error) {\n\treleaseMap := make(map[string]string)\n\tfor page := 1; ; page++ {\n\t\treleases, err := getGithubReleasesPage(ctx, page)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif len(releases) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tfor _, release := range releases {\n\t\t\tversion := semver.Canonical(release.TagName)\n\t\t\t// for each version we only keep the latest k3s patch, i.e. +k3s2 instead of +k3s1\n\t\t\tif oldTag, ok := releaseMap[version]; ok {\n\t\t\t\toldPatch, _ := strconv.Atoi(strings.TrimPrefix(semver.Build(oldTag), \"+k3s\"))\n\t\t\t\tpatch, _ := strconv.Atoi(strings.TrimPrefix(semver.Build(release.TagName), \"+k3s\"))\n\t\t\t\tif oldPatch > patch {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\treleaseMap[version] = release.TagName\n\t\t}\n\t}\n\n\treturn slices.SortedFunc(maps.Values(releaseMap), semver.Compare), nil\n}\n\nfunc getK3sVersions(ctx context.Context) (string, error) {\n\tk3sChannels, err := getK3sChannels(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error fetching k3s channels: %w\", err)\n\t}\n\n\tgithubReleases, err := getGithubReleases(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error fetching GitHub releases: %w\", err)\n\t}\n\n\tresult := map[string]any{\n\t\t\"cacheVersion\": 2,\n\t\t\"channels\":     k3sChannels,\n\t\t\"versions\":     githubReleases,\n\t}\n\n\t// json.Marshal will produce map keys in sort order\n\tjsonResult, err := json.MarshalIndent(result, \"\", \"  \")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error marshalling result to JSON: %w\", err)\n\t}\n\treturn string(jsonResult), nil\n}\n\nfunc main() {\n\tversions, err := getK3sVersions(context.Background())\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tfmt.Println(versions)\n}\n"
  },
  {
    "path": "scripts/k3s-versions.sh",
    "content": "#!/bin/bash\n\n# This script expects to be called from the root of the repo.\n# It will rebuild resources/k3s-versions.json from both the k3s update\n# channel and the GitHub k3s releases list.\n# Creates a pull request if the new version is different.\n\nset -eu\n\nK3S_VERSIONS=\"resources/k3s-versions.json\"\nBRANCH_NAME=\"gha-update-k3s-versions\"\nNEW_PR=\"true\"\n\nif git rev-parse --verify \"origin/${BRANCH_NAME}\" 2>/dev/null; then\n    # This logic relies on the fact that PR branches inside the repo get automatically\n    # deleted when the PR has been merged. We assume that if the branch exists, there\n    # is also a corresponding PR for it, so we just update the branch with a new commit.\n    git checkout \"$BRANCH_NAME\"\n    NEW_PR=\"false\"\nelse\n    git checkout -b \"$BRANCH_NAME\"\nfi\n\ngo run ./scripts/k3s-versions.go >\"$K3S_VERSIONS\"\n\n# Exit if there are no changes\nif git diff --exit-code; then\n    exit\nfi\n\nexport GIT_CONFIG_COUNT=2\nexport GIT_CONFIG_KEY_0=user.name\nexport GIT_CONFIG_VALUE_0=\"Rancher Desktop GitHub Action\"\nexport GIT_CONFIG_KEY_1=user.email\nexport GIT_CONFIG_VALUE_1=\"donotuse@rancherdesktop.io\"\n\ngit add \"$K3S_VERSIONS\"\ngit commit --signoff --message \"Automated update: k3s-versions.json\"\ngit push origin \"$BRANCH_NAME\"\n\nif [ \"$NEW_PR\" = \"false\" ]; then\n    exit\nfi\n\ngh pr create \\\n    --title \"Update k3s-versions.json\" \\\n    --body \"This pull request contains the latest update to k3s-versions.json.\" \\\n    --head \"$BRANCH_NAME\" \\\n    --base main\n"
  },
  {
    "path": "scripts/lib/build-utils.ts",
    "content": "/**\n * This module is a helper for the build & dev scripts.\n */\n\nimport childProcess from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\nimport util from 'util';\n\nimport spawn from 'cross-spawn';\nimport _ from 'lodash';\nimport webpack from 'webpack';\n\nimport babelConfig from 'babel.config.cjs';\n\n/**\n * A promise that is resolved when the child exits.\n */\ntype SpawnResult = Promise<void> & {\n  child: childProcess.ChildProcess;\n};\n\nexport default {\n  /**\n   * Determine if we are building for a development build.\n   */\n  isDevelopment: true,\n\n  get serial() {\n    return process.argv.includes('--serial');\n  },\n\n  sleep: util.promisify(setTimeout),\n\n  /**\n   * Get the root directory of the repository.\n   */\n  get rootDir() {\n    return path.resolve(import.meta.dirname, '..', '..');\n  },\n\n  get rendererSrcDir() {\n    return path.resolve(this.rootDir, `${ process.env.RD_ENV_PLUGINS_DEV ? '' : 'pkg/rancher-desktop' }`);\n  },\n\n  /**\n   * Get the directory where all of the build artifacts should reside.\n   */\n  get distDir() {\n    return path.resolve(this.rootDir, 'dist');\n  },\n\n  /**\n   * Get the directory holding the generated files.\n   */\n  get appDir() {\n    return path.resolve(this.distDir, 'app');\n  },\n\n  /** The package.json metadata. */\n  get packageMeta() {\n    const raw = fs.readFileSync(path.join(this.rootDir, 'package.json'), 'utf-8');\n\n    return JSON.parse(raw);\n  },\n\n  /**\n  * Spawn a new process, returning the child process.\n  * @param command The executable to spawn.\n  * @param args Arguments to the executable. The last argument may be\n  *                        an Object holding options for child_process.spawn().\n  */\n  spawn(command: string, ...args: any[]): SpawnResult {\n    const options: childProcess.SpawnOptions = {\n      cwd:   this.rootDir,\n      stdio: 'inherit',\n    };\n\n    if (args.concat().pop() instanceof Object) {\n      Object.assign(options, args.pop());\n    }\n\n    const child = spawn(command, args, options);\n\n    const promise = new Promise<void>((resolve, reject) => {\n      child.on('exit', (code, signal) => {\n        if (signal && signal !== 'SIGTERM') {\n          reject(new Error(`Process exited with signal ${ signal }`));\n        } else if (code !== 0 && code !== null) {\n          reject(new Error(`Process exited with code ${ code }`));\n        }\n      });\n      child.on('error', (error) => {\n        reject(error);\n      });\n      child.on('close', resolve);\n    });\n\n    return Object.assign(promise, { child });\n  },\n\n  /**\n   * Execute the passed-in array of tasks and wait for them to finish.  By\n   * default, all tasks are executed in parallel.  The user may pass `--serial`\n   * on the command line to causes the tasks to be executed serially instead.\n   * @param tasks Tasks to execute.\n   */\n  async wait(...tasks: (() => Promise<void>)[]) {\n    if (this.serial) {\n      for (const task of tasks) {\n        await task();\n      }\n    } else {\n      await Promise.all(tasks.map(t => t()));\n    }\n  },\n\n  /**\n   * Get the webpack configuration for the main process.\n   */\n  get webpackConfig(): webpack.Configuration {\n    const mode = this.isDevelopment ? 'development' : 'production';\n\n    const config: webpack.Configuration = {\n      mode,\n      target: 'electron-main',\n      node:   {\n        __dirname:  false,\n        __filename: false,\n      },\n      entry:       { background: path.resolve(this.rootDir, 'background') },\n      experiments: { outputModule: true },\n      externals:   [...Object.keys(this.packageMeta.dependencies)],\n      devtool:     this.isDevelopment ? 'source-map' : false,\n      resolve:     {\n        alias:      { '@pkg': path.resolve(this.rootDir, 'pkg', 'rancher-desktop') },\n        extensions: ['.ts', '.js', '.json', '.node'],\n        modules:    ['node_modules'],\n      },\n      output: {\n        filename:      '[name].js',\n        library:  { type: 'modern-module' },\n        path:          this.appDir,\n      },\n      module: {\n        rules: [\n          {\n            test: /\\.ts$/,\n            use:  {\n              loader:  'ts-loader',\n              options: { transpileOnly: this.isDevelopment, onlyCompileBundledFiles: true },\n            },\n          },\n          {\n            test: /\\.js$/,\n            use:  {\n              loader:  'babel-loader',\n              options: {\n                ...babelConfig,\n                cacheDirectory: true,\n              },\n            },\n            exclude: [/node_modules/, this.distDir],\n          },\n          {\n            test:    /\\.ya?ml$/,\n            exclude: [/(?:^|[/\\\\])assets[/\\\\]scripts[/\\\\]/, this.distDir],\n            use:     { loader: 'js-yaml-loader' },\n          },\n          {\n            test: /\\.node$/,\n            use:  { loader: 'node-loader' },\n          },\n          {\n            test: /(?:^|[/\\\\])assets[/\\\\]scripts[/\\\\]/,\n            use:  { loader: 'raw-loader' },\n          },\n        ],\n      },\n      plugins: [\n        new webpack.EnvironmentPlugin({ NODE_ENV: mode }),\n      ],\n    };\n\n    return config;\n  },\n\n  /**\n   * WebPack configuration for the preload script\n   */\n  get webpackPreloadConfig(): webpack.Configuration {\n    const overrides: webpack.Configuration = {\n      target: 'electron-preload',\n      output: {\n        filename: '[name].js',\n        library:  { type: 'commonjs2' },\n        path:     path.join(this.rootDir, 'resources'),\n      },\n      experiments: { outputModule: false },\n    };\n\n    const result = Object.assign({}, this.webpackConfig, overrides);\n    const rules = (result.module?.rules ?? []).filter(\n      (rule): rule is webpack.RuleSetRule => !!rule && typeof rule === 'object',\n    );\n    const tsLoader = rules.find((rule) => {\n      const { use } = rule;\n\n      if (!use || typeof use !== 'object' || Array.isArray(use)) {\n        return false;\n      }\n\n      return use.loader === 'ts-loader';\n    });\n\n    if (!tsLoader) {\n      console.log('rules', util.inspect(rules, false, null, true));\n      throw new Error('failed to find TS loader');\n    } else if (!tsLoader.use || typeof tsLoader.use !== 'object' || Array.isArray(tsLoader.use)) {\n      throw new Error(`Unexpected TS loader config ${ util.inspect(tsLoader, false, null, true) }`);\n    }\n\n    tsLoader.use.options = _.merge({}, tsLoader.use.options, { compilerOptions: { noEmit: false } });\n\n    result.entry = { preload: path.resolve(this.rendererSrcDir, 'preload', 'index.ts') };\n\n    return result;\n  },\n\n  /**\n   * Build the main process JavaScript code.\n   */\n  buildJavaScript(config: webpack.Configuration): Promise<void> {\n    return new Promise((resolve, reject) => {\n      webpack(config).run((err, stats) => {\n        if (err) {\n          return reject(err);\n        }\n        if (stats?.hasErrors()) {\n          return reject(new Error(stats.toString({ colors: true, errorDetails: true })));\n        }\n        console.log(stats?.toString({ colors: true }));\n        resolve();\n      });\n    });\n  },\n\n  get arch(): NodeJS.Architecture {\n    return process.env.M1 ? 'arm64' : process.arch;\n  },\n\n  /**\n   * Build the preload script.\n   */\n  async buildPreload(): Promise<void> {\n    await this.buildJavaScript(this.webpackPreloadConfig);\n  },\n\n  /**\n   * Build the main process code.\n   */\n  buildMain(): Promise<void> {\n    return this.wait(() => this.buildJavaScript(this.webpackConfig));\n  },\n\n};\n"
  },
  {
    "path": "scripts/lib/dependencies.ts",
    "content": "import fs from 'fs';\n\nimport { ThrottlingOptions } from '@octokit/plugin-throttling';\nimport { Octokit } from 'octokit';\nimport semver from 'semver';\nimport YAML from 'yaml';\n\nimport { getResource } from './download';\n\nexport type DependencyPlatform = 'wsl' | 'linux' | 'darwin' | 'win32';\nexport type Platform = 'linux' | 'darwin' | 'win32';\nexport type GoPlatform = 'linux' | 'darwin' | 'windows';\n\nexport interface DownloadContext {\n  versions:           DependencyVersions;\n  dependencyPlatform: DependencyPlatform;\n  platform:           Platform;\n  goPlatform:         GoPlatform;\n  // whether we are running on M1\n  isM1:               boolean;\n  // resourcesDir is the directory that external dependencies and the like go into\n  resourcesDir:       string;\n  // binDir is for binaries that the user will execute\n  binDir:             string;\n  // internalDir is for binaries that RD will execute behind the scenes\n  internalDir:        string;\n  // dockerPluginsDir is for docker CLI plugins.\n  dockerPluginsDir:   string;\n}\n\nexport interface AlpineLimaISOVersion {\n  // The version of the ISO build\n  isoVersion:    string;\n  // The version of Alpine Linux that the ISO is built on\n  alpineVersion: string\n}\n\ntype Version = string | AlpineLimaISOVersion;\n\nexport interface DependencyVersions {\n  lima:                            string;\n  qemu:                            string;\n  socketVMNet:                     string;\n  alpineLimaISO:                   AlpineLimaISOVersion;\n  WSLDistro:                       string;\n  kuberlr:                         string;\n  helm:                            string;\n  dockerCLI:                       string;\n  dockerBuildx:                    string;\n  dockerCompose:                   string;\n  'golangci-lint':                 string;\n  trivy:                           string;\n  steve:                           string;\n  guestAgent:                      string;\n  rancherDashboard:                string;\n  dockerProvidedCredentialHelpers: string;\n  ECRCredentialHelper:             string;\n  mobyOpenAPISpec:                 string;\n  wix:                             string;\n  moproxy:                         string;\n  spinShim:                        string;\n  certManager:                     string;\n  spinOperator:                    string;\n  spinCLI:                         string;\n  spinKubePlugin:                  string;\n  'check-spelling':                string;\n}\n\nexport const DEP_VERSIONS_PATH = 'pkg/rancher-desktop/assets/dependencies.yaml';\n\n/**\n * Download the given checksum file (which contains multiple checksums) and find\n * the correct checksum for the given executable name.\n * @param checksumURL The URL to download the checksum from.\n * @param executableName The name of the executable expected.\n * @returns The checksum.\n */\nexport async function findChecksum(checksumURL: string, executableName: string): Promise<string> {\n  const allChecksums = await getResource(checksumURL);\n  const desiredChecksums = allChecksums.split(/\\r?\\n/).filter(line => line.endsWith(executableName));\n\n  if (desiredChecksums.length < 1) {\n    throw new Error(`Couldn't find a matching SHA for [${ executableName }] in [${ allChecksums }]`);\n  }\n  if (desiredChecksums.length === 1) {\n    return desiredChecksums[0].split(/\\s+/, 1)[0];\n  }\n  throw new Error(`Matched ${ desiredChecksums.length } hits, not exactly 1, for ${ executableName } in [${ allChecksums }]`);\n}\n\nexport async function readDependencyVersions(path: string): Promise<DependencyVersions> {\n  const rawContents = await fs.promises.readFile(path, 'utf-8');\n\n  return YAML.parse(rawContents);\n}\n\nexport async function writeDependencyVersions(path: string, depVersions: DependencyVersions): Promise<void> {\n  const rawContents = YAML.stringify(depVersions);\n\n  await fs.promises.writeFile(path, rawContents, { encoding: 'utf-8' });\n}\n\n/**\n * A dependency is some binary that we need to track.  Generally this is some\n * third-party software, but it may also be things we build in an external\n * repository, or some binary we build from them.\n */\nexport interface Dependency {\n  /** The name of this dependency. */\n  get name(): string,\n  /**\n   * Other dependencies this one requires.\n   * This must be in the form <name>:<platform>, e.g. \"kuberlr:linux\"\n   */\n  dependencies?: (context: DownloadContext) => string[],\n  /**\n   * Download this dependency.  Note that for some dependencies, this actually\n   * builds from source.\n   */\n  download(context: DownloadContext): Promise<void>\n}\n\n/**\n * A VersionedDependency is a {@link Dependency} where we track a version and\n * can be automatically upgraded (i.e. a pull request made to bump the version).\n */\nexport abstract class VersionedDependency implements Dependency {\n  abstract get name(): string;\n  abstract download(context: DownloadContext): Promise<void>;\n  /**\n   * Returns the available versions of the Dependency.\n   */\n  abstract getAvailableVersions(): Promise<Version[]>;\n\n  /** The current version. */\n  abstract get currentVersion(): Promise<Version>;\n\n  /** The newest version that can be upgraded to. */\n  get latestVersion(): Promise<Version> {\n    return (async() => {\n      const availableVersions = await this.getAvailableVersions();\n\n      return availableVersions.reduce((version1, version2) => {\n        return this.rcompareVersions(version1, version2) < 0 ? version1 : version2;\n      });\n    })();\n  }\n\n  /** Whether we can upgrade. */\n  get canUpgrade(): Promise<boolean> {\n    return (async() => {\n      const current = await this.currentVersion;\n      const latest = await this.latestVersion;\n      const compare = this.rcompareVersions(current, latest);\n\n      if (compare < 0) {\n        throw new Error(`${ this.name } at ${ current }, is greater than latest version ${ latest }`);\n      }\n\n      return compare > 0;\n    })();\n  }\n\n  /**\n   * Update the version manifest (e.g. `dependencies.yaml`) for this dependency,\n   * in preparation for making a pull request.\n   * @returns The set of files that have been modified.\n   */\n  abstract updateManifest(newVersion: Version): Promise<Set<string>>;\n\n  /**\n   * Compare the two versions.  The return value is:\n   * Value | Description\n   * --- | ---\n   * -1 | `version1` is higher\n   * 0 | `version1` and `version2` are equal\n   * 1 | `version2` is higher\n   *\n   * The default implementation compares version strings that look like `0.1.2.rd3????`.\n   * Note that anything after the number after `rd` is ignored.\n   */\n  rcompareVersions(version1: Version, version2: Version): -1 | 0 | 1 {\n    if (typeof version1 !== 'string' || typeof version2 !== 'string') {\n      throw new TypeError(`default rcompareVersions only handles string versions (got ${ version1 } / ${ version2 })`);\n    }\n\n    const semver1 = semver.coerce(version1);\n    const semver2 = semver.coerce(version2);\n\n    if (semver1 === null || semver2 === null) {\n      throw new Error(`One of ${ version1 } and ${ version2 } failed to be coerced to semver`);\n    }\n\n    if (semver1.raw !== semver2.raw) {\n      return semver.rcompare(semver1, semver2);\n    }\n\n    // If the two versions are equal, assume we have different build suffixes\n    // e.g. \"0.19.0.rd5\" vs \"0.19.0.rd6\"\n    const [, match1] = /^\\d+\\.\\d+\\.\\d+\\.rd(\\d+)$/.exec(version1) ?? [];\n    const [, match2] = /^\\d+\\.\\d+\\.\\d+\\.rd(\\d+)$/.exec(version2) ?? [];\n\n    if (!match1 && !match2) {\n      // Neither have .rd suffix; treat as equal.\n      return 0;\n    }\n    if (!match1 || !match2) {\n      // One of the two is invalid; prefer the valid one.\n      return match1 ? -1 : match2 ? 1 : 0;\n    }\n\n    return Math.sign(parseInt(match2, 10) - parseInt(match1, 10)) as -1 | 0 | 1;\n  }\n\n  /** Format the version as a string for display. */\n  static versionString(v: Version): string {\n    return typeof v === 'string' ? v : v.isoVersion;\n  }\n}\n\n/**\n * A GlobalDependency is a dependency where the version is managed in the file\n * {@link DEP_VERSIONS_PATH}.\n */\nexport function GlobalDependency<T extends abstract new(...args: any[]) => VersionedDependency>(Base: T) {\n  abstract class GlobalDependency extends Base {\n    /** The name of this dependency; it must be a key in DEP_VERSIONS_PATH. */\n    abstract get name(): keyof DependencyVersions;\n    /** Cache of the loaded {@link DependencyVersions}; should not be used directly. */\n    static #depVersionsCache: Promise<DependencyVersions> | undefined;\n    /** Get the {@link DependencyVersions} as found on disk. */\n    static depVersions(): Promise<DependencyVersions> {\n      GlobalDependency.#depVersionsCache ||= (async() => {\n        return YAML.parse(await fs.promises.readFile(DEP_VERSIONS_PATH, 'utf-8'));\n      })();\n\n      return GlobalDependency.#depVersionsCache;\n    }\n\n    get currentVersion(): Promise<Version> {\n      return GlobalDependency.depVersions().then(v => v[this.name]);\n    }\n\n    async updateManifest(newVersion: string): Promise<Set<string>> {\n      // Make a copy of the read depVersions to not affect other dependencies.\n      const depVersions = structuredClone(await GlobalDependency.depVersions());\n      const name = this.name;\n\n      if (name === 'alpineLimaISO') {\n        throw new Error(`Default updateManifest does not handle ${ name }`);\n      }\n      depVersions[name] = newVersion;\n      const rawContents = YAML.stringify(depVersions);\n\n      await fs.promises.writeFile(DEP_VERSIONS_PATH, rawContents, { encoding: 'utf-8' });\n\n      return new Set([DEP_VERSIONS_PATH]);\n    }\n  }\n\n  return GlobalDependency;\n}\n\n/**\n * A filter for GitHub releases.  Available options are:\n * Value | Description\n * --- | ---\n * `published` | Get GitHub releases (excluding versions marked as *pre-release* on GitHub).\n * `published-pre` | Get GitHub releases (including those marked as *pre-release* on GitHub).\n * `semver` | GitHub releases, excluding those marked as *pre-release*, or those with semver pre-release parts.\n * `custom` | The implementation must override `getAvailableVersions()`.\n */\ntype ReleaseFilter = 'published' | 'published-pre' | 'semver' | 'custom';\n\n/**\n * A {@link VersionedDependency} using GitHub releases.\n */\nexport abstract class GitHubDependency extends VersionedDependency {\n  /** The owner / organization on GitHub. */\n  abstract get githubOwner(): string;\n  /** The repository name (without the owner) on GitHub. */\n  abstract get githubRepo(): string;\n\n  /** Control how to get available releases; defaults to semver. */\n  readonly releaseFilter: ReleaseFilter = 'semver';\n  /**\n   * Converts a version (of the format that is stored in dependencies.yaml)\n   * to a tag that is used in a GitHub release.\n   * The default implementation adds a `v` prefix to the version string.\n   */\n  versionToTagName(version: Version): string {\n    return `v${ version }`;\n  }\n\n  async getAvailableVersions(): Promise<Version[]> {\n    if (this.releaseFilter === 'custom') {\n      throw new Error('class does not override getAvailableVersions()');\n    }\n\n    const tags = await getPublishedReleaseTagNames(this.githubOwner, this.githubRepo, this.releaseFilter);\n\n    return tags.map(tag => tag.replace(/^v/, ''));\n  }\n}\n\nexport interface HasUnreleasedChangesResult { latestReleaseTag: string, hasUnreleasedChanges: boolean }\n\nexport type GitHubRelease = Awaited<ReturnType<Octokit['rest']['repos']['listReleases']>>['data'][0];\n\nlet _octokit: Octokit | undefined;\nlet _octokitAuthToken: string | undefined;\n\n/**\n * Get a cached instance of Octokit, or create a new one as needed.  If the given token does not\n * match the one used to create the cached instance, a new one is created (and cached).\n * @param personalAccessToken Optional GitHub personal access token; defaults to GITHUB_TOKEN.\n */\nexport function getOctokit(personalAccessToken?: string): Octokit {\n  personalAccessToken ||= process.env.GITHUB_TOKEN;\n\n  if (!personalAccessToken) {\n    throw new Error('Please set GITHUB_TOKEN to a PAT to check versions of github-based dependencies.');\n  }\n\n  if (_octokit && _octokitAuthToken === personalAccessToken) {\n    return _octokit;\n  }\n\n  function makeLimitHandler(type: string, maxRetries: number): NonNullable<ThrottlingOptions['onSecondaryRateLimit']> {\n    return (retryAfter, options, octokit, retryCount) => {\n      function getOpt(prop: string) {\n        return options && (prop in options) ? (options as any)[prop] : `(unknown ${ prop })`;\n      }\n\n      let message = `Request ${ type } limit exhausted for request`;\n      let retry = false;\n\n      message += ` ${ getOpt('method') } ${ getOpt('url') }`;\n\n      if (retryCount < maxRetries) {\n        retry = true;\n        message += ` (retrying after ${ retryAfter } seconds: ${ retryCount }/${ maxRetries } retries)`;\n      } else {\n        message += ` (not retrying after ${ maxRetries } retries)`;\n      }\n\n      octokit.log.warn(message);\n\n      return retry;\n    };\n  }\n\n  _octokit = new Octokit({\n    auth:     personalAccessToken,\n    throttle: {\n      onRateLimit:          makeLimitHandler('primary', 3),\n      onSecondaryRateLimit: makeLimitHandler('secondary', 3),\n    },\n  });\n  _octokitAuthToken = personalAccessToken;\n\n  return _octokit;\n}\n\n// Helper function to make iterating through Octokit pagination easier.\n// Pass in a pagination iterator, plus a function to convert one page to a list of results.\nexport async function * iterateIterator<T, U>(input: AsyncIterable<T>, fn: (_: T) => U[]) {\n  for await (const list of input) {\n    yield * fn(list);\n  }\n}\n\nexport type IssueOrPullRequest = Awaited<ReturnType<Octokit['rest']['search']['issuesAndPullRequests']>>['data']['items'][0];\n\n/**\n * Represents the main Rancher Desktop repo (rancher-sandbox/rancher-desktop\n * as of the time of writing) or one of its forks.\n */\nexport class RancherDesktopRepository {\n  owner: string;\n  repo:  string;\n\n  constructor(owner: string, repo: string) {\n    this.owner = owner;\n    this.repo = repo;\n  }\n\n  async createIssue(title: string, body: string, githubToken?: string): Promise<void> {\n    const result = await getOctokit(githubToken).rest.issues.create({\n      owner: this.owner, repo: this.repo, title, body,\n    });\n    const issue = result.data;\n\n    console.log(`Created issue #${ issue.number }: \"${ issue.title }\"`);\n  }\n\n  async reopenIssue(issue: IssueOrPullRequest, githubToken?: string): Promise<void> {\n    await getOctokit(githubToken).rest.issues.update({\n      owner: this.owner, repo: this.repo, issue_number: issue.number, state: 'open',\n    });\n    console.log(`Reopened issue #${ issue.number }: \"${ issue.title }\"`);\n  }\n\n  async closeIssue(issue: IssueOrPullRequest, githubToken?: string): Promise<void> {\n    await getOctokit(githubToken).rest.issues.update({\n      owner: this.owner, repo: this.repo, issue_number: issue.number, state: 'closed',\n    });\n    console.log(`Closed issue #${ issue.number }: \"${ issue.title }\"`);\n  }\n}\n\n/**\n * For a GitHub repository, get a list of published releases and return their\n * tags (including any `v` prefix).\n */\nexport async function getPublishedReleaseTagNames(owner: string, repo: string, releaseFilter: Exclude<ReleaseFilter, 'custom'> = 'semver', githubToken?: string): Promise<string[]> {\n  const response = await getOctokit(githubToken).rest.repos.listReleases({ owner, repo });\n  let releases = response.data;\n\n  // Filter for non-draft releases\n  releases = releases.filter(release => release.published_at !== null);\n\n  // Filter out pre-releases\n  if (releaseFilter !== 'published-pre') {\n    releases = releases.filter(release => !release.prerelease);\n  }\n  let tagNames = releases.map(release => release.tag_name);\n\n  if (releaseFilter === 'semver') {\n    tagNames = tagNames.filter(tag => !semver.coerce(tag)?.prerelease?.length);\n  }\n\n  return tagNames;\n}\n"
  },
  {
    "path": "scripts/lib/download.ts",
    "content": "/**\n * Helpers for downloading files.\n */\n\nimport { execFileSync, spawnSync } from 'child_process';\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\nimport stream from 'stream';\n\nimport { simpleSpawn } from 'scripts/simple_process';\n\ntype ChecksumAlgorithm = 'sha1' | 'sha256' | 'sha512';\n\nexport interface DownloadOptions {\n  expectedChecksum?:  string;\n  checksumAlgorithm?: ChecksumAlgorithm;\n  // Whether to re-download files that already exist.\n  overwrite?:         boolean;\n  // The file mode required.\n  access?:            number;\n  // The file needs a new ad-hoc signature.\n  codesign?:          boolean;\n}\n\nexport type ArchiveDownloadOptions = DownloadOptions & {\n  // The name in the archive of the file; defaults to base name of the destination.\n  entryName?: string;\n};\n\nasync function fetchWithRetry(url: string) {\n  while (true) {\n    try {\n      return await fetch(url, { redirect: 'follow' });\n    } catch (ex: any) {\n      if (ex?.errno === 'EAI_AGAIN') {\n        console.log(`Recoverable error downloading ${ url }, retrying...`);\n        continue;\n      }\n      console.dir(ex);\n      throw ex;\n    }\n  }\n}\n\nfunction checkDownloadStatusOrThrow(url: string, response: Response): void {\n  if (!response.ok) {\n    const requestId = response.headers.get('x-github-request-id');\n    const requestAnnotation = requestId ? ` [request: ${ requestId }]` : '';\n    throw new Error(`Error downloading ${ url } (${ response.status }) ${ response.statusText }${ requestAnnotation }`);\n  }\n}\n\n/**\n * Download the given URL, making the result executable.\n * @param url The URL to download\n * @param destPath The path to download to\n * @param options Additional options for the download.\n */\nexport async function download(url: string, destPath: string, options: DownloadOptions = {}): Promise<void> {\n  const expectedChecksum = options.expectedChecksum;\n  const checksumAlgorithm = options.checksumAlgorithm ?? 'sha256';\n  const overwrite = options.overwrite ?? false;\n  const access = options.access ?? fs.constants.X_OK;\n\n  if (!overwrite) {\n    try {\n      await fs.promises.access(destPath, access);\n      console.log(`${ destPath } already exists, not re-downloading.`);\n\n      return;\n    } catch (ex: any) {\n      if (ex.code !== 'ENOENT') {\n        throw ex;\n      }\n    }\n  }\n  console.log(`Downloading ${ url } to ${ destPath }...`);\n  await fs.promises.mkdir(path.dirname(destPath), { recursive: true });\n  const response = await fetchWithRetry(url);\n\n  checkDownloadStatusOrThrow(url, response);\n  if (!response.body) {\n    throw new Error(`Error downloading ${ url }: did not receive response body`);\n  }\n  const tempPath = `${ destPath }.download`;\n\n  try {\n    const file = fs.createWriteStream(tempPath);\n\n    await response.body.pipeTo(stream.Writable.toWeb(file));\n\n    if (expectedChecksum) {\n      const actualChecksum = await getChecksumForFile(tempPath, checksumAlgorithm);\n\n      if (actualChecksum !== expectedChecksum) {\n        throw new Error(`Expecting URL ${ url } to have ${ checksumAlgorithm } [${ expectedChecksum }], got [${ actualChecksum }]`);\n      }\n    }\n    const mode =\n            (access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;\n\n    await fs.promises.chmod(tempPath, mode);\n    await fs.promises.rename(tempPath, destPath);\n  } finally {\n    try {\n      await fs.promises.unlink(tempPath);\n    } catch (ex: any) {\n      if (ex.code !== 'ENOENT') {\n        console.error(ex);\n      }\n    }\n  }\n\n  if (options.codesign) {\n    spawnSync(\n      'codesign',\n      ['--force', '--sign', '-', destPath],\n      { stdio: 'inherit' },\n    );\n  }\n}\n\n/**\n * Compute the checksum for a given file\n * @param inputPath The file to checksum.\n * @param checksumAlgorithm The checksum algorithm to use.\n * @returns The hex-encoded checksum of the file.\n */\nasync function getChecksumForFile(inputPath: string, checksumAlgorithm: ChecksumAlgorithm = 'sha256'): Promise<string> {\n  const hash = crypto.createHash(checksumAlgorithm);\n\n  await new Promise((resolve) => {\n    hash.on('finish', resolve);\n    fs.createReadStream(inputPath).pipe(hash);\n  });\n\n  return hash.digest('hex');\n}\n\n/**\n * Return the contents of a given URL.\n * @param url The URL to download\n * @returns The file contents.\n */\nexport async function getResource(url: string): Promise<string> {\n  const response = await fetchWithRetry(url);\n\n  checkDownloadStatusOrThrow(url, response);\n\n  return await response.text();\n}\n\n/**\n * Download a tar.gz file to a temp dir, expand,\n * and move the expected binary to the final dir\n *\n * @param url The URL to download.\n * @param destPath The path to download to, including the executable name.\n * @param options Additional options for the download.\n * @returns The full path of the final binary.\n */\nexport async function downloadTarGZ(url: string, destPath: string, options: ArchiveDownloadOptions = {}): Promise<string> {\n  const overwrite = options.overwrite ?? false;\n  const access = options.access ?? fs.constants.X_OK;\n\n  if (!overwrite) {\n    try {\n      await fs.promises.access(destPath, access);\n      console.log(`${ destPath } already exists, not re-downloading.`);\n\n      return destPath;\n    } catch (ex: any) {\n      if (ex.code !== 'ENOENT') {\n        throw ex;\n      }\n    }\n  }\n  const binaryBasename = path.basename(destPath, '.exe');\n  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));\n  const fileToExtract = options.entryName || path.basename(destPath);\n\n  try {\n    const tgzPath = path.join(workDir, `${ binaryBasename }.tar.gz`);\n    const args = ['tar', '-zxf', tgzPath, '--directory', workDir, fileToExtract];\n    const mode =\n            (access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;\n\n    await download(url, tgzPath, { ...options, access: fs.constants.W_OK });\n    if (os.platform().startsWith('win')) {\n      // On Windows, force use the bundled bsdtar.\n      // We may find GNU tar on the path, which looks at the Windows-style path\n      // and considers C:\\Temp to be a reference to a remote host named `C`.\n      const systemRoot = process.env.SystemRoot;\n\n      if (!systemRoot) {\n        throw new Error('Could not find system root');\n      }\n      args[0] = path.join(systemRoot, 'system32', 'tar.exe');\n    }\n    await simpleSpawn(args[0], args.slice(1));\n    await fs.promises.mkdir(path.dirname(destPath), { recursive: true });\n    await fs.promises.copyFile(path.join(workDir, fileToExtract), destPath);\n    await fs.promises.chmod(destPath, mode);\n  } finally {\n    fs.rmSync(workDir, { recursive: true, maxRetries: 10 });\n  }\n\n  return destPath;\n}\n\n/**\n * Download a zip file to a temp dir, expand,\n * and move the expected binary to the final dir\n *\n * @param url The URL to download.\n * @param destPath The path to download to, including the executable name.\n * @param options Additional options for the download.\n * @returns The full path of the final binary.\n */\nexport async function downloadZip(url: string, destPath: string, options: ArchiveDownloadOptions = {}): Promise<string> {\n  const overwrite = options.overwrite ?? false;\n  const access = options.access ?? fs.constants.X_OK;\n\n  if (!overwrite) {\n    try {\n      await fs.promises.access(destPath, access);\n      console.log(`${ destPath } already exists, not re-downloading.`);\n\n      return destPath;\n    } catch (ex: any) {\n      if (ex.code !== 'ENOENT') {\n        throw ex;\n      }\n    }\n  }\n  const binaryBasename = path.basename(destPath, '.exe');\n  const workDir = fs.mkdtempSync(path.join(os.tmpdir(), `rd-${ binaryBasename }-`));\n  const fileToExtract = options.entryName || path.basename(destPath);\n  const mode =\n        (access & fs.constants.X_OK) ? 0o755 : (access & fs.constants.W_OK) ? 0o644 : 0o444;\n\n  try {\n    const zipPath = path.join(workDir, `${ binaryBasename }.zip`);\n    const args = ['unzip', '-q', '-o', zipPath, fileToExtract, '-d', workDir];\n\n    await download(url, zipPath, { ...options, access: fs.constants.W_OK });\n    execFileSync(args[0], args.slice(1), { stdio: 'inherit' });\n    fs.copyFileSync(path.join(workDir, fileToExtract), destPath);\n    fs.chmodSync(destPath, mode);\n  } finally {\n    fs.rmSync(workDir, { recursive: true, maxRetries: 10 });\n  }\n\n  return destPath;\n}\n"
  },
  {
    "path": "scripts/lib/extension-data.ts",
    "content": "/**\n * This file contains routines to manage the extension data at\n * pkg/rancher-desktop/assets/extension-data.yaml\n */\n\nimport childProcess from 'child_process';\nimport fs from 'fs';\nimport util from 'util';\n\nimport yaml from 'yaml';\n\nimport { DownloadContext, getPublishedReleaseTagNames, VersionedDependency } from './dependencies';\n\nconst EXTENSION_PATH = 'scripts/assets/extension-data.yaml';\nconst EXTENSION_OUTPUT_PATH = 'pkg/rancher-desktop/assets/extension-data.yaml';\n\n/**\n * Information about an extension we manage in the bundled marketplace.\n * This is loaded from `scripts/assets/extension-data.yaml`.\n */\ninterface ExtensionInfo {\n  /** Whether this extension is compatible with containerd; defaults to true. */\n  containerd_compatible?: boolean;\n  /** Override for the logo. */\n  logo?:                  string;\n  /** GitHub repository, as \"org/repo\", used to check for updates. */\n  github_repo?:           string;\n}\n\nexport class Extension extends VersionedDependency {\n  constructor(ref: string, info: ExtensionInfo) {\n    super();\n    const [name, tag] = ref.split(':', 2);\n\n    this.name = name;\n    this.currentVersion = Promise.resolve(tag);\n    this.info = info;\n  }\n\n  /** The extension name, i.e. the image name, excluding the tag. */\n  readonly name:           string;\n  readonly currentVersion: Promise<string>;\n  readonly info:           ExtensionInfo;\n\n  download(context: DownloadContext): Promise<void> {\n    // There is no download for marketplace extension data.\n    return Promise.resolve();\n  }\n\n  async getAvailableVersions(): Promise<string[]> {\n    if (!this.info.github_repo) {\n      return Promise.resolve([]);\n    }\n    const [owner, repo] = this.info.github_repo.split('/', 2);\n\n    return await getPublishedReleaseTagNames(owner, repo);\n  }\n\n  async updateManifest(newVersion: string): Promise<Set<string>> {\n    // We want to try to keep the YAML comments; so we do string replace instead.\n    const fileContents = await fs.promises.readFile(EXTENSION_PATH, 'utf-8');\n    const oldRef = `${ this.name }:${ await this.currentVersion }`;\n    const newRef = `${ this.name }:${ newVersion }`;\n    const newContents = fileContents.replaceAll(oldRef, newRef);\n\n    await fs.promises.writeFile(EXTENSION_PATH, newContents, 'utf-8');\n    await generateExtensionMarketplaceData();\n\n    return new Set([EXTENSION_PATH, EXTENSION_OUTPUT_PATH]);\n  }\n\n  async generateMarketplaceData() {\n    const ref = `${ this.name }:${ await this.currentVersion }`;\n    const execFile = util.promisify(childProcess.execFile);\n    const { stdout:out } = await execFile('docker', ['image', 'list', '--format=json', ref]);\n\n    if (!out.trim()) {\n      // Image not found\n      console.log(`Pulling image ${ ref }`);\n      await execFile('docker', ['pull', ref]);\n    }\n    const { stdout } = await execFile('docker', ['inspect', ref]);\n    const data = JSON.parse(stdout)[0];\n    const labels = data.Config.Labels;\n\n    return {\n      slug:                  this.name,\n      version:               await this.currentVersion,\n      containerd_compatible: this.info.containerd_compatible ?? true,\n      labels,\n      title:                 labels['org.opencontainers.image.title'],\n      logo:                  this.info.logo ?? labels['com.docker.desktop.extension.icon'],\n      publisher:             labels['org.opencontainers.image.vendor'],\n      short_description:     labels['org.opencontainers.image.description'],\n    };\n  }\n}\n\nexport function getExtensions(withGitHubRepo = false): Extension[] {\n  const infoData: Record<string, ExtensionInfo> = yaml.parse(fs.readFileSync(EXTENSION_PATH, 'utf-8'));\n  let entries = Object.entries(infoData);\n\n  if (withGitHubRepo) {\n    entries = entries.filter(([, e]) => !!e.github_repo);\n  }\n\n  return entries.map(([ref, info]) => new Extension(ref, info));\n}\n\n// Generate the extension metadata file for the built-in marketplace.\nexport async function generateExtensionMarketplaceData() {\n  // Break this up creatively to avoid being seen by editors.\n  const warningString = [\n    '# Data generated by running ',\n    '`yarn generate:extension-data`. DO NOT ',\n    'EDIT.',\n  ].join('');\n\n  const extensions = getExtensions();\n  const data = await Promise.all(extensions.map(e => e.generateMarketplaceData()));\n  const result = `${ warningString }\\n${ yaml.stringify(data) }`;\n\n  await fs.promises.writeFile(EXTENSION_OUTPUT_PATH, result, 'utf-8');\n}\n"
  },
  {
    "path": "scripts/lib/installer-win32-gen.tsx",
    "content": "/* eslint-disable @typescript-eslint/ban-ts-comment */\n/** @jsx Element.new */\n\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport path from 'path';\n\n/**\n * Element is a class for interpreting JSX; we only need the bare basics to\n * generate a valid XML as input to the WiX toolchain.\n */\n\nexport class Element {\n  constructor(tag: string, attribs: Record<string, string>, ...children: (Element | string)[]) {\n    this.tag = tag;\n    this.attribs = attribs;\n    this.children = children;\n  }\n\n  /**\n   * Create a new element; this is used by the TypeScript JSX support.\n   */\n  static new(tag: string, attribs: Record<string, string> | null, ...children: (Element | Element[] | string)[]) {\n    return new Element(tag, attribs ?? {}, ...children.flat().filter(x => x));\n  }\n\n  tag:     string;\n  attribs: Record<string, string>;\n\n  children: (Element | string)[];\n\n  /** Convert the Element to serialized XML. */\n  toXML(indent = 0) {\n    const indentString = (new Array(indent + 1)).join(' ');\n    let result = `${ indentString }<${ this.tag }`;\n\n    for (const [key, value] of Object.entries(this.attribs)) {\n      result += ` ${ key }=\"${ value }\"`;\n    }\n    if (this.children.length < 1) {\n      result += '/>\\n';\n    } else {\n      result += '>';\n      if (this.children.some(c => c instanceof Element)) {\n        result += '\\n';\n      }\n      for (const child of this.children) {\n        if (typeof child === 'string') {\n          // For text content of elements, always use CDATA.\n          result += `<![CDATA[${ child }]]>`;\n        } else if (child instanceof Element) {\n          result += child.toXML(indent + 2);\n        } else {\n          throw new TypeError(`Don't know how to serialize ${ child } (type ${ typeof child })`);\n        }\n      }\n      if (this.children.some(c => c instanceof Element)) {\n        result += indentString;\n      }\n      result += `</${ this.tag }>${ '\\n' }`;\n    }\n\n    return result;\n  }\n}\n\n// When rendering, the JSX tag name is supposed to be the name of a component\n// that's passed to the first argument of React.createElement; so we need plain\n// constants for every element we use.\nconst Component = 'Component';\nconst ComponentGroup = 'ComponentGroup';\nconst ComponentGroupRef = 'ComponentGroupRef';\nconst Directory = 'Directory';\nconst File = 'File';\nconst Fragment = 'Fragment';\nconst Shortcut = 'Shortcut';\nconst ShortcutProperty = 'ShortcutProperty';\n\n/**\n * A structure representing the files and subdirectories within a directory.\n */\ninterface directory {\n  /** The identifier for this directory. */\n  id:          string;\n  /** The name of this directory, as the path relative to appDir. */\n  name:        string;\n  /** Child directories. */\n  directories: directory[];\n  /** The regular files within this directory */\n  files:       { name: string, id: string }[];\n}\n\n/** Walk the given directory, determining what files exist. */\nfunction walk(root: string): Promise<directory> {\n  async function walkDirectory(dir: string): Promise<directory> {\n    const relPath = path.relative(root, dir);\n    const files: { name: string, id: string }[] = [];\n    const result: directory = {\n      id:          '', // Will be updated later\n      name:        relPath,\n      directories: [],\n      files:       [],\n    };\n    const hasher = crypto.createHash('sha256');\n    const children = await fs.promises.readdir(dir, { withFileTypes: true });\n\n    hasher.update(relPath);\n    await Promise.all(children.sort((a, b) => a.name.localeCompare(b.name)).map(async(child) => {\n      if (child.isDirectory()) {\n        result.directories.push(await walkDirectory(path.join(dir, child.name)));\n      } else if (child.isFile()) {\n        const info = await fs.promises.stat(path.join(dir, child.name));\n        const input = `${ child.name }::${ info.size }@${ info.mtimeMs }`;\n        const id = `f_${ hasher.copy().update(input).digest('base64url').replaceAll('-', '.') }`;\n\n        files.push({ name: child.name, id });\n      } else {\n        throw new Error(`Could not handle non-regular file ${ path.join(dir, child.name) }`);\n      }\n    }));\n\n    result.directories.sort((a, b) => a.name.localeCompare(b.name));\n\n    files.sort((a, b) => a.name.localeCompare(b.name));\n    result.files = files;\n\n    files.forEach(f => hasher.update(f.id));\n    result.id = `d_${ hasher.digest('base64url').replaceAll('-', '.') }`;\n\n    return result;\n  }\n\n  return walkDirectory(root);\n}\n\n/** Given a directory, return all its descendants as a list. */\nfunction getDescendantDirs(d: directory): directory[] {\n  function getDescendantsIncludingSelf(d: directory): directory[] {\n    return d.directories.map(getDescendantsIncludingSelf).flat().concat(d);\n  }\n\n  return getDescendantsIncludingSelf(d).slice(0, -1);\n}\n\n/**\n * Generate the file listings. The output will be a WiX <Fragment> with the\n * following key identifiers:\n * <Directory Id=\"TARGETDIR\" />\n * <ComponentGroup Id=\"ProductComponents\" />\n * @param rootPath Path of the unpacked application directory.\n */\nexport default async function generateFileList(rootPath: string): Promise<string> {\n  const rootDir = await walk(rootPath);\n\n  // Drop the \"build/\" directory, those are files to build the installer.\n  rootDir.directories = rootDir.directories.filter(d => d.name !== 'build');\n\n  const descendantDirs = getDescendantDirs(rootDir).filter(d => d.files.length > 0);\n\n  const specialComponents: Record<string, (d: directory, f: { name: string, id: string }) => Element | null> = {\n    // @ts-ignore\n    'Rancher Desktop.exe': (d, f) => {\n      return <Component>\n        <File\n          Name={f.name}\n          Source={path.join('$(var.appDir)', f.name)}\n          ReadOnly=\"yes\"\n          KeyPath=\"yes\"\n          Id=\"mainExecutable\">\n          <Shortcut\n            Id=\"desktopShortcut\"\n            Directory=\"DesktopFolder\"\n            Name=\"Rancher Desktop\"\n            WorkingDirectory=\"APPLICATIONFOLDER\"\n            Advertise=\"yes\"\n            Icon=\"RancherDesktopIcon.exe\" />\n          <Shortcut\n            Id=\"startMenuShortcut\"\n            Directory=\"ProgramMenuFolder\"\n            Name=\"Rancher Desktop\"\n            WorkingDirectory=\"APPLICATIONFOLDER\"\n            Advertise=\"yes\"\n            Icon=\"RancherDesktopIcon.exe\">\n            <ShortcutProperty\n              Key=\"System.AppUserModel.ID\"\n              Value=\"io.rancherdesktop.app\" />\n          </Shortcut>\n        </File>\n      </Component>;\n    },\n\n    'electron-builder.yml': () => {\n      // This files does not need to be packaged.\n      return null;\n    },\n\n    'wix-custom-action.dll': () => {\n      // This file does not need to be installed; it's used as an unnamed\n      // binary instead; see main.wxs.\n      return null;\n    },\n  };\n\n  const jsxElement = (<Fragment>\n    <Directory Id=\"TARGETDIR\" Name=\"SourceDir\">\n      <Directory Id=\"ProgramFiles64Folder\">\n        <Directory Id=\"APPLICATIONFOLDER\" Name=\"Rancher Desktop\">\n          {(() => {\n            function emit(d: directory) {\n              return d.directories.map(subdir => <Directory Id={subdir.id} Name={path.basename(subdir.name)}>\n                {emit(subdir)}\n              </Directory>);\n            }\n\n            return emit(rootDir);\n          })()}\n        </Directory>\n      </Directory>\n      {/* Desktop link */}\n      <Directory Id=\"DesktopFolder\" Name=\"Desktop\" />\n      {/* Start menu link */}\n      <Directory Id=\"ProgramMenuFolder\" />\n    </Directory>\n\n    <ComponentGroup Id=\"ProductComponents\" Directory=\"APPLICATIONFOLDER\">\n      {rootDir.files.map((f) => {\n        if (f.name in specialComponents) {\n          return specialComponents[f.name](rootDir, f);\n        }\n\n        return <Component>\n          <File\n            Name={f.name}\n            Source={path.join('$(var.appDir)', f.name)}\n            ReadOnly=\"yes\"\n            KeyPath=\"yes\"\n            Id={f.id}\n          />\n        </Component>;\n      })}\n      {descendantDirs.map(d => <ComponentGroupRef Id={d.id} />)}\n    </ComponentGroup>\n\n    {descendantDirs.map(d => <ComponentGroup Id={d.id} Directory={d.id}>\n      {d.files.map((f) => {\n        const relPath = path.join(d.name, f.name);\n\n        if (relPath in specialComponents) {\n          return specialComponents[relPath](d, f);\n        }\n\n        return <Component>\n          <File\n            Name={f.name}\n            Source={path.join('$(var.appDir)', d.name, f.name)}\n            ReadOnly=\"yes\"\n            KeyPath=\"yes\"\n            Id={f.id}\n          />\n        </Component>;\n      })}\n    </ComponentGroup>,\n    )}\n  </Fragment>);\n\n  // @ts-ignore\n  return jsxElement.toXML();\n}\n"
  },
  {
    "path": "scripts/lib/installer-win32.tsx",
    "content": "/**\n * Windows Installer generation.\n *\n * While Electron-Builder has built-in MSI support, it's not quite as flexible\n * as we desired.  This runs WiX manually instead.\n */\n\n/** @jsx Element.new */\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { extractFile } from '@electron/asar';\nimport Mustache from 'mustache';\nimport yaml from 'yaml';\n\nimport buildUtils from './build-utils';\nimport generateFileList from './installer-win32-gen';\n\nimport { simpleSpawn } from 'scripts/simple_process';\n\n/**\n * Return the contents of package.json embedded in the application.\n */\nfunction getPackageJson(appDir: string): Record<string, any> {\n  const packageBytes = extractFile(path.join(appDir, 'resources', 'app.asar'), 'package.json');\n\n  return JSON.parse(packageBytes.toString('utf-8'));\n}\n\n/**\n * Given an unpacked application directory, return the application version.\n */\nfunction getAppVersion(appDir: string): string {\n  const packageVersion = getPackageJson(appDir).version;\n  // We have a git describe style version, 1.2.3-1234-gabcdef\n  const [, semver, offset] = /^v?(\\d+\\.\\d+\\.\\d+)(?:-(\\d+))?/.exec(packageVersion) ?? [];\n\n  if (!semver) {\n    throw new Error(`Could not parse version string ${ packageVersion }`);\n  }\n\n  return offset ? `${ semver }.${ offset }` : semver;\n}\n\nexport async function buildCustomAction(): Promise<string> {\n  const output = path.join(buildUtils.distDir, 'wix-custom-action.dll');\n\n  await buildUtils.spawn('go', 'build', '-o', output, '-buildmode=c-shared', './wix', {\n    cwd: path.join(buildUtils.rootDir, 'src', 'go', 'wsl-helper'),\n    env: { ...process.env, GOOS: 'windows' },\n  });\n\n  return output;\n}\n\n/**\n * Given an unpacked build, produce a MSI installer.\n * @param workDir Directory in which we can write temporary work files.\n * @param appDir Directory containing extracted application zip file.\n * @param outFile Override for the file name to emit.\n * @returns The path of the built installer.\n */\nexport default async function buildInstaller(workDir: string, appDir: string, outFile = ''): Promise<string> {\n  const appVersion = getAppVersion(appDir);\n\n  outFile ||= path.join(process.cwd(), 'dist', `Rancher.Desktop.Setup.${ appVersion }.msi`);\n\n  await writeUpdateConfig(appDir);\n  const fileList = await generateFileList(appDir);\n  const template = await fs.promises.readFile(path.join(process.cwd(), 'build', 'wix', 'main.wxs'), 'utf-8');\n  const output = Mustache.render(template, { appVersion, fileList });\n  const wixDir = path.join(process.cwd(), 'resources', 'host', 'wix');\n\n  console.log('Writing out WiX definition...');\n  await fs.promises.writeFile(path.join(workDir, 'project.wxs'), output);\n  console.log('Compiling WiX...');\n  const iconPath = path.join(appDir, 'resources', 'resources', 'win32', 'bin', 'rdctl.exe');\n  const inputs = [\n    path.join(workDir, 'project.wxs'),\n    path.join(process.cwd(), 'build', 'wix', 'dialogs.wxs'),\n    path.join(process.cwd(), 'build', 'wix', 'welcome.wxs'),\n    path.join(process.cwd(), 'build', 'wix', 'scope.wxs'),\n    path.join(process.cwd(), 'build', 'wix', 'verify.wxs'),\n  ];\n\n  await Promise.all(inputs.map(input => simpleSpawn(\n    path.join(wixDir, 'candle.exe'),\n    [\n      '-arch', 'x64',\n      `-dappDir=${ appDir }`,\n      `-diconPath=${ iconPath }`, // spellcheck-ignore-line\n      `-dlicenseFile=${ path.join(appDir, 'build', 'license.rtf') }`,\n      '-nologo',\n      '-out', path.join(workDir, `${ path.basename(input, '.wxs') }.wixobj`),\n      '-pedantic',\n      '-wx',\n      '-ext', 'WixFirewallExtension',\n      input,\n    ])));\n  console.log('Linking WiX...');\n  await simpleSpawn(path.join(wixDir, 'light.exe'), [\n    // Skip ICE 60, which checks for files with versions but no language (since\n    // Windows Installer will always need to reinstall the file on a repair, in\n    // case it's the wrong language).  This trips up our icon fonts, which we\n    // do not install system-wide.\n    // https://learn.microsoft.com/en-us/windows/win32/msi/ice60\n    '-sice:ICE60',\n    // Skip ICE 61, which is incompatible AllowSameVersionUpgrades and which emits:\n    // error LGHT1076 : ICE61: This product should remove only older versions of itself.\n    // https://learn.microsoft.com/en-us/windows/win32/msi/ice61\n    '-sice:ICE61',\n    `-dappDir=${ appDir }`,\n    `-dlicenseFile=${ path.join(appDir, 'build', 'license.rtf') }`,\n    `-dWixUIBannerBmp=${ path.join(appDir, 'build', 'wix', 'bannrbmp.png') }`,\n    `-dWixUIDialogBmp=${ path.join(appDir, 'build', 'wix', 'dlgbmp.png') }`,\n    '-ext', 'WixUIExtension',\n    '-ext', 'WixUtilExtension',\n    '-ext', 'WixFirewallExtension',\n    '-nologo',\n    '-out', outFile,\n    '-pedantic',\n    '-wx',\n    '-cc', path.join(process.cwd(), 'dist', 'wix-cache'),\n    '-reusecab',\n    '-loc', path.join(path.join(process.cwd(), 'build', 'wix', 'string-overrides.wxl')),\n    ...inputs.map(n => path.join(workDir, `${ path.basename(n, '.wxs') }.wixobj`)),\n  ], { cwd: appDir });\n  console.log(`Built Windows installer: ${ outFile }`);\n\n  return outFile;\n}\n\n/**\n * writeUpdateConfig writes out app-update.yml, because electron-builder won't\n * create that for us if we're not building the NSIS installer.\n * @param appDir The directory containing the extracted application.\n * @postcondition The file <appDir>\\resources\\app-update.yml exists.\n */\nasync function writeUpdateConfig(appDir: string) {\n  const packageJson = getPackageJson(appDir);\n  const electronBuilderConfig = yaml.parse(await fs.promises.readFile(path.join(appDir, 'electron-builder.yml'), 'utf-8'));\n  const repoURL = new URL(packageJson.repository.url.replace(/\\.git$/, ''));\n\n  if (repoURL.hostname !== 'github.com') {\n    throw new Error(`Unexpect repository reference ${ repoURL }`);\n  }\n\n  const [owner, repo] = repoURL.pathname.split('/').filter(x => x);\n  const result = {\n    owner,\n    repo,\n    updaterCacheDirName: packageJson.name, // AppData\\Local\\rancher-desktop\\update-info.json\n    ...electronBuilderConfig.publish,\n  };\n\n  await fs.promises.writeFile(path.join(appDir, 'resources', 'app-update.yml'), yaml.stringify(result), 'utf-8');\n  console.log('app-update.yml written.');\n}\n"
  },
  {
    "path": "scripts/lib/sign-macos.ts",
    "content": "/**\n * Code signing support for macOS.\n */\n\nimport { createHash } from 'crypto';\nimport fs from 'fs';\nimport path from 'path';\n\nimport { notarize } from '@electron/notarize';\nimport { build, Arch, Configuration, Platform } from 'app-builder-lib';\nimport { MacPackager } from 'app-builder-lib/out/macPackager';\nimport { AsyncTaskManager, log } from 'builder-util';\nimport { Target } from 'electron-builder';\nimport _ from 'lodash';\nimport plist from 'plist';\nimport yaml from 'yaml';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\n\ninterface SigningConfig {\n  entitlements: {\n    default:   string[];\n    overrides: {\n      paths:        string[];\n      entitlements: string[];\n    }[];\n  }\n  constraints: {\n    paths:        string[];\n    self?:        Record<string, any>;\n    parent?:      Record<string, any>;\n    responsible?: Record<string, any>;\n  }[]\n  remove: string[];\n}\n\nexport async function sign(workDir: string): Promise<string[]> {\n  const certFingerprint = process.env.CSC_FINGERPRINT ?? '';\n  const appleId = process.env.APPLEID;\n  const appleIdPassword = process.env.AC_PASSWORD;\n  const teamId = process.env.AC_TEAMID;\n\n  if (certFingerprint.length < 1) {\n    throw new Error(`CSC_FINGERPRINT environment variable not set; required to pick signing certificate.`);\n  }\n\n  const unpackedDir = path.join(workDir, 'unpacked');\n  const appDir = path.join(unpackedDir, 'Rancher Desktop.app');\n  const configPath = path.join(appDir, 'Contents/electron-builder.yml');\n  const configText = await fs.promises.readFile(configPath, 'utf-8');\n  const config: Configuration = yaml.parse(configText);\n  const signingConfigPath = path.join(appDir, 'Contents/build/signing-config-mac.yaml');\n  const signingConfigText = await fs.promises.readFile(signingConfigPath, 'utf-8');\n  const signingConfig: SigningConfig = yaml.parse(signingConfigText, { merge: true });\n  const plistsDir = path.join(workDir, 'plists');\n  let wroteDefaultEntitlements = false;\n  let constraintSkipped = false;\n\n  log.info('Removing excess files...');\n  await Promise.all(signingConfig.remove.map(async(relpath) => {\n    await fs.promises.rm(path.join(appDir, relpath), { recursive: true });\n  }));\n\n  log.info('Signing application...');\n  // We're not using @electron/osx-sign because it doesn't allow --launch-constraint-*\n  await fs.promises.mkdir(plistsDir, { recursive: true });\n  for await (const filePath of findFilesToSign(appDir)) {\n    const relPath = path.relative(appDir, filePath);\n    const fileHash = createHash('sha256').update(relPath, 'utf-8').digest('base64url');\n    const args = ['--sign', certFingerprint, '--force', '--timestamp', '--options', 'runtime'];\n\n    // Determine the entitlements\n    const entitlementsOverride = signingConfig.entitlements.overrides.find(e => e.paths.includes(relPath));\n    let entitlementName = 'default';\n    let entitlements = signingConfig.entitlements.default;\n\n    if (entitlementsOverride) {\n      entitlementName = fileHash;\n      entitlements = entitlementsOverride.entitlements;\n    }\n    const entitlementFile = path.join(plistsDir, `${ entitlementName }-entitlement.plist`);\n\n    if (!wroteDefaultEntitlements || entitlementName !== 'default') {\n      await fs.promises.writeFile(entitlementFile,\n        plist.build(Object.fromEntries(entitlements.map(k => [k, true]))));\n      wroteDefaultEntitlements ||= entitlementName === 'default';\n    }\n    args.push('--entitlements', entitlementFile);\n\n    // Determine the launch constraints\n    if (process.argv.includes('--skip-constraints')) {\n      if (!constraintSkipped) {\n        log.warn('Skipping --launch-constraint-...: --skip-constraints given.');\n        constraintSkipped = true;\n      }\n    } else {\n      const launchConstraints = signingConfig.constraints.find(c => c.paths.includes(relPath));\n      const constraintTypes = ['self', 'parent', 'responsible'] as const;\n\n      for (const constraintType of constraintTypes) {\n        const constraint = launchConstraints?.[constraintType];\n\n        if (constraint) {\n          const constraintsFile = path.join(plistsDir, `${ fileHash }-constraint-${ constraintType }.plist`);\n\n          await fs.promises.writeFile(constraintsFile, plist.build(evaluateConstraints(constraint)));\n          args.push(`--launch-constraint-${ constraintType }`, constraintsFile);\n        }\n      }\n    }\n\n    await spawnFile('codesign', [...args, filePath], { stdio: 'inherit' });\n  }\n\n  log.info('Verifying application signature...');\n  await spawnFile('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appDir], { stdio: 'inherit' });\n  await spawnFile('codesign', ['--display', '--entitlements', '-', appDir], { stdio: 'inherit' });\n\n  if (process.argv.includes('--skip-notarize')) {\n    log.warn('Skipping notarization: --skip-notarize given.');\n  } else if (appleId && appleIdPassword && teamId) {\n    log.info('Notarizing application...');\n    await notarize({\n      appPath: appDir,\n      appleId,\n      appleIdPassword,\n      teamId,\n    });\n  } else {\n    const message = [\n      'APPLEID, AC_PASSWORD, or AC_TEAMID environment variables not given, cannot notarize.',\n      'To force skip notarization, please pass --skip-notarize to signing script.',\n    ];\n\n    throw new Error(message.join('\\n'));\n  }\n\n  log.info('Building disk image and update archive...');\n  const arch = process.env.M1 ? Arch.arm64 : Arch.x64;\n  const productFileName = config.productName?.replace(/\\s+/g, '.');\n  const productArch = process.env.M1 ? 'aarch64' : 'x86_64';\n  const artifactName = `${ productFileName }-\\${version}-mac.${ productArch }.\\${ext}`;\n  const formats = ['dmg', 'zip'];\n\n  // Build the dmg, explicitly _not_ using an identity; we just signed\n  // everything as we wanted already.\n  const results = await build({\n    publish: 'never',\n    targets: new Map([[Platform.MAC, new Map([[arch, formats]])]]),\n    config:  _.merge<Configuration, Configuration>(config,\n      {\n        dmg: { writeUpdateInfo: false },\n        mac: { artifactName, identity: null },\n      }),\n    prepackaged:             appDir,\n    // Provide a custom packager factory so that we can override the packager\n    // to skip generating blockmap files.  Generating the blockmap hangs on CI\n    // for some reason.\n    platformPackagerFactory: (info) => {\n      return new CustomPackager(info);\n    },\n  });\n\n  // The .dmg and the .zip have slightly different file names, so we need to\n  // deal with them separately.\n\n  const dmgFile = results.find(f => f.endsWith('.dmg'));\n  const zipFile = results.find(f => f.endsWith('.zip'));\n\n  if (!dmgFile) {\n    throw new Error(`Could not find build disk image`);\n  }\n  if (!zipFile) {\n    throw new Error(`Could not find build zip file`);\n  }\n\n  const dmgRenamedFile = dmgFile.replace('-mac.', '.');\n\n  await fs.promises.rename(dmgFile, dmgRenamedFile);\n  await Promise.all([dmgRenamedFile, zipFile].map((f) => {\n    return spawnFile('codesign', ['--sign', certFingerprint, '--timestamp', f], { stdio: 'inherit' });\n  }));\n\n  return Object.values([dmgRenamedFile, zipFile]);\n}\n\n/**\n * Recursively walk the given directory and locate files to sign.\n */\nasync function * findFilesToSign(dir: string): AsyncIterable<string> {\n  // When doing code signing, the children must be signed before their parents\n  // (so that their signatures can be incorporated into the parent signature,\n  // Merkle tree style).\n  // Also, for \"Foo.app\", we can skip \"Foo.app/Contents/MacOS/Foo\" because the\n  // act of signing the app bundle will sign the executable.\n  for (const file of await fs.promises.readdir(dir, { withFileTypes: true })) {\n    const fullPath = path.resolve(dir, file.name);\n\n    if (file.isSymbolicLink()) {\n      // Skip all symlinks; we sign the symlink target instead.\n      continue;\n    }\n    if (file.isDirectory()) {\n      yield * findFilesToSign(fullPath);\n    }\n    if (!file.isFile()) {\n      continue; // We only sign regular files.\n    }\n\n    if (await isBundleExecutable(fullPath)) {\n      // For bundles (apps and frameworks), we skip signing the executable\n      // itself as it will be signed when signing the bundle.\n      continue;\n    }\n\n    // For regular files, call `file` and check if it thinks it's Mach-O.\n    // We previously read the file header, but that was unreliable.\n    try {\n      const { stdout } = await spawnFile('/usr/bin/file', ['--brief', fullPath], { stdio: 'pipe' });\n\n      if (!stdout.startsWith('Mach-O ')) {\n        continue;\n      }\n    } catch {\n      log.info({ fullPath }, 'Failed to read file, assuming no need to sign.');\n      continue;\n    }\n\n    // If the file is already signed, don't sign it again.\n    try {\n      await spawnFile('codesign', ['--verify', '--strict=all', '--test-requirement=anchor apple', fullPath]);\n      log.info({ fullPath }, 'Skipping signing of already-signed directory');\n    } catch {\n      yield fullPath;\n    }\n  }\n\n  if (dir.endsWith('.app') || dir.endsWith('.framework')) {\n    // We need to sign app bundles, if they haven't been signed yet.\n    try {\n      await spawnFile('codesign', ['--verify', '--strict=all', '--test-requirement=anchor apple', dir]);\n      log.info({ dir }, 'Skipping signing of already-signed directory');\n    } catch {\n      yield dir;\n    }\n  }\n}\n\n/**\n * Detect if the path of a plain file indicates that it's the bundle executable\n */\nasync function isBundleExecutable(fullPath: string): Promise<boolean> {\n  const parts = fullPath.split(path.sep).reverse();\n\n  if (parts.length >= 4) {\n    // Anything.app/Contents/MacOS/executable - the check style here avoids spell checker.\n    if (fullPath.endsWith(`.app/Contents/MacOS/${ parts[0] }`)) {\n      // Check Anything.app/Contents/Info.plist for CFBundleExecutable\n      const infoPlist = path.sep + path.join(...parts.slice(2).reverse(), 'Info.plist');\n\n      try {\n        const executableKey = 'CFBundleExecutable';\n        const plistContents = await fs.promises.readFile(infoPlist, 'utf-8');\n        const value = plist.parse(plistContents);\n\n        if (typeof value !== 'object' || !(executableKey in value)) {\n          return false;\n        }\n\n        return value[executableKey] === parts[0];\n      } catch (ex) {\n        log.info({ ex, infoPlist }, 'Failed to read Info.plist, assuming not the bundle executable.');\n\n        return false;\n      }\n    }\n  }\n\n  if (parts.length >= 4) {\n    // Foo.framework/Versions/A/Foo\n    if (parts[3] === `${ parts[0] }.framework` && parts[2] === 'Versions') {\n      return true;\n    }\n  }\n\n  return false;\n}\n\n/**\n * Given a launch constraint, preprocess it to return values from the environment.\n */\nfunction evaluateConstraints(constraint: Record<string, any>): Record<string, any> {\n  return _.mapValues(constraint, (value) => {\n    switch (typeof value) {\n    case 'string':\n      break;\n    case 'object':\n      if (Array.isArray(value)) {\n        return value.map(v => evaluateConstraints(v));\n      } else {\n        return evaluateConstraints(value);\n      }\n    default:\n      return value;\n    }\n    switch (value) {\n    case '${AC_TEAMID}': // eslint-disable-line no-template-curly-in-string\n      return process.env.AC_TEAMID || value;\n    default:\n      return value;\n    }\n  });\n}\n\n/**\n * CustomPackager overrides MacPackager to avoid building blockmap files\n */\nclass CustomPackager extends MacPackager {\n  override pack(outDir: string, arch: Arch, targets: Target[], taskManager: AsyncTaskManager): Promise<any> {\n    for (const target of targets) {\n      if ('isWriteUpdateInfo' in target) {\n        (target as any).isWriteUpdateInfo = false;\n      }\n    }\n\n    return super.pack.call(this, outDir, arch, targets, taskManager);\n  }\n}\n"
  },
  {
    "path": "scripts/lib/sign-win32.ts",
    "content": "/**\n * Code signing support for Windows.\n */\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { getSignToolPath } from 'app-builder-lib/out/toolsets/windows';\nimport defaults from 'lodash/defaultsDeep';\nimport merge from 'lodash/merge';\nimport yaml from 'yaml';\n\nimport { simpleSpawn } from 'scripts/simple_process';\n\n/** signFileFn is a function that signs a single file. */\ntype signFileFn = (...filePath: string[]) => Promise<void>;\n\n/** verifyFileFn checks if a given file has been signed already. */\ntype verifyFileFn = (filePath: string) => Promise<boolean>;\n\n/**\n * Mandatory configuration for Windows.\n *\n * These values are hard-coded and will always be passed to electron-builder\n * when signing the installer.\n */\nconst REQUIRED_WINDOWS_CONFIG = {\n  signtoolOptions: { signingHashAlgorithms: ['sha256'] },\n  target:          'zip',\n};\n\n/**\n * Default values for optional configuration for Windows.\n *\n * These are defaults that may be overridden (in electron-builder.yml).\n */\nconst DEFAULT_WINDOWS_CONFIG = {\n  certificateSha1:        '', // set via CSC_FINGERPRINT\n  rfc3161TimeStampServer: 'http://timestamp.digicert.com',\n};\n\ninterface ElectronBuilderConfiguration {\n  productName:   string;\n  files?:        string[];\n  win?:          Partial<typeof DEFAULT_WINDOWS_CONFIG & typeof REQUIRED_WINDOWS_CONFIG>;\n  extraMetadata: {\n    version: string;\n  }\n}\n\nexport async function sign(workDir: string): Promise<string[]> {\n  const certFingerprint = process.env.CSC_FINGERPRINT ?? '';\n  const certPassword = process.env.CSC_KEY_PASSWORD ?? '';\n\n  if (certFingerprint.length < 1) {\n    throw new Error(`CSC_FINGERPRINT environment variable not set; required to pick signing certificate.`);\n  }\n\n  // Sign individual files.  See https://github.com/electron-userland/electron-builder/issues/5968\n  // We built this docker.exe, so we need to sign it\n\n  const unpackedDir = path.join(workDir, 'unpacked');\n  const configPath = path.join(unpackedDir, 'electron-builder.yml');\n  const configText = await fs.promises.readFile(configPath, 'utf-8');\n  const config = yaml.parse(configText) as ElectronBuilderConfiguration;\n  const signingConfigPath = path.join(unpackedDir, 'build', 'signing-config-win.yaml');\n  const signingConfigText = await fs.promises.readFile(signingConfigPath, 'utf-8');\n  const signingConfig: Record<string, string[]> = yaml.parse(signingConfigText);\n  const versionedAppName = `${ config.productName } ${ config.extraMetadata.version }`;\n\n  config.win ??= {};\n  defaults(config.win, DEFAULT_WINDOWS_CONFIG);\n  merge(config.win, REQUIRED_WINDOWS_CONFIG);\n  config.win.certificateSha1 = certFingerprint;\n\n  const { path: toolPath } = await getSignToolPath(null, true);\n  const toolArgs = [\n    'sign',\n    '/debug',\n    '/sha1', certFingerprint,\n    '/fd', 'SHA256',\n    '/td', 'SHA256',\n    '/tr', config.win.rfc3161TimeStampServer!,\n    '/du', 'https://rancherdesktop.io',\n    '/d', versionedAppName,\n  ];\n\n  if (certPassword.length > 0) {\n    toolArgs.push('/p', certPassword);\n  }\n\n  const signFn: signFileFn = async(...fullPath) => {\n    await simpleSpawn(toolPath, [...toolArgs, ...fullPath]);\n  };\n  const verifyFn: verifyFileFn = async(fullPath) => {\n    try {\n      await simpleSpawn(toolPath, ['verify', '/pa', fullPath], { stdio: 'ignore' });\n\n      return true;\n    } catch {\n      return false;\n    }\n  };\n\n  const filesToSign = new Set<string>();\n\n  for await (const fullPath of findFilesToSign(unpackedDir, signingConfig, verifyFn)) {\n    // Fail if a whitelisted file doesn't exist\n    await fs.promises.access(fullPath);\n    filesToSign.add(fullPath);\n  }\n\n  await signFn(...filesToSign);\n\n  return [await buildWiX(workDir, unpackedDir, signFn)];\n}\n\n/**\n * Find all the files that should be signed.\n * @param unpackedDir The directory holding the unpacked zip file.\n * @param signingConfig The signing config from electron-builder.yaml\n */\nasync function * findFilesToSign(\n  unpackedDir: string,\n  signingConfig: Record<string, string[]>,\n  verifyFn: verifyFileFn = () => Promise.resolve(true),\n): AsyncIterable<string> {\n  /** toSign is the set of files that we want to sign. */\n  const toSign = new Set<string>();\n  /** toSkip is the set of files we are explicitly skipping signing. */\n  const toSkip = new Set<string>();\n  /** unexpectedFiles is the set of files we found that are not known. */\n  const unexpectedFiles = new Set<string>();\n\n  for (const [dir, files] of Object.entries(signingConfig)) {\n    for (const file of files) {\n      if (file.startsWith('!')) {\n        toSkip.add(path.normalize(path.join(unpackedDir, dir, file.slice(1))));\n      } else {\n        toSign.add(path.normalize(path.join(unpackedDir, dir, file)));\n      }\n    }\n  }\n\n  for await (const childPath of findFiles(unpackedDir)) {\n    if (!['.exe', '.dll', '.ps1'].includes(path.extname(childPath))) {\n      continue;\n    }\n    const relPath = path.relative(unpackedDir, childPath);\n    const signed = await verifyFn(childPath);\n\n    if (toSign.has(childPath)) {\n      if (signed) {\n        console.log(`Warning: ${ relPath } already signed.`);\n      }\n      yield childPath;\n    } else if (toSkip.has(childPath)) {\n      if (signed) {\n        console.log(`Unneeded exclusion of already-signed file ${ relPath }`);\n      }\n    } else {\n      if (signed) {\n        console.log(`Skipping already signed file ${ relPath }`);\n      } else {\n        unexpectedFiles.add(relPath);\n      }\n    }\n  }\n\n  if (unexpectedFiles.size > 0) {\n    const message = [\n      'Found unknown executable files:',\n      ...Array.from(unexpectedFiles).map(f => ` - ${ f }`).sort(),\n      'Please edit build/signing-config-win.yaml to add those files.',\n    ];\n\n    throw new Error(message.join('\\n'));\n  }\n}\n\n/**\n * Recursively yield all plain files in the given directory.\n */\nasync function * findFiles(dir: string): AsyncIterable<string> {\n  for (const child of await fs.promises.readdir(dir, { withFileTypes: true })) {\n    if (child.isDirectory()) {\n      yield * findFiles(path.join(dir, child.name));\n    } else if (child.isFile()) {\n      yield path.normalize(path.join(dir, child.name));\n    }\n  }\n}\n\nasync function buildWiX(workDir: string, unpackedDir: string, signFn: signFileFn): Promise<string> {\n  const buildInstaller = (await import('./installer-win32')).default;\n  const installerPath = await buildInstaller(workDir, unpackedDir);\n\n  await signFn(installerPath);\n\n  return installerPath;\n}\n"
  },
  {
    "path": "scripts/lint-go.ts",
    "content": "/**\n * This script handles linting for go-related files.\n *\n * If any argument is `--fix`, then changes are automatically applied.\n */\nimport fs from 'fs';\nimport path from 'path';\n\nimport { glob } from 'glob';\nimport yaml from 'yaml';\n\nimport { readDependencyVersions } from './lib/dependencies';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\n\ntype SupportedPlatform = Extract<NodeJS.Platform, 'darwin' | 'linux' | 'win32'>;\n\nconst fix = process.argv.includes('--fix');\n\nasync function listFiles(...globs: string[]): Promise<string[]> {\n  const { stdout } = await spawnFile('git', ['ls-files', ...globs], { stdio: 'pipe' });\n\n  return stdout.split(/\\r?\\n/).filter(x => x);\n}\n\nasync function getModules(): Promise<string[]> {\n  return (await listFiles('**/go.mod')).map(mod => path.dirname(mod));\n}\n\n/**\n * Modules whose version depends on a different module.  The top level key is\n * the directory containing `go.mod`, relative to the top of the source tree;\n * for example, `src/go/wsl-helper`.  The second level is the go module to\n * modify; the value is the go module in the same `go.mod` to refer to.\n */\nconst linkedModules: Record<string, Record<string, string>> = {\n  'src/go/wsl-helper': {\n    'github.com/go-openapi/swag': 'github.com/go-swagger/go-swagger',\n  },\n};\n\n/**\n * The subset of `go mod edit -json` output that we care about.\n */\ninterface GoModule {\n  Require: {\n    Path:      string;\n    Version:   string;\n    Indirect?: boolean;\n  }[];\n};\n\n/**\n * Tagged template function for use in error strings, highlighting all the expressions.\n */\nfunction error(input: TemplateStringsArray, ...args: any[]): string {\n  const parts = input.map((s, i) => `${ s }\\x1B[1;33;40m${ args[i] ?? '' }\\x1B[0m`);\n  return `\\x1B[0;1;31mERROR\\x1B[0m ${ parts.join('') }`;\n}\n\nasync function processLinkedModules(dir: string, fix: boolean): Promise<boolean> {\n  let noErrors = true;\n  const moduleMap = linkedModules[dir];\n\n  if (!moduleMap) {\n    // We do not have overrides for this directory.\n    return true;\n  }\n\n  /** Run `go` with the given arguments, returning standard output. */\n  async function go(...args: string[]): Promise<string> {\n    console.log(['go', ...args].join(' '));\n    const { stdout } = await spawnFile('go', args, { cwd: dir, stdio: ['ignore', 'pipe', 'inherit'] });\n\n    return stdout;\n  }\n\n  const modules: GoModule = JSON.parse(await go('mod', 'edit', '-json'));\n  const requires = Object.fromEntries(modules.Require.map(r => [r.Path, r]));\n\n  for (const [target, source] of Object.entries(moduleMap)) {\n    if (!(target in requires)) {\n      console.error(error`${ dir }: failed to find linked module ${ target }`);\n      noErrors = false;\n    }\n    if (!(source in requires)) {\n      console.error(error`${ dir }: linked module ${ target } has missing source ${ source }`);\n      noErrors = false;\n    }\n    if (!noErrors) {\n      continue;\n    }\n\n    const currentVersion = requires[target].Version;\n    const sourcePath = (await go('list', '-m', '-f', '{{ .GoMod }}', source)).trim();\n    const sourceModules: GoModule = await JSON.parse(await go('mod', 'edit', '-json', sourcePath));\n    const sourceRequires = Object.fromEntries(sourceModules.Require.map(r => [r.Path, r]));\n\n    if (target in sourceRequires) {\n      const wantedVersion = sourceRequires[target].Version;\n\n      if (currentVersion !== wantedVersion) {\n        if (fix) {\n          await go('get', `${ target }@${ wantedVersion }`);\n        } else {\n          console.error(error`${ dir }: linked module ${ target } has version ${ currentVersion }, should be ${ wantedVersion }`);\n          noErrors = false;\n        }\n      }\n    } else {\n      console.error(error`${ dir }: linked module ${ target } has source ${ source } but that does not require it`);\n      noErrors = false;\n    }\n  }\n\n  return noErrors;\n}\n\nasync function syncModules(fix: boolean): Promise<boolean> {\n  const modFiles = await listFiles('**/go.mod');\n  const files = ['go.work', ...modFiles, ...await listFiles('**/go.sum')];\n  const getChanges = async() => {\n    const { stdout } = await spawnFile('git', ['status', '--porcelain=1', '--', ...files], { stdio: 'pipe' });\n\n    return stdout.replace(/^\\s+/, '').replace(/\\s+$/, '');\n  };\n\n  if (!fix) {\n    const changes = await getChanges();\n\n    if (changes) {\n      console.log('Cannot run lint without fix with local changes');\n      console.log(changes);\n\n      return false;\n    }\n  }\n\n  const linkedModulesOk = await Promise.all(modFiles.map(f => processLinkedModules(path.dirname(f), fix)));\n  if (linkedModulesOk.some(v => !v)) {\n    return false;\n  }\n\n  await Promise.all((await getModules()).map(cwd => spawnFile('go', ['mod', 'tidy'], { stdio: 'inherit', cwd, env: { ...process.env, GOWORK: 'off' } })));\n  if (!fix) {\n    const changes = await getChanges();\n\n    if (changes) {\n      const { stdout } = await spawnFile('git', ['diff', '--', ...files], { stdio: 'pipe' });\n\n      console.log('Had to make modifications');\n      console.log(changes);\n      console.log(stdout);\n\n      return false;\n    }\n  }\n\n  return true;\n}\n\n// Run golangci-lint with the given arguments for the given OS, and return\n// whether the command succeeded.\nasync function runGoLangCILint(platform: SupportedPlatform, ...args: string[]): Promise<boolean> {\n  const depVersionsPath = path.join('pkg', 'rancher-desktop', 'assets', 'dependencies.yaml');\n  const dependencyVersions = await readDependencyVersions(depVersionsPath);\n  const commandLine = ['go', 'run'];\n\n  if (process.platform !== platform) {\n    // We are emulating a different platform.\n    const os = ({\n      darwin: 'darwin',\n      linux:  'linux',\n      win32:  'windows',\n    } as const)[platform];\n\n    commandLine.push('-exec', `/usr/bin/env GOOS=${ os }`);\n  }\n  commandLine.push(\n    `github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v${ dependencyVersions['golangci-lint'] }`,\n    ...args,\n    ...(await getModules()).map(m => `${ m }/...`));\n\n  try {\n    console.log(commandLine.join(' '));\n    await spawnFile(commandLine[0], commandLine.slice(1), { stdio: 'inherit' });\n\n    return true;\n  } catch (ex) {\n    return false;\n  }\n}\n\nfunction getGoLangCISupportedPlatforms(): SupportedPlatform[] {\n  // On Windows, we can't pretend to be other platforms (due to a lack of\n  // /usr/bin/env).  Also don't do that in CI, because we run all platforms\n  // natively.\n  if (!process.env.CI && process.platform !== 'win32') {\n    return ['darwin', 'linux', 'win32'];\n  }\n\n  return [process.platform] as SupportedPlatform[];\n}\n\nfunction goLangCIFormat(fix: boolean): Promise<boolean> {\n  const args = ['fmt', '--verbose'];\n\n  if (!fix) {\n    // When not fixing, provide `--diff`; this causes the process to exit with\n    // and error when a fix is required.\n    args.push('--diff');\n  }\n\n  // We don't need to run fmt for all platforms, since it seems to format files\n  // whether they would be built.\n  return runGoLangCILint(process.platform as SupportedPlatform, ...args);\n}\n\nasync function goLangCILint(fix: boolean): Promise<boolean> {\n  const args = ['run', '--timeout=10m', '--allow-serial-runners', '--verbose'];\n\n  if (fix) {\n    args.push('--fix');\n  }\n\n  for (const platform of getGoLangCISupportedPlatforms()) {\n    if (!(await runGoLangCILint(platform, ...args))) {\n      return false;\n    }\n  }\n\n  return true;\n}\n\ninterface dependabotConfig {\n  version: 2,\n  updates: {\n    'package-ecosystem':        string;\n    directory:                  string;\n    directories:                string[];\n    schedule:                   { interval: 'daily' };\n    'open-pull-requests-limit': number;\n    labels:                     string[];\n    ignore?:                    { 'dependency-name': string; 'update-types'?: string[]; version?: string[] }[];\n    reviewers?:                 string[];\n  }[];\n}\n\n// Run lint and format in series, for better output.\nasync function lintAndFormat(fix: boolean): Promise<boolean> {\n  return await goLangCIFormat(fix) && await goLangCILint(fix);\n}\n\nasync function checkDependabot(fix: boolean): Promise<boolean> {\n  const configs: dependabotConfig = yaml.parse(await fs.promises.readFile('.github/dependabot.yml', 'utf8'));\n  const modules = await getModules();\n  const dependabotDirs = configs.updates.filter(x => x['package-ecosystem'] === 'gomod').flatMap(x => x.directories || x.directory);\n  const globInputs = dependabotDirs.map(d => `${ d.replace(/^\\//, '') }/go.mod`);\n  const globOutputs = await glob(globInputs);\n  const dependabotModules = globOutputs.map(f => path.dirname(f.replaceAll(path.sep, '/')));\n  const missing = modules.filter(x => !dependabotModules.includes(x));\n\n  if (missing.length > 0) {\n    const message = ['\\x1B[0;1;31m Go modules not listed in dependabot:\\x1B[0m'].concat(missing);\n\n    console.error(message.join('\\n   '));\n\n    return false;\n  }\n\n  return true;\n}\n\nPromise.all([syncModules, lintAndFormat, checkDependabot].map(fn => fn(fix))).then((successes) => {\n  if (!successes.every(x => x)) {\n    process.exit(1);\n  }\n}).catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/lint-typescript.ts",
    "content": "/**\n * This script launches ESLint.\n * This is only needed because cross-env does not portably support setting\n * environment variables with spaces in the value.\n */\n\nimport { simpleSpawn } from './simple_process';\n\nprocess.env.BROWSERSLIST_IGNORE_OLD_DATA = '1';\n\nconst command = [\n  process.execPath,\n  ...process.execArgv,\n  '--max_old_space_size=8192',\n  '--experimental-strip-types',\n  'node_modules/eslint/bin/eslint.js',\n  '--flag', 'unstable_native_nodejs_ts_config',\n  '--report-unused-disable-directives',\n  '--max-warnings=0',\n  ...process.argv.slice(2),\n];\n\nconsole.log(command.join(' '));\nsimpleSpawn(command[0], command.slice(1)).catch(e => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/package.ts",
    "content": "/**\n * This script builds the distributable packages. It assumes that we have _just_\n * built the JavaScript parts.\n */\n\n'use strict';\n\nimport childProcess from 'child_process';\nimport fs from 'fs';\nimport * as path from 'path';\n\nimport { flipFuses, FuseV1Options, FuseVersion } from '@electron/fuses';\nimport { executeAppBuilder, log } from 'builder-util';\nimport {\n  AfterPackContext, Arch, build, CliOptions, Configuration, LinuxTargetSpecificOptions,\n} from 'electron-builder';\nimport _ from 'lodash';\nimport plist from 'plist';\nimport semver from 'semver';\nimport yaml from 'yaml';\n\nimport buildUtils from './lib/build-utils';\nimport buildInstaller, { buildCustomAction } from './lib/installer-win32';\n\nimport { spawnFile } from '@pkg/utils/childProcess';\nimport { ReadWrite } from '@pkg/utils/typeUtils';\n\nclass Builder {\n  private static readonly DEFAULT_VERSION = '0.0.0';\n\n  async replaceInFile(srcFile: string, pattern: string | RegExp, replacement: string, dstFile?: string) {\n    dstFile = dstFile || srcFile;\n    await fs.promises.stat(srcFile);\n    const data = await fs.promises.readFile(srcFile, 'utf8');\n\n    await fs.promises.writeFile(dstFile, data.replace(pattern, replacement));\n  }\n\n  protected get electronBinary() {\n    const platformPath = {\n      darwin: [`mac-${ buildUtils.arch }`, 'Rancher Desktop.app/Contents/MacOS/Rancher Desktop'],\n      win32:  ['win-unpacked', 'Rancher Desktop.exe'],\n    }[process.platform as string];\n\n    if (!platformPath) {\n      throw new Error('Failed to find platform-specific Electron binary');\n    }\n\n    return path.join(buildUtils.distDir, ...platformPath);\n  }\n\n  /**\n   * Flip the Electron fuses so that the app can't be used as a node runtime.\n   * @see https://www.electronjs.org/docs/latest/tutorial/fuses\n   */\n  protected async flipFuses(context: AfterPackContext) {\n    const extension = {\n      darwin: '.app',\n      win32:  '.exe',\n    }[context.electronPlatformName] ?? '';\n    const exeName = `${ context.packager.appInfo.productFilename }${ extension }`;\n    const exePath = path.join(context.appOutDir, exeName);\n    const resetAdHocDarwinSignature = context.arch === Arch.arm64;\n    const integrityEnabled = context.electronPlatformName === 'darwin';\n\n    await flipFuses(\n      exePath,\n      {\n        version:                                               FuseVersion.V1,\n        resetAdHocDarwinSignature,\n        [FuseV1Options.RunAsNode]:                             false,\n        [FuseV1Options.EnableCookieEncryption]:                false,\n        [FuseV1Options.EnableNodeOptionsEnvironmentVariable]:  false,\n        [FuseV1Options.EnableNodeCliInspectArguments]:         false,\n        [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: integrityEnabled,\n        [FuseV1Options.OnlyLoadAppFromAsar]:                   true,\n      },\n    );\n  }\n\n  /**\n   * Manually write out the Linux .desktop application shortcut definition; this\n   * is needed as by default this only happens for snap/fpm/etc., but not zip\n   * files.\n   */\n  protected async writeLinuxDesktopFile(context: AfterPackContext) {\n    const { LinuxPackager } = await import('app-builder-lib/out/linuxPackager');\n    const { LinuxTargetHelper } = await import('app-builder-lib/out/targets/LinuxTargetHelper');\n    const config = context.packager.config.linux;\n\n    if (!(context.packager instanceof LinuxPackager) || !config) {\n      return;\n    }\n\n    const options: LinuxTargetSpecificOptions = {\n      ...context.packager.platformSpecificBuildOptions,\n      compression: undefined,\n    };\n    const helper = new LinuxTargetHelper(context.packager);\n    const leaf = `${ context.packager.executableName }.desktop`;\n    const destination = path.join(context.appOutDir, `resources/resources/linux/${ leaf }`);\n\n    await helper.writeDesktopEntry(options, context.packager.executableName, destination);\n  }\n\n  /**\n   * Edit the application's `Info.plist` file to remove the UsageDescription\n   * keys; there is no reason for the application to get any of those permissions.\n   */\n  protected async removeMacUsageDescriptions(context: AfterPackContext) {\n    const { MacPackager } = await import('app-builder-lib/out/macPackager');\n    const { packager } = context;\n    const config = packager.config.mac;\n\n    if (!(packager instanceof MacPackager) || !config) {\n      return;\n    }\n\n    const { productFilename } = packager.appInfo;\n    const appPath = path.join(context.appOutDir, `${ productFilename }.app`);\n    const plistPath = path.join(appPath, 'Contents', 'Info.plist');\n    const plistContents = await fs.promises.readFile(plistPath, 'utf-8');\n    const plistData = plist.parse(plistContents);\n\n    if (typeof plistData !== 'object' || !('CFBundleName' in plistData)) {\n      return;\n    }\n    const plistCopy: Record<string, plist.PlistValue> = structuredClone(plistData);\n\n    for (const key in plistData) {\n      if (/^NS.*UsageDescription$/.test(key)) {\n        delete plistCopy[key];\n      }\n    }\n    await fs.promises.writeFile(plistPath, plist.build(plistCopy), 'utf-8');\n\n    // Because we modified the Info.plist, we need to re-sign the app.  This is\n    // just using ad-hoc signing.  Note that this will fail on x86_64, so ignore\n    // it there.\n    if (context.arch !== Arch.x64) {\n      await spawnFile('codesign', ['--sign', '-', '--force', '--verbose', appPath], { stdio: 'inherit' });\n    }\n  }\n\n  protected async afterPack(context: AfterPackContext) {\n    await this.flipFuses(context);\n    await this.writeLinuxDesktopFile(context);\n    await this.removeMacUsageDescriptions(context);\n  }\n\n  async package(): Promise<CliOptions> {\n    log.info('Packaging...');\n\n    // Build the electron builder configuration to include the version data\n    const config: ReadWrite<Configuration> = yaml.parse(await fs.promises.readFile('packaging/electron-builder.yml', 'utf-8'));\n    const configPath = path.join(buildUtils.distDir, 'electron-builder.yaml');\n    const fallbackVersion = buildUtils.packageMeta.version ?? Builder.DEFAULT_VERSION;\n    const fallbackSuffix = '-fallback';\n    let fullBuildVersion: string;\n    const fallbackTaggedVersion = semver.valid(`${ fallbackVersion }${ fallbackSuffix }`) ?? Builder.DEFAULT_VERSION;\n    try {\n      fullBuildVersion = semver.valid(childProcess.execFileSync('git', ['describe', '--tags']).toString()) ?? fallbackTaggedVersion;\n    } catch {\n      fullBuildVersion = fallbackTaggedVersion;\n    }\n    const finalBuildVersion = fullBuildVersion.replace(/^v/, '');\n    const distDir = path.join(process.cwd(), 'dist');\n    const electronPlatform = ({\n      darwin: 'mac',\n      win32:  'win',\n      linux:  'linux',\n    } as const)[process.platform as string];\n\n    if (!electronPlatform) {\n      throw new Error(`Packaging for ${ process.platform } is not supported`);\n    }\n\n    switch (electronPlatform) {\n    case 'linux':\n      await this.createLinuxResources(finalBuildVersion);\n      break;\n    case 'win':\n      await this.createWindowsResources(distDir);\n      break;\n    }\n\n    // When there are files (e.g., extraFiles or extraResources) specified at both\n    // the top-level and platform-specific levels, we need to combine them\n    // and place the combined list at the top level. This approach enables us to have\n    // platform-specific exclusions, since the two lists are initially processed\n    // separately and then merged together afterward.\n    for (const key of ['files', 'extraFiles', 'extraResources'] as const) {\n      const section = config[electronPlatform];\n      const items = config[key];\n      const overrideItems = section?.[key];\n\n      if (!section || !Array.isArray(items) || !Array.isArray(overrideItems)) {\n        continue;\n      }\n      config[key] = items.concat(overrideItems);\n      delete section[key];\n    }\n\n    _.set(config, 'extraMetadata.version', finalBuildVersion);\n    await fs.promises.writeFile(configPath, yaml.stringify(config), 'utf-8');\n\n    config.afterPack = this.afterPack.bind(this);\n\n    const options: CliOptions = {\n      config,\n      publish: 'never',\n      arm64:   buildUtils.arch === 'arm64',\n      x64:     buildUtils.arch === 'x64',\n    };\n\n    if (electronPlatform) {\n      if (process.argv.includes('--zip')) {\n        options[electronPlatform] = ['zip'];\n      } else {\n        const rawTarget = config[electronPlatform]?.target ?? [];\n        const target = Array.isArray(rawTarget) ? rawTarget : [rawTarget];\n\n        options[electronPlatform] = target.map(t => typeof t === 'string' ? t : t.target);\n      }\n    }\n\n    await build(options);\n\n    return options;\n  }\n\n  async buildInstaller(config: CliOptions) {\n    const appDir = path.join(buildUtils.distDir, 'win-unpacked');\n    const { version } = (config.config as any).extraMetadata;\n    const installerPath = path.join(buildUtils.distDir, `Rancher.Desktop.Setup.${ version }.msi`);\n\n    if (config.win && !process.argv.includes('--zip')) {\n      // Only build installer if we're not asked not to.\n      await buildInstaller(buildUtils.distDir, appDir, installerPath);\n    }\n  }\n\n  protected async createLinuxResources(finalBuildVersion: string) {\n    const appData = 'packaging/linux/rancher-desktop.appdata.xml';\n    const release = `<release version=\"${ finalBuildVersion }\" date=\"${ new Date().toISOString() }\"/>`;\n\n    await this.replaceInFile(appData, /<release.*\\/>/g, release, appData.replace('packaging', 'resources'));\n  }\n\n  protected async createWindowsResources(workDir: string) {\n    // Create stub executable with the correct icon (for the installer)\n    const imageFile = path.join(process.cwd(), 'resources', 'icons', 'logo-square-512.png');\n    const iconArgs = ['icon', '--format', 'ico', '--out', workDir, '--input', imageFile];\n    const iconResult = await this.executeAppBuilderAsJson(iconArgs);\n    const iconFile = iconResult.icons[0].file;\n    const executable = path.join(process.cwd(), 'resources', 'win32', 'bin', 'rdctl.exe');\n    const rceditArgs = [executable, '--set-icon', iconFile];\n\n    await executeAppBuilder(['rcedit', '--args', JSON.stringify(rceditArgs)], undefined, undefined, 3);\n\n    // Create the custom action for the installer\n    log.info('building Windows Installer custom action...');\n    const customActionFile = await buildCustomAction();\n\n    // Wait for the virus scanner to be done with the new DLL file\n    for (let i = 0; i < 30; i++) {\n      try {\n        await fs.promises.readFile(customActionFile);\n        break;\n      } catch {\n        await buildUtils.sleep(5_000);\n      }\n    }\n  }\n\n  protected async executeAppBuilderAsJson(...args: Parameters<typeof executeAppBuilder>) {\n    const result = JSON.parse(await executeAppBuilder(...args));\n\n    if (result.error) {\n      throw new Error(result.error);\n    }\n\n    return result;\n  }\n\n  async run() {\n    const options = await this.package();\n\n    await this.buildInstaller(options);\n  }\n}\n\n(new Builder()).run().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/populate-update-server.ts",
    "content": "/**\n * This script is run as part of the \"Build Upgrade Testing\" GitHub workflow\n * (.github/workflows/upgrade-test.yaml) to generate upgrade data for testing\n * Rancher Desktop upgrades.\n *\n * This will push changes to the \"gh-pages\" branch (for the upgrade manifest\n * JSON file), as well as publish releases (or update existing ones) for the\n * upgrade target.\n *\n * Note that this script intentionally blacklists the upstream repository (as\n * defined in package.json) because it changes releases.\n *\n * Inputs are all in environment variables:\n *   GITHUB_TOKEN:      GitHub access token.\n *   GITHUB_REPOSITORY: The GitHub owner/repository (from GitHub Actions).\n *   GITHUB_SHA:        Commit hash (if creating a new release).\n *   GITHUB_ACTOR:      User that triggered this, github.actor\n *   RD_SETUP_MSI:      The installer (msi file) to upload.\n *   RD_MACX86_ZIP:     The macOS (x86_64) zip archive to upload.\n *   RD_MACARM_ZIP:     The macOS (aarch64) zip archive to upload.\n *   RD_BUILD_INFO:     Build information (\"latest.yml\" from electron-builder)\n *   RD_OUTPUT_DIR:     Checkout of `gh-pages`, to be updated.\n */\n\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport path from 'path';\n\nimport { Octokit } from 'octokit';\nimport yaml from 'yaml';\n\nimport { simpleSpawn } from './simple_process';\n\nimport { defined } from '@pkg/utils/typeUtils';\n\n/** Read input from the environment; throws an error if unset. */\nfunction getInput(name: string) {\n  const result = process.env[name];\n\n  if (!result) {\n    throw new Error(`Could not read input; \\$${ name } is not set correctly.`);\n  }\n\n  return result;\n}\n\n/** Given an input variable that expects a single file, return it. */\nasync function getInputFile(name: string) {\n  const inputPath = getInput(name);\n  const stat = await fs.promises.stat(inputPath);\n\n  if (!stat.isDirectory()) {\n    return inputPath;\n  }\n\n  for (const dirent of await fs.promises.readdir(inputPath, { withFileTypes: true })) {\n    if (dirent.isFile()) {\n      return path.join(inputPath, dirent.name);\n    }\n  }\n\n  throw new Error(`Could not find input file for ${ name }`);\n}\n\n/**\n * assetInfo describes information we need about one asset.\n */\ninterface assetInfo {\n  /** filepath is the (full) path to the asset file. */\n  filepath:     string;\n  /** filename is the base name of the asset. */\n  filename:     string;\n  /** length of the file */\n  length:       number;\n  /** checksum is the checksum file contents of the file. */\n  checksum:     string;\n  /** checksumName is the base name of the checksum. */\n  checksumName: string;\n}\n\n/**\n * Given environment name, write checksum contents for the file.\n * @param name Name of the environment variable that holds the file path.\n * @returns File name and checksum data.\n */\nasync function getChecksum(name: string, filenameOverride?: string): Promise<assetInfo> {\n  const filepath = await getInputFile(name);\n  const outputName = filenameOverride || path.basename(filepath);\n  const stat = await fs.promises.stat(filepath);\n  const input = fs.createReadStream(filepath);\n  const hasher = crypto.createHash('sha512');\n  const promise = new Promise<void>((resolve) => {\n    input.on('end', resolve);\n  });\n\n  input.pipe(hasher).setEncoding('hex');\n  await promise;\n  await new Promise<void>((resolve) => {\n    hasher.end(() => {\n      resolve();\n    });\n  });\n\n  return {\n    filepath,\n    filename:     outputName,\n    length:       stat.size,\n    checksum:     `${ hasher.read() }  ${ outputName }`,\n    checksumName: `${ outputName }.sha512sum`,\n  };\n}\n\nasync function getOctokit(): Promise<Octokit> {\n  const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });\n\n  try {\n    await octokit.rest.meta.getZen();\n  } catch (ex) {\n    console.error(`Invalid credentials: please check GITHUB_TOKEN is set. ${ ex }`);\n    process.exit(1);\n  }\n\n  return octokit;\n}\n\nasync function updateRelease(octokit: Octokit, owner: string, repo: string, tag: string) {\n  const files = {\n    msi:    await getChecksum('RD_SETUP_MSI', `Rancher.Desktop.Setup.${ tag }.msi`),\n    macx86: await getChecksum('RD_MACX86_ZIP', `Rancher.Desktop-${ tag }-mac.x86_64.zip`),\n    macarm: await getChecksum('RD_MACARM_ZIP', `Rancher.Desktop-${ tag }-mac.aarch64.zip`),\n  };\n\n  console.log(`Updating release with files:\\n${ yaml.stringify(files) }`);\n\n  let release: Awaited<ReturnType<Octokit['rest']['repos']['createRelease']>>['data'] | undefined;\n\n  try {\n    ({ data: release } = await octokit.rest.repos.getReleaseByTag({\n      owner, repo, tag,\n    }));\n  } catch (ex) {\n    console.log(`Creating new release for ${ tag }: ${ ex }`);\n    ({ data: release } = await octokit.rest.repos.createRelease({\n      owner,\n      repo,\n      name:             tag,\n      tag_name:         tag,\n      target_commitish: getInput('GITHUB_SHA'),\n      draft:            true,\n    }));\n  }\n  if (!release) {\n    throw new Error(`Could not get or create release for ${ tag }`);\n  }\n  console.log(`Got release info for ${ release.name }`);\n\n  await Promise.all(Object.values(files).map(async(info) => {\n    if (!release) {\n      throw new Error(`Could not get or create release for ${ tag }`);\n    }\n    const checksumAsset = release.assets.find(asset => asset.name === info.checksumName);\n\n    if (checksumAsset && release.assets.find(asset => asset.name === info.filename)) {\n      const existingChecksum = (await octokit.rest.repos.getReleaseAsset({\n        owner,\n        repo,\n        asset_id: checksumAsset.id,\n        headers:  { accept: 'application/octet-stream' },\n      })) as unknown as string;\n\n      if (existingChecksum.trim() === info.checksum.trim()) {\n        console.log(`Skipping ${ info.filename }, checksum matches`);\n\n        return;\n      }\n    }\n\n    await Promise.all([info.checksumName, info.filename]\n      .map(name => release?.assets.find(asset => asset.name === name))\n      .filter(defined)\n      .map((asset) => {\n        console.log(`Deleting obsolete asset ${ asset.name }`);\n\n        return octokit.rest.repos.deleteReleaseAsset({\n          owner, repo, asset_id: asset.id,\n        });\n      },\n      ));\n\n    await Promise.all([\n      octokit.rest.repos.uploadReleaseAsset({\n        owner,\n        repo,\n        release_id: release.id,\n        name:       info.checksumName,\n        data:       info.checksum,\n      }),\n      // We need a custom request for the  main file, as we need to stream it\n      // from a file stream.\n      octokit.request({\n        method:  'POST',\n        url:     release.upload_url,\n        headers: {\n          'Content-Length': info.length,\n          'Content-Type':   'application/octet-stream',\n        },\n        data: fs.createReadStream(info.filepath),\n        name: info.filename,\n      }),\n    ]);\n  }));\n  console.log(`Release ${ release.name } updated.`);\n\n  return release.html_url;\n}\n\nasync function updatePages(tag: string) {\n  const response = {\n    versions: [{\n      Name:        tag,\n      ReleaseDate: (new Date()).toISOString(),\n      Tags:        ['latest'],\n    }],\n    requestIntervalInMinutes: 1,\n  };\n\n  console.log('Updating gh-pages...');\n  await fs.promises.writeFile(path.join(getInput('RD_OUTPUT_DIR'), 'response.json'),\n    JSON.stringify(response),\n    'utf-8');\n  await simpleSpawn('git',\n    [\n      '-c', `user.name=${ getInput('GITHUB_ACTOR') }`,\n      '-c', `user.email=${ getInput('GITHUB_ACTOR') }@users.noreply.github.com`,\n      'commit', `--message=Automated update to ${ tag }`, 'response.json',\n    ], {\n      stdio: ['ignore', 'inherit', 'inherit'],\n      cwd:   getInput('RD_OUTPUT_DIR'),\n    });\n  await simpleSpawn('git',\n    ['push'], {\n      stdio: ['ignore', 'inherit', 'inherit'],\n      cwd:   getInput('RD_OUTPUT_DIR'),\n    });\n  console.log('gh-pages updated.');\n}\n\nasync function main() {\n  console.log('Reading configuration information...');\n  const buildInfoPath = await getInputFile('RD_BUILD_INFO');\n  const [owner, repo] = getInput('GITHUB_REPOSITORY').split('/');\n  const packageURL = new URL(JSON.parse(await fs.promises.readFile('package.json', 'utf-8')).repository.url);\n  const [packageOwner, packageRepo] = packageURL.pathname.replace(/\\.git$/, '').split('/').filter(x => x);\n  const buildInfo = yaml.parse(await fs.promises.readFile(buildInfoPath, 'utf-8'));\n  const tag: string = buildInfo.extraMetadata.version.replace(/^v?/, 'v');\n\n  console.log(`Publishing ${ tag } from ${ owner }/${ repo } (upstream is ${ packageOwner }/${ packageRepo })...`);\n  if (packageOwner === owner && packageRepo === repo) {\n    console.error(`Cowardly refusing to touch ${ packageURL }`);\n    process.exit(1);\n  }\n\n  const octokit = await getOctokit();\n  const releaseURL = await updateRelease(octokit, owner, repo, tag);\n  const summaryPath = process.env.GITHUB_STEP_SUMMARY;\n\n  await updatePages(tag);\n  if (summaryPath) {\n    await fs.promises.writeFile(\n      summaryPath,\n      `# Usage instructions\n      1. Publish the release at ${ releaseURL }\n      2. Configure \\`resources\\\\app-update.yml\\` to contain:\n      \\`\\`\\`yaml\n      upgradeServer: https://${ owner }.github.io/${ repo }/response.json\n      owner: ${ owner }\n      repo: ${ repo }\n      \\`\\`\\`\n      `.split(/\\r?\\n/).map(s => s.trim()).filter(s => s).join('\\n'),\n      { encoding: 'utf-8' });\n  }\n}\n\nmain().catch((err) => {\n  console.error(err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/postinstall.ts",
    "content": "import childProcess from 'child_process';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport * as goUtils from 'scripts/dependencies/go-source';\nimport { Lima, Qemu, SocketVMNet, AlpineLimaISO } from 'scripts/dependencies/lima';\nimport { MobyOpenAPISpec } from 'scripts/dependencies/moby-openapi';\nimport { SudoPrompt } from 'scripts/dependencies/sudo-prompt';\nimport { ExtensionProxyImage, WSLDistroImage } from 'scripts/dependencies/tar-archives';\nimport * as tools from 'scripts/dependencies/tools';\nimport { Wix } from 'scripts/dependencies/wix';\nimport { WSLDistro, Moproxy } from 'scripts/dependencies/wsl';\nimport {\n  DependencyPlatform, DependencyVersions, readDependencyVersions, DownloadContext, Dependency,\n} from 'scripts/lib/dependencies';\nimport { simpleSpawn } from 'scripts/simple_process';\n\ninterface DependencyWithContext {\n  dependency: Dependency;\n  context:    DownloadContext;\n}\n\n/**\n * The amount of time we allow the post-install script to run, in milliseconds.\n */\nconst InstallTimeout = 10 * 60 * 1_000; // Ten minutes.\n\n/**\n * Retrieves the application version from package.json to stamp Go binaries.\n * This version number ensures Go utilities like WSL helpers are tagged with\n * the same version as the main application, maintaining consistency across\n * all components of Rancher Desktop.\n */\nconst versionToStamp = getStampVersion();\n\n// Dependencies that should be installed into places that users touch\n// (so users' WSL distros and hosts as of the time of writing).\nconst userTouchedDependencies = [\n  new tools.KuberlrAndKubectl(),\n  new tools.Helm(),\n  new tools.DockerCLI(),\n  new tools.DockerBuildx(),\n  new tools.DockerCompose(),\n  new tools.DockerProvidedCredHelpers(),\n  new tools.ECRCredHelper(),\n  new tools.SpinCLI(),\n  new goUtils.RDCtl(versionToStamp),\n  new goUtils.GoDependency('docker-credential-none'),\n];\n\n// Dependencies that are specific to unix hosts.\nconst unixDependencies = [\n  new Lima(),\n  new Qemu(),\n  new AlpineLimaISO(),\n];\n\n// Dependencies that are specific to macOS hosts.\nconst macOSDependencies = [\n  new SocketVMNet(),\n  new SudoPrompt(),\n];\n\n// Dependencies that are specific to windows hosts.\nconst windowsDependencies = [\n  new WSLDistro(),\n  new WSLDistroImage(),\n  new Wix(),\n  new goUtils.GoDependency('networking/cmd/host', 'internal/host-switch'),\n  new goUtils.WSLHelper(versionToStamp),\n  new goUtils.NerdctlStub(),\n  new goUtils.SpinStub(),\n];\n\n// Dependencies that are specific to WSL.\nconst wslDependencies = [\n  new Moproxy(),\n  new goUtils.RDCtl(versionToStamp),\n  new goUtils.GoDependency('guestagent', 'staging'),\n  new goUtils.GoDependency('networking/cmd/vm', 'staging/vm-switch'),\n  new goUtils.GoDependency('networking/cmd/network', 'staging/network-setup'),\n  new goUtils.GoDependency('networking/cmd/proxy', 'staging/wsl-proxy'),\n  new goUtils.WSLHelper(versionToStamp),\n  new goUtils.NerdctlStub(),\n];\n\n// Dependencies that are specific to WSL and Lima VMs.\nconst vmDependencies = [\n  new tools.Trivy(),\n  new tools.WasmShims(),\n  new tools.CertManager(),\n  new tools.SpinOperator(),\n  new goUtils.GoDependency('extension-proxy', { outputPath: 'staging', env: { CGO_ENABLED: '0' } }),\n  new ExtensionProxyImage(),\n];\n\n// Dependencies that are specific to hosts.\nconst hostDependencies = [\n  new tools.Steve(),\n  new tools.RancherDashboard(),\n  new MobyOpenAPISpec(),\n];\n\nasync function downloadDependencies(items: DependencyWithContext[]): Promise<void> {\n  function specialize(item: DependencyWithContext) {\n    return `${ item.dependency.name }:${ item.context.platform }`;\n  }\n  // Dependencies might depend on other dependencies.  Note that we may have\n  // multiple dependencies of the same name, but different platforms; therefore,\n  // all dependencies are keyed by <name>:<platform>.\n  const dependenciesByName = Object.fromEntries(items.map(item => [specialize(item), item]));\n  const forwardDependencies = Object.fromEntries(items.map(item => [specialize(item), [] as string[]] as const));\n  const reverseDependencies = Object.fromEntries(items.map(item => [specialize(item), [] as string[]] as const));\n  const all = new Set(Object.keys(dependenciesByName));\n  const running = new Set<string>();\n  const done = new Set<string>();\n  const promises: Record<string, Promise<void>> = {};\n\n  for (const item of items) {\n    const dependencies = item.dependency.dependencies?.(item.context) ?? [];\n\n    forwardDependencies[specialize(item)].push(...dependencies);\n    for (const dependency of dependencies) {\n      if (dependency in reverseDependencies) {\n        reverseDependencies[dependency].push(specialize(item));\n      } else {\n        throw new Error(`Dependency ${ item.dependency.name } depends on unknown dependency ${ dependency }`);\n      }\n    }\n  }\n  async function process(name: string) {\n    running.add(name);\n    const item = dependenciesByName[name];\n\n    await item.dependency.download(item.context);\n    done.add(name);\n    for (const dependent of reverseDependencies[name]) {\n      if (!running.has(dependent)) {\n        if (forwardDependencies[dependent].every(d => done.has(d))) {\n          promises[dependent] = process(dependent);\n        }\n      }\n    }\n  }\n\n  for (const item of items.filter(d => (d.dependency.dependencies?.(d.context) ?? []).length === 0)) {\n    promises[specialize(item)] = process(specialize(item));\n  }\n\n  const abortSignal = AbortSignal.timeout(InstallTimeout);\n\n  while (!abortSignal.aborted && running.size > done.size) {\n    const timeout = new Promise((resolve) => {\n      setTimeout(resolve, 60_000);\n      abortSignal.onabort = resolve;\n    });\n    const pending = Array.from(running).filter(v => !done.has(v));\n\n    await Promise.race([timeout, ...pending.map(v => promises[v])]);\n  }\n  abortSignal.onabort = null;\n\n  if (all.size > done.size) {\n    const remaining = Array.from(all).filter(d => !done.has(d)).sort();\n    const message = [`${ remaining.length } dependencies are stuck:`];\n\n    for (const key of remaining) {\n      const deps = forwardDependencies[key].filter(d => !done.has(d));\n      const depsString = deps.length > 0 ? deps.join(', ') : '(nothing)';\n      const started = running.has(key) ? ' (started)' : '';\n\n      message.push(`    ${ key }${ started } depends on ${ depsString }`);\n    }\n    if (abortSignal.aborted) {\n      message.unshift('Timed out downloading dependencies');\n    }\n    throw new Error(message.join('\\n'));\n  }\n}\n\nasync function runScripts(): Promise<void> {\n  // load desired versions of dependencies\n  const depVersions = await readDependencyVersions(path.join('pkg', 'rancher-desktop', 'assets', 'dependencies.yaml'));\n  const platform = os.platform();\n  const dependencies: DependencyWithContext[] = [];\n\n  if (platform === 'linux' || platform === 'darwin') {\n    // download things that go on unix host\n    const hostDownloadContext = await buildDownloadContextFor(platform, depVersions);\n\n    for (const dependency of [...userTouchedDependencies, ...unixDependencies, ...hostDependencies]) {\n      dependencies.push({ dependency, context: hostDownloadContext });\n    }\n\n    // download things for macOS host\n    if (platform === 'darwin') {\n      for (const dependency of macOSDependencies) {\n        dependencies.push({ dependency, context: hostDownloadContext });\n      }\n    }\n\n    // download things that go inside Lima VM\n    const vmDownloadContext = await buildDownloadContextFor('linux', depVersions);\n\n    dependencies.push(...vmDependencies.map(dependency => ({ dependency, context: vmDownloadContext })));\n  } else if (platform === 'win32') {\n    // download things for windows\n    const hostDownloadContext = await buildDownloadContextFor('win32', depVersions);\n\n    for (const dependency of [...userTouchedDependencies, ...windowsDependencies, ...hostDependencies]) {\n      dependencies.push({ dependency, context: hostDownloadContext });\n    }\n\n    // download things that go inside WSL distro\n    const vmDownloadContext = await buildDownloadContextFor('wsl', depVersions);\n\n    for (const dependency of [...userTouchedDependencies, ...wslDependencies, ...vmDependencies]) {\n      dependencies.push({ dependency, context: vmDownloadContext });\n    }\n  }\n\n  await downloadDependencies(dependencies);\n}\n\nasync function buildDownloadContextFor(rawPlatform: DependencyPlatform, depVersions: DependencyVersions): Promise<DownloadContext> {\n  const platform = rawPlatform === 'wsl' ? 'linux' : rawPlatform;\n  const resourcesDir = path.join(process.cwd(), 'resources');\n  const downloadContext: DownloadContext = {\n    versions:           depVersions,\n    dependencyPlatform: rawPlatform,\n    platform,\n    goPlatform:         platform === 'win32' ? 'windows' : platform,\n    isM1:               !!process.env.M1,\n    resourcesDir,\n    binDir:             path.join(resourcesDir, platform, 'bin'),\n    internalDir:        path.join(resourcesDir, platform, 'internal'),\n    dockerPluginsDir:   path.join(resourcesDir, platform, 'docker-cli-plugins'),\n  };\n\n  const dirsToCreate = ['binDir', 'internalDir', 'dockerPluginsDir'] as const;\n\n  await Promise.all(dirsToCreate.map(d => fs.promises.mkdir(downloadContext[d], { recursive: true })));\n\n  return downloadContext;\n}\n\n// The main purpose of this setTimeout is to keep the script waiting until the main async function finishes\nconst keepScriptAlive = setTimeout(() => { }, 24 * 3600 * 1000);\n\n(async() => {\n  let exitCode = 2;\n\n  try {\n    await runScripts();\n    await simpleSpawn('node',\n      ['node_modules/electron-builder/out/cli/cli.js', 'install-app-deps']);\n    exitCode = 0;\n  } catch (e: any) {\n    console.error('POSTINSTALL ERROR: ', e);\n  } finally {\n    clearTimeout(keepScriptAlive);\n    process.exit(exitCode);\n  }\n})();\n\n/**\n* Gets the version string for Go tools from git.\n* Format: {tag}-{commits}-{hash}{dirty}\n* Examples: v1.18.0, v1.18.0-39-gf46609959, v1.18.0-39-gf46609959.m\n*/\nfunction getStampVersion(): string {\n  const gitCommand = 'git describe --match v[0-9]* --dirty=.m --always --tags';\n  const stdout = childProcess.execSync(gitCommand, { encoding: 'utf-8' });\n\n  return stdout;\n}\n"
  },
  {
    "path": "scripts/rddepman.ts",
    "content": "// A cross-platform script to create PRs that bump versions of dependencies.\n\nimport { spawnSync } from 'child_process';\n\nimport { Octokit } from 'octokit';\nimport semver from 'semver';\n\nimport { getExtensions } from './lib/extension-data';\n\nimport { Lima, Qemu, SocketVMNet, AlpineLimaISO } from 'scripts/dependencies/lima';\nimport { MobyOpenAPISpec } from 'scripts/dependencies/moby-openapi';\nimport * as tools from 'scripts/dependencies/tools';\nimport { Wix } from 'scripts/dependencies/wix';\nimport { WSLDistro, Moproxy } from 'scripts/dependencies/wsl';\nimport {\n  AlpineLimaISOVersion, getOctokit,\n  iterateIterator,\n  GitHubDependency,\n  VersionedDependency,\n} from 'scripts/lib/dependencies';\n\nconst MAIN_BRANCH = 'main';\nconst GITHUB_OWNER = process.env.GITHUB_REPOSITORY?.split('/')[0] || 'rancher-sandbox';\nconst GITHUB_REPO = process.env.GITHUB_REPOSITORY?.split('/')[1] || 'rancher-desktop';\n\ninterface VersionComparison {\n  dependency:     VersionedDependency;\n  currentVersion: string | AlpineLimaISOVersion;\n  latestVersion:  string | AlpineLimaISOVersion;\n}\n\nconst dependencies: VersionedDependency[] = [\n  new tools.KuberlrAndKubectl(),\n  new tools.Helm(),\n  new tools.DockerCLI(),\n  new tools.DockerBuildx(),\n  new tools.DockerCompose(),\n  new tools.DockerProvidedCredHelpers(),\n  new tools.GoLangCILint(),\n  new tools.CheckSpelling(),\n  new tools.Trivy(),\n  new tools.Steve(),\n  new tools.RancherDashboard(),\n  new tools.ECRCredHelper(),\n  new Lima(),\n  new Qemu(),\n  new SocketVMNet(),\n  new AlpineLimaISO(),\n  new WSLDistro(),\n  new Wix(),\n  new MobyOpenAPISpec(),\n  new Moproxy(),\n  new tools.WasmShims(),\n  new tools.CertManager(),\n  new tools.SpinOperator(),\n  new tools.SpinCLI(),\n  new tools.SpinKubePlugin(),\n  ...getExtensions(true),\n];\n\n/**\n * Run a git command line.  If the first argument is `true`, return the exit\n * code.  Otherwise, throw an error if the command did not exit with `0`.\n */\nfunction git(...args: string[]): 0 | null;\nfunction git(returnStatus: true, ...args: string[]): number | null;\nfunction git(returnOrArg: string | true, ...args: string[]): number | null {\n  const name = 'Rancher Desktop Dependency Manager';\n  const email = 'donotuse@rancherdesktop.io';\n\n  if (typeof returnOrArg === 'string') {\n    args.unshift(returnOrArg);\n  }\n\n  const result = spawnSync('git', args, {\n    stdio: 'inherit',\n    env:   {\n      ...process.env,\n      GIT_AUTHOR_NAME:     name,\n      GIT_AUTHOR_EMAIL:    email,\n      GIT_COMMITTER_NAME:  name,\n      GIT_COMMITTER_EMAIL: email,\n    },\n  });\n\n  if (result.error) {\n    throw result.error;\n  }\n\n  if (returnOrArg !== true && result.status) {\n    throw `git returned error code ${ result.status }`;\n  }\n\n  return result.status;\n}\n\nfunction printable(version: string | AlpineLimaISOVersion): string {\n  return typeof version === 'string' ? version : version.isoVersion;\n}\n\nfunction getBranchName(name: string, currentVersion: string | AlpineLimaISOVersion, latestVersion: string | AlpineLimaISOVersion): string {\n  return `rddepman/${ name }/${ printable(currentVersion) }-to-${ printable(latestVersion) }`;\n}\n\nfunction getTitle(name: string, currentVersion: string | AlpineLimaISOVersion, latestVersion: string | AlpineLimaISOVersion): string {\n  return `rddepman: bump ${ name } from ${ printable(currentVersion) } to ${ printable(latestVersion) }`;\n}\n\nasync function getBody(dependency: VersionedDependency, currentVersion: string | AlpineLimaISOVersion, latestVersion: string | AlpineLimaISOVersion): Promise<string> {\n  if (!(dependency instanceof GitHubDependency) || typeof currentVersion !== 'string' || typeof latestVersion !== 'string') {\n    // If the dependency is not on GitHub, we don't have any additional information yet.\n    return '';\n  }\n  const currentSemver = semver.parse(currentVersion, true);\n  const latestSemver = semver.parse(latestVersion, true);\n  const { githubOwner: owner, githubRepo: repo } = dependency;\n\n  if (!currentSemver || !latestSemver) {\n    console.log(`Can't parse ${ dependency.name } current or latest version ${ currentVersion } / ${ latestVersion }`);\n\n    return '';\n  }\n\n  type releaseType = Awaited<ReturnType<Octokit['rest']['repos']['listReleases']>>['data'][number];\n  const releaseIterator = getOctokit().paginate.iterator(\n    getOctokit().rest.repos.listReleases,\n    { owner, repo });\n  const releaseNotes: [semver.SemVer, releaseType][] = [];\n\n  for await (const release of iterateIterator(releaseIterator, r => r.data)) {\n    const version = semver.parse(release.tag_name, true);\n\n    if (!version) {\n      if (release.tag_name === dependency.versionToTagName(currentVersion)) {\n        // Version cannot be parsed, but it's the current version.\n        break;\n      }\n      console.log(`Ignoring non-semver ${ dependency.name } version ${ release.tag_name }`);\n      continue;\n    }\n    if (semver.eq(version, currentSemver)) {\n      // Found the current version, don't look at anything older.\n      break;\n    }\n    if (semver.lt(version, currentSemver)) {\n      // Found a patch release of the previous version, or similar.\n      continue;\n    }\n    if (semver.gt(version, latestSemver)) {\n      // Found a version after the latest version (alpha or similar).\n      continue;\n    }\n    if (version.prerelease.length && !latestSemver.prerelease.length) {\n      // This is a pre-release, but the release we're picking is not a pre-release.\n      continue;\n    }\n    releaseNotes.push([version, release]);\n  }\n\n  releaseNotes.sort(([a], [b]) => semver.compare(a, b));\n  let lastVersion = dependency.versionToTagName(currentVersion);\n\n  return releaseNotes.map(([, release]) => {\n    const body = release.body || `Release ${ release.name } does not have release notes.`;\n    const compareLink = [\n      `[Compare between ${ lastVersion } and ${ release.tag_name }]`,\n      `(https://github.com/${ owner }/${ repo }/compare/${ lastVersion }...${ release.tag_name })`,\n    ].join('');\n\n    lastVersion = release.tag_name;\n    if (releaseNotes.length > 1) {\n      // Make sure we don't have leading spaces or this turns into <pre>.\n      return [\n        '<details>',\n        `<summary><h3>${ release.name } (${ release.tag_name })</h3></summary>`,\n        '',\n        body,\n        '</details>',\n        '',\n        compareLink,\n      ].join('\\n');\n    }\n\n    return `## ${ release.name } (${ release.tag_name })\\n${ body }\\n${ compareLink }\\n`;\n  }).join('\\n');\n}\n\nasync function createDependencyBumpPR(dependency: VersionedDependency, currentVersion: string | AlpineLimaISOVersion, latestVersion: string | AlpineLimaISOVersion): Promise<void> {\n  const title = getTitle(dependency.name, currentVersion, latestVersion);\n  const branchName = getBranchName(dependency.name, currentVersion, latestVersion);\n\n  console.log(`Creating PR \"${ title }\".`);\n  try {\n    await getOctokit().rest.pulls.create({\n      owner: GITHUB_OWNER,\n      repo:  GITHUB_REPO,\n      title,\n      body:  await getBody(dependency, currentVersion, latestVersion),\n      base:  MAIN_BRANCH,\n      head:  branchName,\n    });\n  } catch (err: any) {\n    console.log(JSON.stringify(err.response?.data, undefined, 2));\n    throw err;\n  }\n}\n\ntype PRSearchFn = ReturnType<Octokit['rest']['search']['issuesAndPullRequests']>;\n\nasync function getPulls(name: string): Promise<Awaited<PRSearchFn>['data']['items']> {\n  const queryString = `type:pr repo:${ GITHUB_OWNER }/${ GITHUB_REPO } head:rddepman/${ name } sort:updated`;\n  const pullsIterator = getOctokit().paginate.iterator(\n    getOctokit().rest.search.issuesAndPullRequests,\n    { q: queryString });\n  const results: Awaited<PRSearchFn>['data']['items'] = [];\n\n  for await (const item of iterateIterator(pullsIterator, p => p.data)) {\n    if (!item.pull_request) {\n      continue;\n    }\n    const { data: pr } = await getOctokit().rest.pulls.get({\n      owner: GITHUB_OWNER, repo: GITHUB_REPO, pull_number: item.number,\n    });\n\n    if (pr.head.repo && pr.head.repo.full_name !== `${ GITHUB_OWNER }/${ GITHUB_REPO }`) {\n      // Ignore cross-repo PRs; they're not automatically generated.\n      continue;\n    }\n    results.push(item);\n  }\n\n  return results;\n}\n\nasync function determineUpdatesAvailable(): Promise<VersionComparison[]> {\n  const results = await Promise.all(dependencies.map(async dependency => ({\n    dependency,\n    currentVersion: await dependency.currentVersion,\n    latestVersion:  await dependency.latestVersion,\n    canUpgrade:     await dependency.canUpgrade,\n  })));\n\n  for (const {\n    dependency, currentVersion, latestVersion, canUpgrade,\n  } of results) {\n    if (!canUpgrade) {\n      console.log(`${ dependency.name } is up to date (${ JSON.stringify(currentVersion) }).`);\n      continue;\n    }\n\n    console.log(`Can update ${ dependency.name } from ${ JSON.stringify(currentVersion) } to ${ JSON.stringify(latestVersion) }`);\n  }\n\n  return results.filter(x => x.canUpgrade);\n}\n\nasync function checkDependencies(): Promise<void> {\n  // exit if there are unstaged changes\n  git('update-index', '--refresh');\n  if (git(true, 'diff-index', '--quiet', 'HEAD', '--')) {\n    console.log('You have unstaged changes. Commit or stash them to manage dependencies.');\n\n    return;\n  }\n\n  if (process.env.CI) {\n    // When in CI, make sure we compare against the main branch.\n    git('switch', '--force-create', 'main', 'origin/main');\n  }\n\n  const updatesAvailable = await determineUpdatesAvailable();\n\n  if (!process.env.CI) {\n    // When not running in CI, don't try to make pull requests.\n    if (updatesAvailable.length) {\n      console.log(`Not running in CI, skipping creation of ${ updatesAvailable.length } pull requests.`);\n    }\n\n    return;\n  }\n\n  // reconcile dependencies that need an update with state of repo's PRs\n  const needToCreatePR: VersionComparison[] = [];\n\n  await Promise.all(updatesAvailable.map(async({ dependency, currentVersion, latestVersion }) => {\n    // try to find PR for this combo of name, current version and latest version\n    const prs = await getPulls(dependency.name);\n\n    // we use title, rather than branch name, to filter pull requests\n    // because branch name is not available from the search endpoint\n    const title = getTitle(dependency.name, currentVersion, latestVersion);\n    let prExists = false;\n\n    await Promise.all(prs.map(async(pr) => {\n      if (pr.title !== title && pr.state === 'open') {\n        console.log(`Closing stale PR \"${ pr.title }\" (#${ pr.number }).`);\n        await getOctokit().rest.pulls.update({\n          owner: GITHUB_OWNER, repo: GITHUB_REPO, pull_number: pr.number, state: 'closed',\n        });\n      } else if (pr.title === title) {\n        console.log(`Found existing PR \"${ title }\" (#${ pr.number }).`);\n        prExists = true;\n      }\n    }));\n    if (!prExists) {\n      console.log(`Could not find PR \"${ title }\". Will create.`);\n      needToCreatePR.push({\n        dependency, currentVersion, latestVersion,\n      });\n    }\n  }));\n\n  // create a branch for each version update, make changes, and make a PR from the branch\n  for (const { dependency, currentVersion, latestVersion } of needToCreatePR) {\n    const branchName = getBranchName(dependency.name, currentVersion, latestVersion);\n    const commitMessage = `Bump ${ dependency.name } from ${ printable(currentVersion) } to ${ printable(latestVersion) }`;\n\n    git('checkout', '-b', branchName, MAIN_BRANCH);\n    git('add', ...await dependency.updateManifest(latestVersion));\n    git('commit', '--signoff', '--message', commitMessage);\n    git('push', '--force', `https://${ process.env.GITHUB_TOKEN }@github.com/${ GITHUB_OWNER }/${ GITHUB_REPO }`);\n    await createDependencyBumpPR(dependency, currentVersion, latestVersion);\n  }\n}\n\n(async() => {\n  await checkDependencies();\n})().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/release-merge-to-main.ts",
    "content": "// This file creates PRs when releases are published to merge back to the main\n// branch.\n\n// Environment:\n//   GITHUB_REPOSITORY, GITHUB_EVENT_PATH, and others\n//     See https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables\n//   GITHUB_WRITE_TOKEN: GitHub authorization token for creating a branch.\n//     Must have `contents:write` permissions.\n//   GITHUB_PR_TOKEN: GitHub authorization token.\n//     Must have write permissions for `actions` and `pull_requests`.\n\nimport fs from 'fs';\n\nimport { RequestError } from 'octokit';\n\nimport { getOctokit, iterateIterator } from './lib/dependencies';\n\n/**\n * Valid value for an environment variable.\n */\ntype EnvironmentVariableName =\n  'GITHUB_REPOSITORY' |\n  'GITHUB_EVENT_PATH' |\n  'GITHUB_WRITE_TOKEN' |\n  'GITHUB_PR_TOKEN';\n\n/**\n * Partial contents of the event payload, for a release event.\n */\ninterface GitHubReleasePayload {\n  release: {\n    tag_name: string;\n  }\n}\n\n/**\n * The name of the branch to merge into.\n */\nconst base = 'main';\n\n/**\n * Read the environment variable, or throw an error.\n * @param variable The environment variable to look up.\n * @returns The environment variable value.\n */\nfunction getEnv(variable: EnvironmentVariableName): string {\n  const result = process.env[variable];\n\n  if (typeof result !== 'string') {\n    throw new ReferenceError(`Environment variable ${ variable } is not set`);\n  }\n\n  return result;\n}\n\n/**\n * Ensure that the given branch exists, and points to the given tag.\n * @param owner The repository owner.\n * @param repo The repository name (without the owner).\n * @param branchName The name of the branch.\n * @param tagName The name of the tag.\n */\nasync function ensureBranch(owner: string, repo: string, branchName: string, tagName: string): Promise<void> {\n  const ref = `heads/${ branchName }`;\n  const { git } = getOctokit(getEnv('GITHUB_WRITE_TOKEN')).rest;\n  const { data: tagRef } = await git.getRef({\n    owner, repo, ref: `tags/${ tagName }`,\n  });\n  const { sha } = tagRef.object;\n\n  try {\n    const { data: existingBranch } = await git.getRef({\n      owner, repo, ref,\n    });\n\n    if (existingBranch.object.sha !== sha) {\n      // Branch exists, but points at the wrong hash; update it.\n      console.log(`Updating existing branch ${ owner }/${ repo }/${ ref } ` +\n        `from ${ existingBranch.object.sha } to new commit ${ sha }`);\n      await git.updateRef({\n        owner, repo, ref, sha,\n      });\n    } else {\n      console.log(`Branch ${ owner }/${ repo }/${ ref } is already up-to-date.`);\n    }\n  } catch (ex) {\n    if (!(ex instanceof RequestError) || ex.status !== 404) {\n      throw ex;\n    }\n    console.log(`Creating new branch ${ owner }/${ repo }/${ ref } at ${ sha }`);\n    // Branch does not exist; create it.\n    await git.createRef({\n      // Only this API takes a `refs/` prefix; get & update omit it.\n      owner, repo, ref: `refs/${ ref }`, sha,\n    });\n  }\n}\n\n/**\n * Locate an existing pull request.\n * @param owner The repository owner.\n * @param repo The repository name (without the owner).\n * @param branch The branch to merge from (i.e. the release branch).\n * @returns The found pull request, or undefined.\n */\nasync function findExisting(owner: string, repo: string, branch: string) {\n  const fullRepo = `${ owner }/${ repo }`;\n  const query = `type:pr is:open repo:${ fullRepo } base:${ base } head:${ branch } sort:updated`;\n  const pullsIterator = getOctokit(getEnv('GITHUB_WRITE_TOKEN')).paginate.iterator(\n    getOctokit(getEnv('GITHUB_WRITE_TOKEN')).rest.search.issuesAndPullRequests,\n    { q: query });\n\n  for await (const item of iterateIterator(pullsIterator, r => r.data)) {\n    // Must be an open item, and that item must be a pull request.\n    if (item.state !== 'open' || !item.pull_request) {\n      continue;\n    }\n    const { data: pr } = await getOctokit(getEnv('GITHUB_PR_TOKEN')).rest.pulls.get({\n      owner, repo, pull_number: item.number,\n    });\n\n    // PR target must be the expected repository.\n    if (pr.base.repo.full_name !== fullRepo) {\n      console.log(`Skipping ${ item.number }: incorrect base repo ${ pr.base.repo.full_name } (expected ${ fullRepo })`);\n      continue;\n    }\n    // PR target must merge into the default branch.\n    if (pr.base.ref !== base) {\n      console.log(`Skipping ${ item.number }: incorrect base ref ${ pr.base.ref } (expected ${ base })`);\n      continue;\n    }\n    // Must not be a cross-repository (fork) pull request.\n    if (pr.head.repo && pr.head.repo.full_name !== fullRepo) {\n      console.log(`Skipping ${ item.number }: incorrect head repo ${ pr.head.repo.full_name } (expected ${ fullRepo })`);\n      continue;\n    }\n    // Must be a pull request from the expected branch.\n    if (pr.head.ref !== branch) {\n      console.log(`Skipping ${ item.number }: incorrect head ref ${ pr.head.ref } (expected ${ branch })`);\n      continue;\n    }\n\n    return item;\n  }\n}\n\n(async() => {\n  const rawPayload = await fs.promises.readFile(getEnv('GITHUB_EVENT_PATH'), 'utf-8');\n  const payload: GitHubReleasePayload = JSON.parse(rawPayload);\n  const tagName = payload.release.tag_name;\n  const branchName = `merge-${ tagName }`;\n  const fullRepo = getEnv('GITHUB_REPOSITORY');\n  const [, owner, repo] = /([^/]+)\\/(.*)/.exec(fullRepo) ?? [];\n\n  if (!owner || !repo) {\n    throw new TypeError(`Could not determine owner or repo from ${ fullRepo }`);\n  }\n  if (!tagName) {\n    throw new TypeError(`Could not detect tag from ${ rawPayload }`);\n  }\n  console.log(`Processing release event on ${ owner }/${ repo } for tag ${ tagName }...`);\n\n  const existing = await findExisting(owner, repo, branchName);\n\n  if (existing) {\n    console.log(`Found existing PR ${ existing.number }: ${ existing.html_url }`);\n\n    // Note that the existing PR might not be from the same commit as the tag;\n    // this is fine because somebody might have pushed commits on top to resolve\n    // merge conflicts.  Ideally we'd check that the existing PR is a descendant\n    // of the tag commit, but that would essentially involve doing a breadth-\n    // first crawl from the head commit and any limits could lead to false\n    // negatives.  (Or we clone and do `git merge-base --is-ancestor`...)\n    return;\n  }\n\n  console.log(`Creating new PR on ${ owner }/${ repo }: ${ base } <- ${ branchName }`);\n  await ensureBranch(owner, repo, branchName, tagName);\n  const title = `Merge release ${ tagName } back into ${ base }`;\n  const { data: item } = await getOctokit(getEnv('GITHUB_PR_TOKEN')).rest.pulls.create({\n    owner, repo, title, head: branchName, base, maintainer_can_modify: true,\n  });\n\n  console.log(`Created PR #${ item.number }: ${ item.html_url }`);\n})().catch((ex) => {\n  console.error(ex);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/sign.ts",
    "content": "/**\n * This script signs existing builds.\n *\n * Usage: yarn sign -- blahblah.zip\n *\n * Currently, only Windows is supported; mac support is planned.\n */\n\nimport crypto from 'crypto';\nimport fs from 'fs';\nimport path from 'path';\n\nimport extract from 'extract-zip';\n\nimport * as macos from './lib/sign-macos';\nimport * as windows from './lib/sign-win32';\n\nasync function signArchive(archive: string): Promise<void> {\n  const distDir = path.join(process.cwd(), 'dist');\n\n  await fs.promises.mkdir(distDir, { recursive: true });\n  const workDir = await fs.promises.mkdtemp(path.join(distDir, 'sign-'));\n  const archiveDir = path.join(workDir, 'unpacked');\n  let artifacts: string[] | undefined;\n\n  try {\n    // Extract the archive\n    console.log(`Extracting ${ archive } to ${ archiveDir }...`);\n    await fs.promises.mkdir(archiveDir, { recursive: true });\n    await extract(archive, { dir: archiveDir });\n\n    // Detect the archive type\n    for (const file of await fs.promises.readdir(archiveDir)) {\n      if (file.endsWith('.exe')) {\n        artifacts = await windows.sign(workDir);\n        break;\n      }\n      if (file.endsWith('.app')) {\n        artifacts = await macos.sign(workDir);\n        break;\n      }\n    }\n\n    if (!artifacts) {\n      throw new Error(`Could not find any files to sign in ${ archive }`);\n    }\n    await Promise.all(artifacts.map(f => computeChecksum(f)));\n\n    for (const line of ['Signed results:', ...artifacts.map(f => ` - ${ f }`)]) {\n      console.log(line);\n    }\n  } finally {\n    await fs.promises.rm(workDir, { recursive: true, maxRetries: 3 });\n  }\n}\n\nasync function computeChecksum(artifact: string) {\n  const hash = crypto.createHash('sha512');\n  const reader = fs.createReadStream(artifact);\n\n  await new Promise((resolve, reject) => {\n    hash.on('finish', resolve);\n    hash.on('error', reject);\n    reader.pipe(hash);\n  });\n  await fs.promises.writeFile(\n    `${ artifact }.sha512sum`,\n    `${ hash.digest('hex') } *${ path.basename(artifact) }`);\n}\n\n(async() => {\n  try {\n    let fileCount = 0;\n\n    for (const path of process.argv) {\n      if (path.endsWith('.zip')) {\n        fileCount++;\n        await signArchive(path);\n      }\n    }\n    if (fileCount < 1) {\n      throw new Error('No files provided to sign!');\n    }\n  } catch (e) {\n    console.error(e);\n    process.exit(1);\n  }\n})();\n"
  },
  {
    "path": "scripts/simple_process.ts",
    "content": "import { CommonSpawnOptions } from 'child_process';\n\nimport spawn from 'cross-spawn';\n\n/**\n * A wrapper around child_process.spawnFile that doesn't depend on any of the @pkg code\n * @param command\n * @param args - a string array of the arguments\n * @param options - options to pass to spawn()\n */\nexport async function simpleSpawn(\n  command: string,\n  args?: string[],\n  options?: CommonSpawnOptions,\n): Promise<void> {\n  options ||= {};\n  options.windowsHide ??= true;\n  options.stdio ??= 'inherit';\n  const child = spawn(command, args ?? [], options);\n  const currentLine: Record<'stdout' | 'stderr', string> = { stdout: '', stderr: '' };\n  let sawStderr = false;\n\n  child.stdout?.on('data', (chunk: string) => {\n    const currentChunk = chunk.toString();\n    const lastNLIndex = currentChunk.lastIndexOf('\\n');\n\n    if (lastNLIndex === -1) {\n      currentLine.stdout += currentChunk;\n    } else {\n      console.log(currentLine.stdout + currentChunk.substring(0, lastNLIndex));\n      currentLine.stdout = currentChunk.substring(lastNLIndex + 1);\n    }\n  });\n  child.stderr?.on('data', (chunk: string) => {\n    const currentChunk = chunk.toString();\n    const lastNLIndex = currentChunk.lastIndexOf('\\n');\n\n    sawStderr ||= currentChunk.length > 0;\n    if (lastNLIndex === -1) {\n      currentLine.stderr += currentChunk;\n    } else {\n      console.log(currentLine.stderr + currentChunk.substring(0, lastNLIndex));\n      currentLine.stderr = currentChunk.substring(lastNLIndex + 1);\n    }\n  });\n\n  await new Promise<void>((resolve, reject) => {\n    child.on('exit', (code, signal) => {\n      if (currentLine.stdout) {\n        console.log(currentLine.stdout);\n      }\n      if (currentLine.stderr) {\n        console.log(currentLine.stderr);\n      }\n      if (!sawStderr && ((code === 0 && signal === null) || (code === null && signal === 'SIGTERM'))) {\n        return resolve();\n      }\n      reject(JSON.stringify({\n        code, signal, message: `Command failed: ${ [command].concat(args ?? []).join(' ') }`,\n      }));\n    });\n    child.on('error', reject);\n  });\n}\n"
  },
  {
    "path": "scripts/spelling.sh",
    "content": "#!/usr/bin/env bash\n\nset -o errexit -o nounset\n\ncheck_prerequisites() {\n    if [[ -n ${CI:-} && -z ${RD_LINT_SPELLING:-} ]]; then\n        echo \"Skipping spell checking in CI.\"\n        exit\n    fi\n\n    case $(uname -s) in # BSD uname doesn't support long option `--kernel-name`\n        Darwin) check_prerequisites_darwin;;\n        Linux) check_prerequisites_linux;;\n        CYGWIN*|MINGW*|MSYS*) check_prerequisites_windows;;\n        *) printf \"Prerequisites not checked on %s\\n\" \"$(uname -s)\" >&2 ;;\n    esac\n}\n\ncheck_prerequisites_darwin() {\n    if command -v cpanm &>/dev/null; then\n        return\n    fi\n    echo \"Please install cpanminus first:\" >&2\n    if command -v brew &>/dev/null; then\n        echo \"brew install cpanminus\" >&2\n    fi\n    exit 1\n}\n\ncheck_prerequisites_linux() {\n    if command -v wslpath >&/dev/null; then\n        check_prerequisites_windows\n        return\n    fi\n    if [[ -z \"${PERL5LIB:-}\" ]]; then\n        export PERL5LIB=$HOME/perl5/lib/perl5\n    fi\n    if command -v cpanm &>/dev/null; then\n        return\n    fi\n    echo \"Please install cpanminus first:\" >&2\n    if command -v zypper &>/dev/null; then\n        echo \"zypper install perl-App-cpanminus\" >&2\n    elif command -v apt &>/dev/null; then\n        echo \"apt install cpanminus\" >&2\n    fi\n    exit 1\n}\n\ncheck_prerequisites_windows() {\n    # cygwin, mingw, msys, or WSL2.\n    echo \"Skipping spell checking, Windows is not supported.\"\n    exit\n}\n\n# Locate the spell checking script, cloning the GitHub repository if necessary.\nfind_script() {\n    # Put the check-spelling files in `$PWD/resources/host/check-spelling`\n    local checkout=$PWD/resources/host/check-spelling\n    local script=$checkout/unknown-words.sh\n    local repo=https://github.com/check-spelling/check-spelling\n    local version\n    version=\"v$(yq --exit-status .check-spelling pkg/rancher-desktop/assets/dependencies.yaml)\"\n\n    if [[ ! -d \"$checkout\" ]]; then\n        git clone --branch \"$version\" --depth 1 \"$repo\" \"$checkout\" >&2\n    else\n        git -C \"$checkout\" fetch origin \"$version\" >&2\n        git -C \"$checkout\" checkout \"$version\" >&2\n    fi\n\n    if [[ ! -x \"$script\" ]]; then\n        printf \"Failed to checkout check-spelling@%s: %s not found.\\n\" \"$version\" \"$script\" >&2\n        exit 1\n    fi\n\n    echo \"$script\"\n}\n\ncheck_prerequisites\nscript=$(find_script)\n\nINPUTS=$(yq --output-format=json <<EOF\n    suppress_push_for_open_pull_request: 1\n    checkout: true\n    check_file_names: 1\n    post_comment: 0\n    use_magic_file: 1\n    ignore-next-line: spell-checker:disable-next-line\n    report-timing: 1\n    warnings: bad-regex,binary-file,deprecated-feature,ignored-expect-variant,large-file,limited-references,no-newline-at-eof,noisy-file,non-alpha-in-dictionary,token-is-substring,unexpected-line-ending,whitespace-in-dictionary,minified-file,unsupported-configuration,no-files-to-check\n    use_sarif: ${CI:-0}\n    check_extra_dictionaries: \"\"\n    dictionary_source_prefixes: >\n        {\n        \"cspell\": \"https://raw.githubusercontent.com/check-spelling/cspell-dicts/v20241114/dictionaries/\",\n        \"census\": \"https://raw.githubusercontent.com/check-spelling-sandbox/census/dictionaries-d90e686f89dd241ad61d30f26619e54d73e73c6e/dictionaries/\"\n        }\n    extra_dictionaries:\n        cspell:software-terms/softwareTerms.txt\n        census:census-5.txt\n        cspell:npm/npm.txt\n        cspell:k8s/k8s.txt\n        cspell:node/node.txt\n        cspell:aws/aws.txt\n        cspell:python/python/python-lib.txt\n        cspell:golang/go.txt\n        cspell:typescript/typescript.txt\n        cspell:shell/shell-all-words.txt\n        cspell:filetypes/filetypes.txt\n        cspell:html/html.txt\n        cspell:fonts/fonts.txt\n        cspell:php/php.txt\n        cspell:css/css.txt\n        cspell:fullstack/fullstack.txt\n        cspell:cpp/stdlib-cmath.txt\n        cspell:powershell/powershell.txt\n        cspell:dart/dart.txt\nEOF\n)\n\nexport INPUTS\n\nif [[ -z \"${GITHUB_STEP_SUMMARY:-}\" ]]; then\n    # check-spelling falls over without this set; it writes to this file.\n    export GITHUB_STEP_SUMMARY=/dev/null\nfi\n\nexec \"$script\"\n"
  },
  {
    "path": "scripts/ts-wrapper.js",
    "content": "/**\n * This script is a wrapper to run TypeScript scripts via tsx.  This is mostly\n * to set defaults for our tools, such as disabling BrowsersList updates and\n * showing deprecations.\n */\n\nimport { spawnSync } from 'node:child_process';\n\nfunction main(args) {\n  const childArgs = [\n    'node_modules/tsx/dist/cli.mjs',\n    '--trace-warnings',\n    '--trace-deprecation',\n    '--max_old_space_size=4096',\n    '--stack-size=16384',\n  ];\n\n  const finalArgs = [...childArgs, ...args];\n\n  console.log(process.argv0, ...finalArgs);\n  const result = spawnSync(process.argv0, finalArgs, { stdio: 'inherit' });\n\n  if (result.error) {\n    throw result.error;\n  }\n\n  if (typeof result.status === 'number') {\n    process.exit(result.status);\n  }\n\n  if (result.signal) {\n    console.log(`Process exited with signal ${ result.signal }`);\n    process.exit(-1);\n  }\n}\n\n// Silence BrowsersList warnings because they're pointless for us\nprocess.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; // spellcheck-ignore-line\nmain(process.argv.slice(2));\n"
  },
  {
    "path": "scripts/unreleased-change-monitor.ts",
    "content": "import { Octokit } from 'octokit';\n\nimport { Lima, Qemu, AlpineLimaISO } from 'scripts/dependencies/lima';\nimport * as tools from 'scripts/dependencies/tools';\nimport { WSLDistro } from 'scripts/dependencies/wsl';\nimport { GitHubDependency, HasUnreleasedChangesResult, getOctokit, RancherDesktopRepository } from 'scripts/lib/dependencies';\n\nconst GITHUB_OWNER = process.env.GITHUB_REPOSITORY?.split('/')[0] || 'rancher-sandbox';\nconst GITHUB_REPO = process.env.GITHUB_REPOSITORY?.split('/')[1] || 'rancher-desktop';\n// a (hopefully) unique and communicative key that is used to find issues created by\n// this script by filtering them down to the ones that have it in their title\nconst UCMONITOR = 'ucmonitor';\nconst mainRepo = new RancherDesktopRepository(GITHUB_OWNER, GITHUB_REPO);\n\ntype DependencyState = { dependency: GitHubDependency } & HasUnreleasedChangesResult;\n\nconst dependencies: GitHubDependency[] = [\n  new Lima(),\n  new Qemu(),\n  new WSLDistro(),\n  new tools.DockerCLI(),\n  new tools.Steve(),\n  new tools.RancherDashboard(),\n  new AlpineLimaISO(),\n];\n\ntype Issue = Awaited<ReturnType<Octokit['rest']['search']['issuesAndPullRequests']>>['data']['items'][0];\n\nasync function getExistingIssuesFor(dependencyName: string): Promise<Issue[]> {\n  const queryString = `type:issue in:title repo:${ GITHUB_OWNER }/${ GITHUB_REPO } ${ UCMONITOR } ${ dependencyName } sort:updated`;\n  const response = await getOctokit().rest.search.issuesAndPullRequests({ q: queryString });\n\n  return response.data.items;\n}\n\n/**\n * Tells the caller whether the given dependency has any\n * changes that have not been released.\n */\nexport async function hasUnreleasedChanges(dependency: GitHubDependency): Promise<HasUnreleasedChangesResult> {\n  const latestVersion = await dependency.latestVersion;\n  const latestTagName = dependency.versionToTagName(latestVersion);\n\n  // Get the date of the commit that the tag points to.\n  // We can't use the publish date of the release, because that\n  // omits commits that were made after the commit that was tagged\n  // for the release, but before the actual release.\n  const result = await getOctokit().rest.repos.getCommit({\n    owner: dependency.githubOwner, repo: dependency.githubRepo, ref: latestTagName,\n  });\n  const dateOfTaggedCommit = result.data.commit.committer?.date;\n\n  const response = await getOctokit().rest.repos.listCommits({\n    owner: dependency.githubOwner, repo: dependency.githubRepo, since: dateOfTaggedCommit,\n  });\n  const commits = response.data;\n\n  console.log(`Found ${ commits.length - 1 } unreleased commits ` +\n              `for repository ${ dependency.githubOwner }/${ dependency.githubRepo } ` +\n              `since ${ JSON.stringify(latestVersion) } (${ latestTagName }).`);\n\n  return {\n    latestReleaseTag:     latestTagName,\n    hasUnreleasedChanges: commits.length > 1,\n  };\n}\n\n// Creates issues in the main Rancher Desktop repo for external\n// dependencies that have changes that have not been released.\n// Also closes issues that were previously created by this script,\n// but that are no longer relevant.\nasync function checkForUnreleasedChanges(): Promise<void> {\n  const dependencyStates: DependencyState[] = await Promise.all(dependencies.map(async(dependency) => {\n    const result = await hasUnreleasedChanges(dependency);\n\n    return { ...result, dependency };\n  }));\n\n  // reconcile issues with dependency states\n  await Promise.all(dependencyStates.map(async(dependencyState) => {\n    const dependency = dependencyState.dependency;\n\n    // get issues that are relevant to this specific dependency\n    const existingIssues = await getExistingIssuesFor(dependency.name);\n\n    if (dependencyState.hasUnreleasedChanges) {\n      let issueExists = false;\n\n      await Promise.all(existingIssues.map(async(existingIssue) => {\n        const issueTitleMatchesLatestReleaseTag = existingIssue.title.endsWith(` ${ dependencyState.latestReleaseTag }`);\n\n        if (existingIssue.state === 'closed' && issueTitleMatchesLatestReleaseTag) {\n          // issue is closed, but it is the same as the one we would create; open it\n          issueExists = true;\n          await mainRepo.reopenIssue(existingIssue);\n        } else if (existingIssue.state === 'open' && issueTitleMatchesLatestReleaseTag) {\n          // we have an issue that is open that we want to be open\n          issueExists = true;\n        } else if (existingIssue.state === 'open' && !issueTitleMatchesLatestReleaseTag) {\n          // this is an open issue that does not match this release; close it\n          await mainRepo.closeIssue(existingIssue);\n        }\n      }));\n      if (!issueExists) {\n        const title = `${ UCMONITOR }: ${ dependency.name } has changes since ${ dependencyState.latestReleaseTag }`;\n        const body = `Unreleased Change Monitor has detected changes to ${ dependency.name } since its last release, ${ dependencyState.latestReleaseTag }.` +\n          `\\n\\nThis is a reminder to release these changes so they make it into the next Rancher Desktop release.`;\n\n        await mainRepo.createIssue(title, body);\n      }\n    } else {\n      await Promise.all(existingIssues.map(async(existingIssue) => {\n        if (existingIssue.state === 'open') {\n          // there should be no open issues; close this one\n          await mainRepo.closeIssue(existingIssue);\n        }\n      }));\n    }\n  }));\n}\n\ncheckForUnreleasedChanges().catch((e) => {\n  console.error(e);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/windows/generate-nerdctl-stub.ps1",
    "content": "# This script is executed on Windows to regenerate the nerdctl stub argument\n# parsers.  This must be executed on Windows as we need a stable platform to be\n# able to find nerdctl.\n\nparam(\n  [switch]$Verbose\n)\n\n$ENV:GOOS = \"linux\"\n\nSet-Location src/go/nerdctl-stub/generate\ngo build .\nwsl.exe -d rancher-desktop --exec ./generate \"-verbose=$Verbose\"\nRemove-Item ./generate\ngofmt -w ../nerdctl_commands_generated.go\n"
  },
  {
    "path": "scripts/windows/install-wsl.ps1",
    "content": "# //////////////////////////////////////////////////////////////////////\n# install-wsl.ps1\n# See https://docs.microsoft.com/en-us/windows/wsl/install-win10 for the official story behind this code\n\nParam([ValidateSet(\"EnableWSL-01\", \"EnableVMPlatform-02\", \"InstallLinuxUpdatePackage-03\")]$Step = \"EnableWSL-01\")\n\n$workDir = New-Item -ItemType Directory -Force -Path ([System.IO.Path]::GetTempPath()) -Name rdinstall\n\n$logFile = (Join-Path $workDir restarts.txt)\n$wslMsiFile = (Join-Path $workDir wsl_update_x64.msi)\n\n$script = $myInvocation.MyCommand.Definition\n$scriptPath = Split-Path -parent $script\n. (Join-Path $scriptpath restart-helpers.ps1)\n$sudoInstallScript = (Join-Path $scriptPath sudo-install-wsl.ps1)\n\n# Magic PowerShell comment to require admin; see\n# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_requires?view=powershell-5.1#-runasadministrator\n\n#Requires -RunAsAdministrator\n\nif ($Step -eq \"EnableWSL-01\") {\n  Write-Output \"Doing Step EnableWSL-01\"\n  Write-Output \"Doing Step EnableWSL-01\" | Out-File $logFile\n  dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart\n  Restart-Machine-With-Resume-Command $sudoInstallScript \"EnableVMPlatform-02\" \"installation (step 2)\"\n}\n\nif ($Step -eq \"EnableVMPlatform-02\") {\n  Write-Output \"Doing Step EnableVMPlatform-02\"\n  Write-Output \"Doing Step EnableVMPlatform-02\" | Out-File -Append $logFile\n  dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart\n  Restart-Machine-With-Resume-Command $sudoInstallScript \"InstallLinuxUpdatePackage-03\" \"installation (step 3)\"\n}\n\nif ($Step -eq \"InstallLinuxUpdatePackage-03\") {\n  Write-Output \"Doing Step InstallLinuxUpdatePackage-03\"\n  Write-Output \"Doing Step InstallLinuxUpdatePackage-03\" | Out-File -Append $logFile\n  Invoke-WebRequest -Uri https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi -OutFile $wslMsiFile\n  msiexec /norestart /i$wslMsiFile /passive\n  wsl --set-default-version 2\n\n  Write-Host -NoNewLine 'WSL is now installed - press any key to continue'\n  $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');\n}\n"
  },
  {
    "path": "scripts/windows/restart-helpers.ps1",
    "content": "# //////////////////////////////////////////////////////////////////////\n# restart-helpers.ps1\n\nfunction Set-Key([string] $path, [string] $key, [string] $value)\n{\n  Set-ItemProperty -path $path -name $key -value $value\n}\n\nfunction Get-Key([string] $path, [string] $key)\n{\n  return (Get-ItemProperty $path).$key\n}\n\n\n$global:RegRunKey =\"HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce\"\n$global:restartKey = \"Restart-And-Resume\"\n$global:powershell = (Join-Path $PSHOME \"powershell.exe\")\nfunction Restart-Machine-With-Resume-Command([string] $script, [string] $step, [string] $action)\n{\n  $command = \"$global:powershell $script -Step $step\"\n  Set-Key $global:RegRunKey $global:restartKey $command\n  Restart-Machine-On-Acceptance -Action $action -Restart\n}\n\nfunction Restart-Machine-On-Acceptance([string] $action, [switch] $Restart=$false)\n{\n  $prompt = \"A restart is needed to continue $action. You can do this later if you prefer.\"\n  if ($Restart) {\n    $prompt += \"`n`nThere might be a short delay, around a minute, after restart before this script restarts.`n\"\n  }\n  $answer = $Host.UI.PromptForChoice($prompt, 'Restart now?', @('&Yes', '&No'), 1)\n  if ($answer -eq 0) {\n    Restart-Computer\n    exit\n  }\n}\n"
  },
  {
    "path": "scripts/windows/sudo-install-wsl.ps1",
    "content": "# //////////////////////////////////////////////////////////////////////\n# sudo-install-ws1.ps1\n\nParam(\n    [Parameter(Mandatory)]\n    [ValidateSet(\"EnableWSL-01\", \"EnableVMPlatform-02\", \"InstallLinuxUpdatePackage-03\")]\n    $Step = \"EnableWSL-01\")\n\n$script = $myInvocation.MyCommand.Definition\n$scriptPath = Split-Path -parent $script\n. (Join-Path $scriptpath restart-helpers.ps1)\n\ntry {\n  Start-Process $psHome\\powershell.exe -Verb Runas -ArgumentList \"$ScriptPath/install-wsl.ps1 -Step $Step\"\n} catch {\n  echo \"Something bad happened\"\n  pause\n}\n"
  },
  {
    "path": "scripts/windows/uninstall-wsl.ps1",
    "content": "# //////////////////////////////////////////////////////////////////////\n# uninstall-wsl.ps1\n\n$script = $myInvocation.MyCommand.Definition\n$scriptPath = Split-Path -parent $script\n. (Join-Path $scriptpath restart-helpers.ps1)\n\n# Magic PowerShell comment to require admin; see\n# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_requires?view=powershell-5.1#-runasadministrator\n\n#Requires -RunAsAdministrator\n\nwslconfig /u k3s\n\nDisable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart\n\nRestart-Machine-On-Acceptance -Action \"uninstall wsl\"\n"
  },
  {
    "path": "scripts/windows-setup.ps1",
    "content": "param (\n    [switch] $SkipVisualStudio,\n    [switch] $SkipTools,\n    [switch] $SkipWSL\n)\n\n$InformationPreference = 'Continue'\n\nWrite-Information 'Installing components required for Rancher Desktop development...'\n\n# Start separate jobs for things we want to install (in subprocesses).\n\nif (!$SkipVisualStudio) {\n    Start-Job -Name 'Visual Studio' -ErrorAction Stop -ScriptBlock {\n        $location = Get-CimInstance -ClassName MSFT_VSInstance `\n            | Where-Object { $_.IsComplete } `\n            | Select-Object -First 1 -ExpandProperty InstallLocation\n        # This path appears to be hard-coded:\n        # https://docs.microsoft.com/en-us/visualstudio/install/modify-visual-studio#open-the-visual-studio-installer\n        $installer = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vs_installer.exe'\n\n        # Updating first is required; otherwise, the installer just complains that\n        # the installer itself is out of date.\n        Write-Information 'Updating Visual Studio components...'\n        & $installer update --installPath $location --passive\n        Write-Information 'Waiting for Visual Studio update to complete...'\n        Get-Process | Where-Object Name -in ('setup', 'vs_installer') | Wait-Process\n\n        Write-Information 'Installing additional Visual Studio components...'\n        & $installer modify --installPath $location --passive `\n            --add Microsoft.VisualStudio.Component.VC.v141.x86.x64 `\n            --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64\n        Write-Information 'Waiting for Visual Studio installation to complete...'\n        Get-Process | Where-Object Name -in ('setup', 'vs_installer') | Wait-Process\n\n        # Tell NPM to use MSBuild from the just-updated copy of Visual Studio.\n        # This is required as otherwise node-gyp will be unable to find it.\n        $msbuild = Join-Path $location 'MSBuild/Current/Bin/MSBuild.exe'\n        Write-Output \"msbuild_path=${msbuild}\" `\n            | Out-File -Encoding UTF8 -FilePath ~/.npmrc\n    }\n}\n\n\nif (!$SkipTools) {\n    Start-Job -Name 'Install Tools' -ErrorAction Stop -ScriptBlock {\n        Write-Information 'Installing Tools...'\n\n        Invoke-WebRequest -UseBasicParsing -Uri 'https://get.scoop.sh' `\n            | Invoke-Expression\n        scoop install 7zip git go mingw nvm python unzip\n        # Install and use latest node 18* version\n        nvm install 18\n        nvm use $(nvm list | Select-String '[18\\.[0-9.]+]' | Select-Object -First 1 | ForEach-Object { $_.Matches.Value })\n        # Install the yarn package manager\n        npm install --global yarn\n    }\n}\n\n# Wait for all jobs to finish.\nGet-Job | Receive-Job -Wait -ErrorAction Stop\n# Show that all jobs are done\nGet-Job\nWrite-Information 'Rancher Desktop development environment setup complete.'\n\nif (! (Get-Command wsl -ErrorAction SilentlyContinue) -and !$SkipWSL) {\n    Write-Information 'installing wsl.... This will require a restart'\n\n    $targetDir = (Join-Path ([System.IO.Path]::GetTempPath()) rdinstall)\n    New-Item -ItemType Directory -Force -Path $targetDir\n\n    $files = (\"install-wsl.ps1\", \"restart-helpers.ps1\", \"sudo-install-wsl.ps1\", \"uninstall-wsl.ps1\")\n    foreach ($file in $files) {\n        $url = \"https://raw.githubusercontent.com/rancher-sandbox/rancher-desktop/main/scripts/windows/$file\"\n        $outFile = (Join-Path $targetDir $file)\n        Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $outFile\n    }\n\n    $sudoPath = (Join-Path $targetDir sudo-install-wsl.ps1)\n    & $sudoPath -Step \"EnableWSL-01\"\n}\n"
  },
  {
    "path": "scripts/wix.ts",
    "content": "// This script builds the wix installer, assuming the zip file has already been\n// built (and dist/win-unpacked is populated).\n// This is only used during development.\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport buildInstaller, { buildCustomAction } from './lib/installer-win32';\n\nasync function run() {\n  const distDir = path.join(process.cwd(), 'dist');\n  const appDir = path.join(distDir, 'win-unpacked');\n\n  try {\n    await fs.promises.access(path.join(appDir, 'resources', 'app.asar'), fs.constants.R_OK);\n  } catch (ex) {\n    if ((ex as NodeJS.ErrnoException).code !== 'ENOENT') {\n      throw ex;\n    }\n    console.error(`Could not find ${ appDir }, please run \\`yarn build\\` first.`);\n    process.exit(1);\n  }\n\n  const customActionFile = await buildCustomAction();\n\n  await fs.promises.copyFile(customActionFile,\n    path.join(appDir, path.basename(customActionFile)));\n  await buildInstaller(distDir, appDir);\n}\n\nrun().catch((ex) => {\n  console.error(ex);\n  process.exit(1);\n});\n"
  },
  {
    "path": "scripts/yarn-dedupe.sh",
    "content": "#!/bin/bash\n\n# Deduplicate yarn.lock and optionally push a pull request.\n# Expects to run from the repository root on the main branch.\n#\n# Usage: yarn-dedupe.sh [--push]\n#\n# Without --push, runs yarn dedupe and reports changes (safe for local use).\n# With --push, commits, pushes, and creates a PR if one does not already exist.\n\nset -eu\n\nPUSH=\"false\"\nfor arg in \"$@\"; do\n    case \"$arg\" in\n        --push) PUSH=\"true\" ;;\n        *) echo \"Unknown option: $arg\" >&2; exit 1 ;;\n    esac\ndone\n\nBRANCH_NAME=\"yarn-dedupe\"\n\nyarn dedupe\n\n# Exit if yarn.lock is unchanged\nif git diff --quiet yarn.lock; then\n    echo \"yarn.lock is already deduplicated.\"\n    exit\nfi\n\nif [ \"$PUSH\" = \"false\" ]; then\n    echo \"yarn.lock has duplicates that yarn dedupe would remove.\"\n    echo \"Run with --push to commit, push, and open a PR.\"\n    git diff --stat yarn.lock\n    exit\nfi\n\nexport GIT_CONFIG_COUNT=2\nexport GIT_CONFIG_KEY_0=user.name\nexport GIT_CONFIG_VALUE_0=\"Rancher Desktop GitHub Action\"\nexport GIT_CONFIG_KEY_1=user.email\nexport GIT_CONFIG_VALUE_1=\"donotuse@rancherdesktop.io\"\n\ngit checkout -B \"$BRANCH_NAME\"\ngit add yarn.lock\ngit commit --signoff --message \"Run yarn dedupe\"\ngit push --force origin \"$BRANCH_NAME\"\n\n# Create a PR only if one does not already exist for this branch.\nif gh pr list --head \"$BRANCH_NAME\" --json number --jq '.[].number' | grep --quiet .; then\n    echo \"PR already exists for branch $BRANCH_NAME; skipping creation.\"\nelse\n    gh pr create \\\n        --title \"Deduplicate yarn.lock\" \\\n        --body \"Automated pull request to remove duplicate dependency resolutions from yarn.lock.\" \\\n        --head \"$BRANCH_NAME\" \\\n        --base main\nfi\n"
  },
  {
    "path": "src/go/docker-credential-none/dcnone/dcnone.go",
    "content": "// Package dcnone implements a `none` based credential helper.\n// Passwords are stored base64-encoded but unencrypted in\n// ~/.docker/plaintext-credentials.config.json\n// in the `auths` section\n// as `ServerURL: auth : base64Encode(Username + \":\" + Secret)`\n\npackage dcnone\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"path/filepath\"\n\n\tdockerconfig \"github.com/docker/cli/cli/config\"\n\t\"github.com/docker/docker-credential-helpers/credentials\"\n)\n\nconst configFileName = \"plaintext-credentials.config.json\"\n\nconst VERSION = \"0.6.4\"\n\n// DCNone handles secrets using HOME/.docker/plaintext-credentials.config.json as a store.\ntype DCNone struct{}\n\nvar configFile string\n\nfunc init() {\n\tconfigFile = filepath.Join(dockerconfig.Dir(), configFileName)\n\tcredentials.Name = \"docker-credential-none\"\n\tcredentials.Package = \"github.com/rancher-sandbox/rancher-desktop/src/go/docker-credential-none\"\n\tcredentials.Version = VERSION\n}\n\n// Add stores a new credentials or updates an existing one.\nfunc (p DCNone) Add(creds *credentials.Credentials) error {\n\tvar auths map[string]any\n\n\tif creds == nil {\n\t\treturn errors.New(\"missing credentials\")\n\t}\n\tconfig, err := getParsedConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tauthsInterface, ok := config[\"auths\"]\n\tif ok {\n\t\tauths, ok = authsInterface.(map[string]any)\n\t}\n\tif !ok {\n\t\t// Either config['auths'] doesn't exist or it isn't a hash\n\t\tauths = map[string]any{}\n\t\tconfig[\"auths\"] = auths\n\t}\n\tpayload := fmt.Sprintf(\"%s:%s\", creds.Username, creds.Secret)\n\tencoded := base64.URLEncoding.EncodeToString([]byte(payload))\n\tauths[creds.ServerURL] = map[string]string{\"auth\": encoded}\n\treturn saveParsedConfig(&config)\n}\n\n// Delete removes credentials from the store.\nfunc (p DCNone) Delete(serverURL string) error {\n\tif serverURL == \"\" {\n\t\treturn errors.New(\"missing server url\")\n\t}\n\tconfig, err := getParsedConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tauthsInterface, ok := config[\"auths\"]\n\tif !ok {\n\t\t// Not an error if there's no URL (or auths)\n\t\treturn nil\n\t}\n\tauths, ok := authsInterface.(map[string]any)\n\tif !ok {\n\t\t// Same as above -- if we can't get the hash we don't have a URL entry to remove\n\t\treturn nil\n\t}\n\t_, ok = auths[serverURL]\n\tif !ok {\n\t\t// Not an error if there's no URL (or auths)\n\t\treturn nil\n\t}\n\tdelete(auths, serverURL)\n\treturn saveParsedConfig(&config)\n}\n\n// Get returns the username and secret to use for a given registry server URL.\nfunc (p DCNone) Get(serverURL string) (string, string, error) {\n\tif serverURL == \"\" {\n\t\treturn \"\", \"\", errors.New(\"missing server url\")\n\t}\n\tconfig, err := getParsedConfig()\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\tusername, secret, err := getRecordForServerURL(&config, serverURL)\n\tif err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\treturn username, secret, nil\n}\n\n// List returns the stored URLs and corresponding usernames for a given credentials label\nfunc (p DCNone) List() (map[string]string, error) {\n\tentries := make(map[string]string)\n\tconfig, err := getParsedConfig()\n\tif err != nil {\n\t\treturn entries, err\n\t}\n\tauthsInterface, ok := config[\"auths\"]\n\tif ok {\n\t\tauths, ok := authsInterface.(map[string]any)\n\t\tif !ok {\n\t\t\treturn entries, fmt.Errorf(\"unexpected data: %v: not a hash\", authsInterface)\n\t\t}\n\t\tfor url := range auths {\n\t\t\tusername, _, err := getRecordForServerURL(&config, url)\n\t\t\tif username != \"\" && err == nil {\n\t\t\t\tentries[url] = username\n\t\t\t}\n\t\t}\n\t}\n\treturn entries, nil\n}\n"
  },
  {
    "path": "src/go/docker-credential-none/dcnone/dcnone_test.go",
    "content": "package dcnone\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/docker/docker-credential-helpers/credentials\"\n)\n\nfunc TestDCNoneHelper(t *testing.T) {\n\thelper := DCNone{}\n\n\tconst server1 = \"https://foobar.docker.io:2376/v1\"\n\tconst server2 = \"https://foobar.docker.io:9999/v2\"\n\tsawServers := map[string]bool{\n\t\tserver1: false,\n\t\tserver2: false,\n\t}\n\tcreds := &credentials.Credentials{\n\t\tServerURL: server1,\n\t\tUsername:  \"nothing\",\n\t\tSecret:    \"isthebestmeshuggahalbum\",\n\t}\n\n\thelper.Add(creds)\n\n\tcreds.ServerURL = server2\n\thelper.Add(creds)\n\n\tcredsList, err := helper.List()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfor server, username := range credsList {\n\t\t_, ok := sawServers[server]\n\t\tif ok {\n\t\t\tsawServers[server] = true\n\t\t} else {\n\t\t\tcontinue\n\t\t}\n\n\t\tif username != \"nothing\" {\n\t\t\tt.Fatalf(\"invalid username: %v\", username)\n\t\t}\n\n\t\tu, s, err := helper.Get(server)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tif u != username {\n\t\t\tt.Fatalf(\"invalid username %s\", u)\n\t\t}\n\n\t\tif s != \"isthebestmeshuggahalbum\" {\n\t\t\tt.Fatalf(\"invalid secret: %s\", s)\n\t\t}\n\n\t\terr = helper.Delete(server)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tusername, _, err = helper.Get(server)\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"Not an error trying to find deleted serverURL %s\", server)\n\t\t}\n\t\tif !errors.Is(err, credentials.NewErrCredentialsNotFound()) {\n\t\t\tt.Fatalf(\"Trying to search delete URL %s should give error %s, gave error %s\", server, credentials.NewErrCredentialsNotFound(), err)\n\t\t}\n\n\t\tif username != \"\" {\n\t\t\tt.Fatalf(\"%s shouldn't exist any more\", username)\n\t\t}\n\t}\n\tfor serverURL, processed := range sawServers {\n\t\tif !processed {\n\t\t\tt.Fatalf(\"Failed to store server %s\", serverURL)\n\t\t}\n\t}\n\n\tcredsList, err = helper.List()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor server := range credsList {\n\t\t_, ok := sawServers[server]\n\t\tif ok {\n\t\t\tt.Fatalf(\"Failed to delete server %s\", server)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/go/docker-credential-none/dcnone/helpers.go",
    "content": "package dcnone\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"strings\"\n\n\tdockerconfig \"github.com/docker/cli/cli/config\"\n\t\"github.com/docker/docker-credential-helpers/credentials\"\n)\n\ntype dockerConfigType map[string]any\n\nfunc getParsedConfig() (dockerConfigType, error) {\n\tdockerConfig := make(dockerConfigType)\n\tcontents, err := os.ReadFile(configFile)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// Time to create a new config (or return no data)\n\t\t\treturn dockerConfig, nil\n\t\t}\n\t\treturn dockerConfig, err\n\t}\n\terr = json.Unmarshal(contents, &dockerConfig)\n\tif err != nil {\n\t\treturn dockerConfig, fmt.Errorf(\"reading config file %s: %s\", configFile, err)\n\t}\n\treturn dockerConfig, nil\n}\n\nfunc saveParsedConfig(config *dockerConfigType) error {\n\tcontents, err := json.MarshalIndent(config, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tscratchFile, err := os.CreateTemp(dockerconfig.Dir(), \"tmpconfig.json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.WriteFile(scratchFile.Name(), contents, 0o600)\n\tscratchFile.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(scratchFile.Name(), configFile)\n}\n\n/**\n * Returns the Username and Secret associated with `urlArg`, or an error if there was a problem.\n */\nfunc getRecordForServerURL(config *dockerConfigType, urlArg string) (string, string, error) {\n\tauthsInterface, ok := (*config)[\"auths\"]\n\tif !ok {\n\t\treturn \"\", \"\", credentials.NewErrCredentialsNotFound()\n\t}\n\tauths := authsInterface.(map[string]any)\n\tauthDataForURL, ok := auths[urlArg]\n\tif !ok {\n\t\treturn \"\", \"\", credentials.NewErrCredentialsNotFound()\n\t}\n\tauthData, ok := authDataForURL.(map[string]any)[\"auth\"]\n\tif !ok {\n\t\treturn \"\", \"\", credentials.NewErrCredentialsNotFound()\n\t}\n\tcredentialPair, err := base64.StdEncoding.DecodeString(authData.(string))\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"base64-decoding authdata for URL %s: %s\", urlArg, err)\n\t}\n\tparts := strings.SplitN(string(credentialPair), \":\", 2)\n\tif len(parts) == 1 {\n\t\treturn \"\", \"\", fmt.Errorf(\"not a valid base64-encoded pair: <%s>\", authData.(string))\n\t}\n\tif parts[0] == \"\" {\n\t\treturn \"\", \"\", credentials.NewErrCredentialsMissingUsername()\n\t}\n\treturn parts[0], parts[1], nil\n}\n"
  },
  {
    "path": "src/go/docker-credential-none/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/docker-credential-none\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/docker/cli v29.3.0+incompatible\n\tgithub.com/docker/docker-credential-helpers v0.9.5\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n\tgotest.tools/v3 v3.5.2 // indirect\n)\n"
  },
  {
    "path": "src/go/docker-credential-none/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=\ngithub.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=\ngithub.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\n"
  },
  {
    "path": "src/go/docker-credential-none/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/docker/docker-credential-helpers/credentials\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/docker-credential-none/dcnone\"\n)\n\nfunc main() {\n\tcredentials.Serve(dcnone.DCNone{})\n}\n"
  },
  {
    "path": "src/go/extension-proxy/README.md",
    "content": "# extension-proxy\n\nThis program is used to forward HTTP requests from the extension frontend to the\nextension backend.  The frontend makes a HTTP request using a relative URL\n(doing something like `ddClient.extension.vm.service.get('/foo')`), which must\nbe routed to the backend listening on a Unix socket.  This program is used to\nhandle the forwarding from some TCP port into that Unix socket.\n\nThe environment variable `SOCKET` should be set to the path of a Unix socket,\nwhich will be forwarded to port 80.  Typically this would be set to the name of\na socket in `/run/guest-services/`, which is then shared (via a volume) with\nother containers.\n"
  },
  {
    "path": "src/go/extension-proxy/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/extension-port-forwarder\n\ngo 1.25.0\n"
  },
  {
    "path": "src/go/extension-proxy/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"net/url\"\n\t\"os\"\n\t\"time\"\n)\n\nfunc main() {\n\tsocketPath := flag.String(\"socket\", os.Getenv(\"SOCKET\"), \"socket to forward to\")\n\n\tif socketPath == nil {\n\t\tlog.Fatal(\"no socket path specified, aborting\")\n\t}\n\n\t// A explicit dialer is required to get a DialContext.\n\tdialer := &net.Dialer{}\n\n\tproxy := &httputil.ReverseProxy{\n\t\tTransport: &http.Transport{\n\t\t\tDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\treturn dialer.DialContext(ctx, \"unix\", *socketPath)\n\t\t\t},\n\t\t},\n\t\tDirector: func(r *http.Request) {\n\t\t\t// The incoming URL is normally missing scheme and host.\n\t\t\t// Re-resolve the URL with dummy values so that it could at least get far\n\t\t\t// enough to hit our transport (which ignores the host name).\n\t\t\tbase := url.URL{Scheme: \"http\", Host: \"localhost\"}\n\t\t\tr.URL = base.ResolveReference(r.URL)\n\t\t},\n\t}\n\n\tserver := &http.Server{\n\t\tAddr:        \":80\",\n\t\tHandler:     proxy,\n\t\tReadTimeout: time.Minute,\n\t}\n\terr := server.ListenAndServe()\n\tif err != nil {\n\t\tlog.Printf(\"stopped listening: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "src/go/guestagent/README.md",
    "content": "[Rancher Desktop Guest Agent](/docs/networking/windows/rancher-desktop-guest-agent.md)"
  },
  {
    "path": "src/go/guestagent/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/guestagent\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/Masterminds/log-go v1.0.0\n\tgithub.com/containerd/containerd v1.7.30\n\tgithub.com/containerd/containerd/api v1.10.0\n\tgithub.com/containernetworking/plugins v1.9.1\n\tgithub.com/containers/gvisor-tap-vsock v0.8.8\n\tgithub.com/docker/docker v28.5.2+incompatible\n\tgithub.com/docker/go-connections v0.6.0\n\tgithub.com/lima-vm/lima v1.0.0-beta.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/sync v0.20.0\n\tgolang.org/x/sys v0.42.0\n\tgoogle.golang.org/protobuf v1.36.11\n\tk8s.io/api v0.35.3\n\tk8s.io/apimachinery v0.35.3\n\tk8s.io/client-go v0.35.3\n)\n\nrequire (\n\tcyphar.com/go-pathrs v0.2.1 // indirect\n\tgithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect\n\tgithub.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Microsoft/hcsshim v0.13.0 // indirect\n\tgithub.com/containerd/cgroups/v3 v3.0.3 // indirect\n\tgithub.com/containerd/continuity v0.4.5 // indirect\n\tgithub.com/containerd/errdefs v0.3.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/fifo v1.1.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v0.2.1 // indirect\n\tgithub.com/containerd/ttrpc v1.2.7 // indirect\n\tgithub.com/containerd/typeurl/v2 v2.2.0 // indirect\n\tgithub.com/coreos/go-iptables v0.8.0 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.6.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.12.2 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.1 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.3 // indirect\n\tgithub.com/go-openapi/swag v0.23.1 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.1 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.17.9 // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/locker v1.0.1 // indirect\n\tgithub.com/moby/sys/atomicwriter v0.1.0 // indirect\n\tgithub.com/moby/sys/mountinfo v0.7.1 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/signal v0.7.0 // indirect\n\tgithub.com/moby/sys/user v0.3.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.0 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/morikuni/aec v1.0.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/onsi/ginkgo/v2 v2.28.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/opencontainers/runtime-spec v1.2.0 // indirect\n\tgithub.com/opencontainers/selinux v1.13.1 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/vishvananda/netlink v1.3.1 // indirect\n\tgithub.com/vishvananda/netns v0.0.5 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.1.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect\n\tgo.opentelemetry.io/otel v1.37.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.37.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.37.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/oauth2 v0.30.0 // indirect\n\tgolang.org/x/term v0.41.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect\n\tgoogle.golang.org/grpc v1.69.2 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgotest.tools/v3 v3.5.2 // indirect\n\tk8s.io/klog/v2 v2.130.1 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect\n\tk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect\n\tsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect\n\tsigs.k8s.io/knftables v0.0.18 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect\n\tsigs.k8s.io/yaml v1.6.0 // indirect\n)\n\nreplace github.com/lima-vm/lima => github.com/rancher-sandbox/lima v1.0.3-0.20250115235144-24eb898b3a96\n"
  },
  {
    "path": "src/go/guestagent/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=\ncyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA=\ngithub.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/Masterminds/log-go v1.0.0 h1:yjncypw3bbpezgjTSv+Jsy7+W5Pn/7S5RSoy+Wc8zCI=\ngithub.com/Masterminds/log-go v1.0.0/go.mod h1:l7N6BwMpaAz9Wn6f7YSz/OTpAbfiKqdB6t++H/EYWoM=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA=\ngithub.com/Microsoft/hcsshim v0.13.0/go.mod h1:9KWJ/8DgU+QzYGupX4tzMhRQE8h6w90lH6HAaclpEok=\ngithub.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0=\ngithub.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0=\ngithub.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=\ngithub.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M=\ngithub.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=\ngithub.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=\ngithub.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=\ngithub.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=\ngithub.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=\ngithub.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=\ngithub.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=\ngithub.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso=\ngithub.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g=\ngithub.com/containernetworking/cni v1.3.0 h1:v6EpN8RznAZj9765HhXQrtXgX+ECGebEYEmnuFjskwo=\ngithub.com/containernetworking/cni v1.3.0/go.mod h1:Bs8glZjjFfGPHMw6hQu82RUgEPNGEaBb9KS5KtNMnJ4=\ngithub.com/containernetworking/plugins v1.9.1 h1:8oU6WsIsU3bpnNZuvHp74a6cE1MJwbj2P7s4/yTUNlA=\ngithub.com/containernetworking/plugins v1.9.1/go.mod h1:fj7kS55qg3o/RgS+WGsF3+ZxwIImMPusQZKzBpcSr4c=\ngithub.com/containers/gvisor-tap-vsock v0.8.8 h1:5FznbOYMIuaCv8B6zQ7M6wjqP63Lasy0A6GpViEnjTg=\ngithub.com/containers/gvisor-tap-vsock v0.8.8/go.mod h1:m/PzhZWAS6T9pCRH1fLkq2OqbEd6QEUZWjm3FS5F+CE=\ngithub.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=\ngithub.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=\ngithub.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=\ngithub.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=\ngithub.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=\ngithub.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=\ngithub.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=\ngithub.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=\ngithub.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=\ngithub.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=\ngithub.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=\ngithub.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=\ngithub.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=\ngithub.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=\ngithub.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc=\ngithub.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=\ngithub.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=\ngithub.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g=\ngithub.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI=\ngithub.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg=\ngithub.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo=\ngithub.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=\ngithub.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc=\ngithub.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=\ngithub.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=\ngithub.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=\ngithub.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=\ngithub.com/rancher-sandbox/lima v1.0.3-0.20250115235144-24eb898b3a96 h1:ZznVstK4n3Ehhr6YCKrxs+Axhr7HbSnPPc78RtbDd+o=\ngithub.com/rancher-sandbox/lima v1.0.3-0.20250115235144-24eb898b3a96/go.mod h1:VUsWVZHOFu2m/bnCBqk5Kk/wv9Rrx7Sj1+phdqA2ka4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=\ngithub.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=\ngo.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=\ngo.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=\ngo.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=\ngo.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=\ngo.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=\ngo.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=\ngo.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0=\ngo.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc=\ngo.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8=\ngo.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=\ngo.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=\ngo.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=\ngo.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=\ngo.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=\ngolang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk=\ngoogle.golang.org/genproto v0.0.0-20241118233622-e639e219e697/go.mod h1:JJrvXBWRZaFMxBufik1a4RpFw4HhgVtBBWQeQgUj2cc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU=\ngoogle.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=\ngoogle.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=\ngopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nk8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ=\nk8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4=\nk8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8=\nk8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=\nk8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=\nk8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=\nk8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=\nsigs.k8s.io/knftables v0.0.18 h1:6Duvmu0s/HwGifKrtl6G3AyAPYlWiZqTgS8bkVMiyaE=\nsigs.k8s.io/knftables v0.0.18/go.mod h1:f/5ZLKYEUPUhVjUCg6l80ACdL7CIIyeL0DxfgojGRTk=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "src/go/guestagent/main.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Rancher-desktop-guestagent runs inside the WSL VM on Windows. It is\n// primarily used to monitor and forward Kubernetes Service Ports\n// (NodePorts and LoadBalancers) to the host. Also, it can be configured\n// to perform port forwarding for the exposed container ports on both\n// Moby and Containerd backends.\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/containerd\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/docker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/forwarder\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/iptables\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/kube\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/procnet\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n)\n\nconst (\n\tiptablesUpdateInterval = 3 * time.Second\n\tprocNetScanInterval    = 3 * time.Second\n\tsocketInterval         = 5 * time.Second\n\tsocketRetryTimeout     = 2 * time.Minute\n\tdockerSocketFile       = \"/var/run/docker.sock\"\n\tcontainerdSocketFile   = \"/run/k3s/containerd/containerd.sock\"\n)\n\nfunc main() {\n\tvar (\n\t\tdebug            = flag.Bool(\"debug\", false, \"display debug output\")\n\t\tconfigPath       = flag.String(\"kubeconfig\", \"/etc/rancher/k3s/k3s.yaml\", \"path to kubeconfig\")\n\t\tenableKubernetes = flag.Bool(\"kubernetes\", false, \"enable Kubernetes service forwarding\")\n\t\tenableDocker     = flag.Bool(\"docker\", false, \"enable Docker event monitoring\")\n\t\tenableContainerd = flag.Bool(\"containerd\", false, \"enable Containerd event monitoring\")\n\t\tcontainerdSock   = flag.String(\"containerdSock\",\n\t\t\tcontainerdSocketFile,\n\t\t\t\"file path for Containerd socket address\")\n\t\tk8sServiceListenerAddr = flag.String(\"k8sServiceListenerAddr\", net.IPv4zero.String(),\n\t\t\t\"address to bind Kubernetes services to on the host, valid options are 0.0.0.0 or 127.0.0.1\")\n\t\tadminInstall = flag.Bool(\"adminInstall\", false, \"indicates if Rancher Desktop is installed as admin or not\")\n\t\tk8sAPIPort   = flag.String(\"k8sAPIPort\", \"6443\",\n\t\t\t\"K8sAPI port number to forward to rancher-desktop wsl-proxy as a static portMapping event\")\n\t\ttapIfaceIP = flag.String(\"tap-interface-ip\", \"192.168.127.2\",\n\t\t\t\"IP address for the tap interface eth0 in network namespace\")\n\t)\n\n\t// Setup logging with debug and trace levels\n\tlogger := log.NewStandard()\n\n\tflag.Parse()\n\n\tif *debug {\n\t\tlogger.Level = log.DebugLevel\n\t}\n\n\tlog.Current = logger\n\n\tlog.Infof(\"Starting Rancher Desktop Agent in [AdminInstall=%t] mode\", *adminInstall)\n\n\tif os.Geteuid() != 0 {\n\t\tlog.Fatal(\"agent must run as root\")\n\t}\n\n\tif !*enableContainerd &&\n\t\t!*enableDocker {\n\t\tlog.Fatal(\"requires either -docker or -containerd enabled.\")\n\t}\n\n\tif *enableContainerd &&\n\t\t*enableDocker {\n\t\tlog.Fatal(\"requires either -docker or -containerd but not both.\")\n\t}\n\n\tif err := runAgent(\n\t\t*enableContainerd, *enableDocker, *enableKubernetes,\n\t\t*containerdSock, *configPath, *k8sServiceListenerAddr,\n\t\t*adminInstall, *k8sAPIPort, *tapIfaceIP,\n\t); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tlog.Info(\"Rancher Desktop Agent Shutting Down\")\n}\n\nfunc runAgent(\n\tenableContainerd, enableDocker, enableKubernetes bool,\n\tcontainerdSock, configPath, k8sServiceListenerAddr string,\n\tadminInstall bool,\n\tk8sAPIPort, tapIfaceIP string,\n) error {\n\tgroupCtx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tgroup, ctx := errgroup.WithContext(groupCtx)\n\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, syscall.SIGTERM)\n\n\tgo func() {\n\t\ts := <-sigCh\n\t\tlog.Debugf(\"received [%s] signal\", s)\n\t\tcancel()\n\t}()\n\n\tvar portTracker tracker.Tracker\n\n\twslProxyForwarder := forwarder.NewWSLProxyForwarder(ctx, \"/run/wsl-proxy.sock\")\n\tportTracker = tracker.NewAPITracker(ctx, wslProxyForwarder, tracker.GatewayBaseURL, tapIfaceIP, adminInstall)\n\t// Manually register the port for K8s API, we would\n\t// only want to send this manual port mapping if both\n\t// of the following conditions are met:\n\t// 1) if kubernetes is enabled\n\t// 2) when wsl-proxy for wsl-integration is enabled\n\tif enableKubernetes {\n\t\tport, err := nat.NewPort(\"tcp\", k8sAPIPort)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to parse port for k8s API: %w\", err)\n\t\t}\n\t\tk8sAPIPortMapping := types.PortMapping{\n\t\t\tRemove: false,\n\t\t\tPorts: nat.PortMap{\n\t\t\t\tport: []nat.PortBinding{\n\t\t\t\t\t{\n\t\t\t\t\t\tHostIP:   \"127.0.0.1\",\n\t\t\t\t\t\tHostPort: k8sAPIPort,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tif err := wslProxyForwarder.Send(k8sAPIPortMapping); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to send a static portMapping event to wsl-proxy: %w\", err)\n\t\t}\n\t\tlog.Debugf(\"successfully forwarded k8s API port [%s] to wsl-proxy\", k8sAPIPort)\n\t}\n\n\tif enableContainerd {\n\t\tgroup.Go(func() error {\n\t\t\tfor {\n\t\t\t\teventMonitor, err := containerd.NewEventMonitor(containerdSock, portTracker)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error initializing containerd event monitor: %w\", err)\n\t\t\t\t}\n\t\t\t\tif err := tryConnectAPI(ctx, containerdSocketFile, eventMonitor.IsServing); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\teventMonitor.MonitorPorts(ctx)\n\t\t\t\tif err := eventMonitor.Close(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tif enableDocker {\n\t\tgroup.Go(func() error {\n\t\t\tfor {\n\t\t\t\teventMonitor, err := docker.NewEventMonitor(portTracker)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"error initializing docker event monitor: %w\", err)\n\t\t\t\t}\n\t\t\t\tif err := tryConnectAPI(ctx, dockerSocketFile, eventMonitor.Info); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\teventMonitor.MonitorPorts(ctx)\n\t\t\t\teventMonitor.Flush()\n\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn nil\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tif enableKubernetes {\n\t\tk8sServiceListenerIP := net.ParseIP(k8sServiceListenerAddr)\n\n\t\tif k8sServiceListenerIP == nil || (!k8sServiceListenerIP.Equal(net.IPv4zero) && !k8sServiceListenerIP.Equal(net.IPv4(127, 0, 0, 1))) {\n\t\t\treturn fmt.Errorf(\"empty or invalid Kubernetes service listener IP address %s; \"+\n\t\t\t\t\"valid options are 0.0.0.0 and 127.0.0.1\", k8sServiceListenerAddr)\n\t\t}\n\n\t\tgroup.Go(func() error {\n\t\t\t// Watch for kube\n\t\t\terr := kube.WatchForServices(ctx,\n\t\t\t\tconfigPath,\n\t\t\t\tk8sServiceListenerIP,\n\t\t\t\tportTracker)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"kubernetes service watcher failed: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\n\t\tgroup.Go(func() error {\n\t\t\tiptablesScanner := iptables.NewIptablesScanner()\n\t\t\tiptablesHandler := iptables.New(ctx, portTracker, iptablesScanner, k8sServiceListenerIP, iptablesUpdateInterval)\n\t\t\terr := iptablesHandler.ForwardPorts()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"iptables port forwarding failed: %w\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tgroup.Go(func() error {\n\t\tprocScanner, err := procnet.NewProcNetScanner(ctx, portTracker, procNetScanInterval)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"scanning /proc/net/{tcp, udp} failed: %w\", err)\n\t\t}\n\t\treturn procScanner.ForwardPorts()\n\t})\n\n\treturn group.Wait()\n}\n\nfunc tryConnectAPI(ctx context.Context, socketFile string, verify func(context.Context) error) error {\n\tsocketRetry := time.NewTicker(socketInterval)\n\tdefer socketRetry.Stop()\n\t// it can potentially take a few minutes to start RD\n\tctxTimeout, cancel := context.WithTimeout(ctx, socketRetryTimeout)\n\tdefer cancel()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctxTimeout.Done():\n\t\t\treturn fmt.Errorf(\"tryConnectAPI failed: %w\", ctxTimeout.Err())\n\t\tcase <-socketRetry.C:\n\t\t\tlog.Debugf(\"checking if container engine API is running at %s\", socketFile)\n\n\t\t\tif _, err := os.Stat(socketFile); errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err := verify(ctx); err != nil {\n\t\t\t\tlog.Errorf(\"container engine is not ready yet: %v\", err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/containerd/events_linux.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package containerd handles port binding events from containerd API\npackage containerd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/containerd/containerd\"\n\t\"github.com/containerd/containerd/api/events\"\n\t\"github.com/containerd/containerd/errdefs\"\n\t\"github.com/containerd/containerd/namespaces\"\n\tcnutils \"github.com/containernetworking/plugins/pkg/utils\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"google.golang.org/protobuf/proto\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/utils\"\n)\n\nconst (\n\tnamespaceKey = \"nerdctl/namespace\"\n\tportsKey     = \"nerdctl/ports\"\n\tstateDirKey  = \"nerdctl/state-dir\"\n\tnetworkKey   = \"nerdctl/networks\"\n)\n\n// EventMonitor monitors the Containerd API\n// for container events.\ntype EventMonitor struct {\n\tcontainerdClient *containerd.Client\n\tportTracker      tracker.Tracker\n}\n\n// NewEventMonitor creates and returns a new Event Monitor for\n// Containerd API. Caller is responsible to make sure that\n// Docker engine is up and running.\nfunc NewEventMonitor(\n\tcontainerdSock string,\n\tportTracker tracker.Tracker,\n) (*EventMonitor, error) {\n\tclient, err := containerd.New(containerdSock, containerd.WithDefaultNamespace(namespaces.Default))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &EventMonitor{\n\t\tcontainerdClient: client,\n\t\tportTracker:      portTracker,\n\t}, nil\n}\n\n// MonitorPorts subscribes to event API\n// for container Create/Update/Delete events.\nfunc (e *EventMonitor) MonitorPorts(ctx context.Context) {\n\tsubscribeFilters := []string{\n\t\t`topic==\"/tasks/start\"`,\n\t\t`topic==\"/containers/update\"`,\n\t\t`topic==\"/tasks/exit\"`,\n\t}\n\tmsgCh, errCh := e.containerdClient.Subscribe(ctx, subscribeFilters...)\n\n\tgo e.initializeRunningContainers(ctx)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.Errorf(\"context cancellation: %v\", ctx.Err())\n\n\t\t\treturn\n\t\tcase envelope := <-msgCh:\n\t\t\tlog.Debugf(\"received an event: %+v\", envelope.Topic)\n\n\t\t\tswitch envelope.Topic {\n\t\t\tcase \"/tasks/start\":\n\t\t\t\tstartTask := &events.TaskStart{}\n\n\t\t\t\terr := proto.Unmarshal(envelope.Event.GetValue(), startTask)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to unmarshal container's start task: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tcontainer, err := e.containerdClient.ContainerService().Get(ctx, startTask.ContainerID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to get the container %s from namespace %s: %s\", startTask.ContainerID, envelope.Namespace, err)\n\t\t\t\t}\n\t\t\t\tports, err := createPortMappingFromContainer(container.ID, container.Labels)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to create port mapping from container's start task: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif len(ports) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\terr = execIptablesRules(ctx, ports, startTask.ContainerID, container.Labels[networkKey], envelope.Namespace, strconv.Itoa(int(startTask.Pid)))\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed running iptable rules to update DNAT rule in CNI-HOSTPORT-DNAT chain: %v\", err)\n\t\t\t\t}\n\n\t\t\t\terr = e.portTracker.Add(startTask.ContainerID, ports)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"adding port mapping to tracker failed: %v\", err)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\tcase \"/containers/update\":\n\t\t\t\tcuEvent := &events.ContainerUpdate{}\n\t\t\t\terr := proto.Unmarshal(envelope.Event.GetValue(), cuEvent)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to unmarshal container update event: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tcontainer, err := e.containerdClient.ContainerService().Get(ctx, cuEvent.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to get the container %s from namespace %s: %s\", cuEvent.ID, envelope.Namespace, err)\n\t\t\t\t}\n\n\t\t\t\tports, err := createPortMappingFromContainer(container.ID, container.Labels)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to create port mapping from container's start task: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tif len(ports) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\texistingPortMap := e.portTracker.Get(cuEvent.ID)\n\t\t\t\tif existingPortMap != nil {\n\t\t\t\t\tif !reflect.DeepEqual(ports, existingPortMap) {\n\t\t\t\t\t\terr := e.portTracker.Remove(cuEvent.ID)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Errorf(\"failed to remove port mapping from container update event: %v\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr = e.portTracker.Add(cuEvent.ID, ports)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tlog.Errorf(\"failed to add port mapping from container update event: %v\", err)\n\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// Not 100% sure if we ever get here...\n\t\t\t\tif err = e.portTracker.Add(cuEvent.ID, ports); err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to add port mapping from container update event: %v\", err)\n\t\t\t\t}\n\n\t\t\tcase \"/tasks/exit\":\n\t\t\t\texitTask := &events.TaskExit{}\n\t\t\t\terr := proto.Unmarshal(envelope.Event.GetValue(), exitTask)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to unmarshal container's exit task: %v\", err)\n\t\t\t\t}\n\n\t\t\t\tcontainer, err := e.containerdClient.LoadContainer(ctx, exitTask.ContainerID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errdefs.IsNotFound(err) {\n\t\t\t\t\t\tlog.Debugf(\"container: %s in namespace: %s not found, deleting port mapping\", exitTask.ContainerID, envelope.Namespace)\n\t\t\t\t\t\te.removePortMapping(exitTask.ContainerID)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tlog.Errorf(\"failed to get the container %s from namespace %s: %s\", exitTask.ContainerID, envelope.Namespace, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ttsk, err := container.Task(ctx, nil)\n\t\t\t\tif err != nil {\n\t\t\t\t\tif errdefs.IsNotFound(err) {\n\t\t\t\t\t\tlog.Debugf(\"task for container %s in namespace %s not found, deleting port mapping\", exitTask.ContainerID, envelope.Namespace)\n\t\t\t\t\t\te.removePortMapping(exitTask.ContainerID)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tlog.Errorf(\"failed to get the task for container %s: %s\", exitTask.ContainerID, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tstatus, err := tsk.Status(ctx)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to get the task status for container %s: %s\", exitTask.ContainerID, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif status.Status == containerd.Running {\n\t\t\t\t\tlog.Debugf(\"container %s is still running, but received exit event with status %d\", exitTask.ContainerID, exitTask.ExitStatus)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\te.removePortMapping(exitTask.ContainerID)\n\t\t\t}\n\n\t\tcase err := <-errCh:\n\t\t\tlog.Errorf(\"receiving container event failed: %v\", err)\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// IsServing returns true if the client can successfully connect to the\n// containerd daemon and the healthcheck service returns the SERVING\n// response.\n// This call will block if a transient error is encountered during\n// connection. A timeout can be set in the context to ensure it returns\n// early.\nfunc (e *EventMonitor) IsServing(ctx context.Context) error {\n\tserving, err := e.containerdClient.IsServing(ctx)\n\tif serving {\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"containerd API is not serving: %w\", err)\n}\n\n// initializeRunningContainers calls the API to get a list of all existing\n// containers. If the port monitoring misses any /tasks/start events during\n// startup or due to timing issues, this acts as a backup to capture all\n// previously running containers.\nfunc (e *EventMonitor) initializeRunningContainers(ctx context.Context) {\n\tcontainers, err := e.containerdClient.Containers(ctx)\n\tif err != nil {\n\t\tlog.Errorf(\"failed getting containers: %s\", err)\n\t\treturn\n\t}\n\tfor _, c := range containers {\n\t\t// skip already added containers\n\t\tif len(e.portTracker.Get(c.ID())) != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tt, err := c.Task(ctx, nil)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed getting container %s task: %s\", c.ID(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\tstatus, err := t.Status(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed getting container %s task status: %s\", c.ID(), err)\n\t\t\tcontinue\n\t\t}\n\t\tif status.Status != containerd.Running {\n\t\t\tcontinue\n\t\t}\n\t\tlabels, err := c.Labels(ctx)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed getting container %s labels: %s\", c.ID(), err)\n\t\t\tcontinue\n\t\t}\n\n\t\tports, err := createPortMappingFromContainer(c.ID(), labels)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed to create port mapping for container %s: %v\", c.ID(), err)\n\t\t}\n\t\tif len(ports) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\terr = execIptablesRules(ctx, ports, c.ID(), labels[networkKey], labels[namespaceKey], strconv.Itoa(int(t.Pid())))\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"failed running iptable rules to update DNAT rule in CNI-HOSTPORT-DNAT chain: %v\", err)\n\t\t}\n\n\t\terr = e.portTracker.Add(c.ID(), ports)\n\t\tif err != nil {\n\t\t\tlog.Errorf(\"adding port mapping to tracker failed: %v\", err)\n\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Debugf(\"initialized container %s task status: %+v with ports: %+v\", c.ID(), status, ports)\n\t}\n}\n\n// Close closes the client connection to the API server.\nfunc (e *EventMonitor) Close() error {\n\tvar finalErr error\n\n\tif err := e.containerdClient.Close(); err != nil {\n\t\tfinalErr = fmt.Errorf(\"failed to close containerd client: %w\", err)\n\t}\n\n\tif err := e.portTracker.RemoveAll(); err != nil {\n\t\tfinalErr = fmt.Errorf(\"failed to remove all ports from port tracker: %w\", err)\n\t}\n\n\treturn finalErr\n}\n\n// execIptablesRules creates an additional DNAT rule to allow service exposure on\n// other network addresses if port binding is bound to 127.0.0.1.\nfunc execIptablesRules(ctx context.Context, portMappings nat.PortMap, containerID, networks, namespace, pid string) error {\n\tvar errs []error\n\n\tvar containerNetworks []string\n\terr := json.Unmarshal([]byte(networks), &containerNetworks)\n\tif err != nil {\n\t\terrs = append(errs, fmt.Errorf(\"unmarshaling container networks: %w\", err))\n\t\treturn errors.Join(errs...)\n\t}\n\tfor portProto, portBindings := range portMappings {\n\t\tfor _, portBinding := range portBindings {\n\t\t\tif portBinding.HostIP == \"127.0.0.1\" {\n\t\t\t\terr := createLoopbackIPtablesRules(\n\t\t\t\t\tctx,\n\t\t\t\t\tcontainerNetworks,\n\t\t\t\t\tcontainerID,\n\t\t\t\t\tnamespace,\n\t\t\t\t\tpid,\n\t\t\t\t\tportProto.Port(),\n\t\t\t\t\tportProto.Proto(),\n\t\t\t\t\tportBinding.HostPort)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(errs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", utils.ErrExecIptablesRule, errs)\n\t}\n\n\treturn nil\n}\n\n// When the port binding is set to 127.0.0.1, an additional DNAT rule is added to the main\n// CNI DNAT chain (CNI-HOSTPORT-DNAT) after the existing rule (using --append). This is necessary\n// because the initial CNI rule created by containerd only allows traffic to be routed to localhost.\n// To make the service discoverable via the namespaced network's subnet, an additional rule is added\n// to allow traffic to any destination IP address. This effectively causes the service to listen on\n// the eth0 interface instead of localhost, which is required since the traffic is routed through the\n// vm-switch over the tap network.\n//\n// The existing DNAT rule is as follows:\n//\n//\tDNAT       tcp  --  anywhere             localhost            tcp dpt:9119 to:10.4.0.22:80.\n//\n// After the existing rule, the following new rule is added:\n//\n//\tDNAT       tcp  --  anywhere             anywhere             tcp dpt:9119 to:10.4.0.22:80.\nfunc createLoopbackIPtablesRules(ctx context.Context, networks []string, containerID, namespace, pid, port, protocol, destinationPort string) error {\n\teth0IP, err := extractIPAddress(ctx, pid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Debugf(\"found the ip address: %s for containerID: %s\", eth0IP, containerID)\n\tcID := fmt.Sprintf(\"%s-%s\", namespace, containerID)\n\n\tvar allErrs []error\n\n\t// Run the rule per network\n\tfor _, network := range networks {\n\t\tchainName := cnutils.MustFormatChainNameWithPrefix(network, cID, \"DN-\")\n\n\t\t// Instead of modifying the existing rule, a new rule is added that overrides the previous one.\n\t\t// The original rule only allows traffic from anywhere to localhost, but the new rule permits traffic\n\t\t// from anywhere to anywhere. The new rule is appended below the existing one in the chain, ensuring\n\t\t// that traffic is correctly routed to the specified destination.\n\t\t//\n\t\t// Example of the new rule:\n\t\t//   iptables -t nat -A CNI-DN-xxxxxx -p tcp -d 0.0.0.0/0 -j DNAT --dport 9119 --to-destination 10.4.0.10:80\n\t\t//\n\t\t// IMPORTANT: Unlike the Docker events API, we do not attempt to delete the rules we create. This is due\n\t\t// to how containerd manages CNI chains. Specifically, containerd deletes the entire CNI chain (e.g., CNI-DN-xxxxxx)\n\t\t// when a container exits or is deleted, which automatically removes any rules appended during container startup.\n\t\tiptableCmd := exec.CommandContext(ctx,\n\t\t\t\"iptables\",\n\t\t\t\"--table\", \"nat\",\n\t\t\t\"--append\", chainName,\n\t\t\t\"--protocol\", protocol,\n\t\t\t\"--destination\", \"0.0.0.0/0\",\n\t\t\t\"--jump\", \"DNAT\",\n\t\t\t\"--dport\", destinationPort,\n\t\t\t\"--to-destination\", fmt.Sprintf(\"%s:%s\", eth0IP, port))\n\t\tvar stderr bytes.Buffer\n\t\tiptableCmd.Stderr = &stderr\n\t\tif err := iptableCmd.Run(); err != nil {\n\t\t\tallErrs = append(allErrs, fmt.Errorf(\"running iptables rule [%s] failed: %w - %s\", iptableCmd.String(), err, stderr.String()))\n\t\t}\n\t\tlog.Debugf(\"running the following loopback rule [%s] in chain: %s for containerID: %s\", iptableCmd.String(), chainName, containerID)\n\t}\n\n\tif len(allErrs) != 0 {\n\t\treturn errors.Join(allErrs...)\n\t}\n\treturn nil\n}\n\nfunc createPortMappingFromContainer(id string, labels map[string]string) (nat.PortMap, error) {\n\tvar err error\n\tvar data struct {\n\t\tPortMappings []Port `json:\"portMappings\"`\n\t}\n\tportMap := make(nat.PortMap)\n\n\tportString := labels[portsKey]\n\tif portString != \"\" {\n\t\terr = json.Unmarshal([]byte(portString), &data.PortMappings)\n\t\tif err != nil {\n\t\t\tlog.Warnf(\"failed to read container %s port mappings label: %w\", id, err)\n\t\t}\n\t}\n\tif portString == \"\" || err != nil {\n\t\tif stateDir := labels[stateDirKey]; stateDir != \"\" {\n\t\t\tvar configBytes []byte\n\t\t\tconfigBytes, err = os.ReadFile(filepath.Join(stateDir, \"network-config.json\"))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed reading network state for container %s: %w\", id, err)\n\t\t\t}\n\t\t\terr = json.Unmarshal(configBytes, &data)\n\t\t}\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, port := range data.PortMappings {\n\t\tportMapKey, err := nat.NewPort(strings.ToLower(port.Protocol), strconv.Itoa(port.ContainerPort))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tportBinding := nat.PortBinding{\n\t\t\tHostIP:   utils.NormalizeHostIP(port.HostIP),\n\t\t\tHostPort: strconv.Itoa(port.HostPort),\n\t\t}\n\t\tif pb, ok := portMap[portMapKey]; ok {\n\t\t\tportMap[portMapKey] = append(pb, portBinding)\n\t\t} else {\n\t\t\tportMap[portMapKey] = []nat.PortBinding{portBinding}\n\t\t}\n\t}\n\n\treturn portMap, nil\n}\n\nfunc extractIPAddress(ctx context.Context, pid string) (string, error) {\n\t// retrieve the eth0 IP address from the container\n\tnsenterInfIPCmd := exec.CommandContext(ctx, \"nsenter\", \"-t\", pid, \"-n\", \"ip\", \"-o\", \"-4\", \"addr\", \"show\", \"dev\", \"eth0\")\n\toutput, err := nsenterInfIPCmd.CombinedOutput()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\t// Regular expression pattern to match the IP address\n\trx := regexp.MustCompile(`\\binet\\s+(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})/\\d{1,2}`)\n\n\tmatches := rx.FindStringSubmatch(string(output))\n\tsegments := 2\n\tif len(matches) < segments {\n\t\treturn \"\", utils.ErrIPAddressNotFound\n\t}\n\n\treturn matches[1], nil\n}\n\nfunc (e *EventMonitor) removePortMapping(containerID string) {\n\tif portMap := e.portTracker.Get(containerID); portMap != nil {\n\t\tif err := e.portTracker.Remove(containerID); err != nil {\n\t\t\tlog.Errorf(\"failed to remove port mapping for %s: %v\", containerID, err)\n\t\t}\n\t}\n}\n\n// Port is representing nerdctl/ports entry in the\n// event envelope's labels.\ntype Port struct {\n\tHostPort      int\n\tContainerPort int\n\tProtocol      string\n\tHostIP        string\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/containerd/events_stub.go",
    "content": "//go:build !linux\n\n/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage containerd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n)\n\ntype EventMonitor struct {\n}\n\nfunc NewEventMonitor(containerdSock string, portTracker tracker.Tracker) (*EventMonitor, error) {\n\tpanic(\"not implement for non-Linux\")\n}\n\nfunc (e *EventMonitor) MonitorPorts(ctx context.Context) {\n\tpanic(\"not implement for non-Linux\")\n}\n\nfunc (e *EventMonitor) IsServing(ctx context.Context) error {\n\treturn fmt.Errorf(\"not implemented for non-Linux\")\n}\n\nfunc (e *EventMonitor) Close() error {\n\treturn fmt.Errorf(\"not implemented for non-Linux\")\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/docker/events.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package docker handles port binding events from docker events API\npackage docker\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/docker/api/types\"\n\tcontainerapi \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/docker/api/types/events\"\n\t\"github.com/docker/docker/api/types/filters\"\n\t\"github.com/docker/docker/client\"\n\t\"github.com/docker/go-connections/nat\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/utils\"\n)\n\n// EventMonitor monitors the Docker engine's Event API\n// for container events.\ntype EventMonitor struct {\n\tdockerClient *client.Client\n\tportTracker  tracker.Tracker\n\t// map of containerID to iptables rule entry to remove from DOCKER chain\n\tiptablesRulesToDelete map[string]*exec.Cmd\n}\n\n// NewEventMonitor creates and returns a new Event Monitor for\n// Docker's event API. Caller is responsible to make sure that\n// Docker engine is up and running.\nfunc NewEventMonitor(portTracker tracker.Tracker) (*EventMonitor, error) {\n\tcli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &EventMonitor{\n\t\tdockerClient:          cli,\n\t\tportTracker:           portTracker,\n\t\tiptablesRulesToDelete: make(map[string]*exec.Cmd),\n\t}, nil\n}\n\n// MonitorPorts scans Docker's event stream API\n// for container start/stop events.\nfunc (e *EventMonitor) MonitorPorts(ctx context.Context) {\n\tmsgCh, errCh := e.dockerClient.Events(ctx, events.ListOptions{\n\t\tFilters: filters.NewArgs(\n\t\t\tfilters.Arg(\"type\", string(types.ContainerObject)),\n\t\t\tfilters.Arg(\"event\", string(events.ActionStart)),\n\t\t\tfilters.Arg(\"event\", string(events.ActionStop)),\n\t\t\tfilters.Arg(\"event\", string(events.ActionDie))),\n\t})\n\n\tif err := e.initializeRunningContainers(ctx); err != nil {\n\t\tlog.Errorf(\"failed to initialize existing container port mappings: %s\", err)\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlog.Errorf(\"context cancellation: %s\", ctx.Err())\n\n\t\t\treturn\n\t\tcase event := <-msgCh:\n\t\t\tcontainer, err := e.dockerClient.ContainerInspect(ctx, event.Actor.ID)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"inspecting container [%v] failed: %s\", event.Actor.ID, err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Debugf(\"received an event: {Status: %+v ContainerID: %+v Ports: %+v}\",\n\t\t\t\tevent.Action,\n\t\t\t\tevent.Actor.ID,\n\t\t\t\tcontainer.NetworkSettings.Ports)\n\n\t\t\tswitch event.Action {\n\t\t\tcase events.ActionStart:\n\t\t\t\tif len(container.NetworkSettings.Ports) != 0 {\n\t\t\t\t\tvalidatePortMapping(container.NetworkSettings.Ports)\n\t\t\t\t\terr = e.portTracker.Add(container.ID, container.NetworkSettings.Ports)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Errorf(\"adding port mapping to tracker failed: %s\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\te.createIptablesRuleForContainer(ctx, container)\n\t\t\t\t}\n\t\t\tcase events.ActionStop, events.ActionDie:\n\t\t\t\terr := e.portTracker.Remove(container.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"remove port mapping from tracker failed: %s\", err)\n\t\t\t\t}\n\t\t\t\tif deleteIptablesCmd, ok := e.iptablesRulesToDelete[container.ID]; ok {\n\t\t\t\t\tlog.Debugf(\"removing the following rules from iptables: %s\", deleteIptablesCmd.String())\n\t\t\t\t\tvar stderr bytes.Buffer\n\t\t\t\t\tdeleteIptablesCmd.Stderr = &stderr\n\t\t\t\t\tif err := deleteIptablesCmd.Run(); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"deleting loopback iptables rule failed: %s [%s]\", err, stderr.String())\n\t\t\t\t\t}\n\t\t\t\t\tdelete(e.iptablesRulesToDelete, container.ID)\n\t\t\t\t}\n\t\t\t}\n\t\tcase err := <-errCh:\n\t\t\tlog.Errorf(\"receiving container event failed: %s\", err)\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// Flush clears all the container port mappings\n// out of the port tracker upon shutdown.\nfunc (e *EventMonitor) Flush() {\n\terr := e.portTracker.RemoveAll()\n\tif err != nil {\n\t\tlog.Errorf(\"Flush received an error to remove all portMappings: %v\", err)\n\t}\n}\n\n// Info returns information about the docker server\n// it is used to verify that docker engine server is up.\nfunc (e *EventMonitor) Info(ctx context.Context) error {\n\t_, err := e.dockerClient.Info(ctx)\n\n\treturn err\n}\n\nfunc (e *EventMonitor) initializeRunningContainers(ctx context.Context) error {\n\tcontainers, err := e.dockerClient.ContainerList(ctx, containerapi.ListOptions{\n\t\tFilters: filters.NewArgs(filters.Arg(\"status\", \"running\")),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range containers {\n\t\tcontainer := &containers[i]\n\t\tif len(container.Ports) != 0 {\n\t\t\tportMap, err := createPortMapping(container.Ports)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"creating initial port mapping failed: %v\", err)\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := e.portTracker.Add(container.ID, portMap); err != nil {\n\t\t\t\tlog.Errorf(\"registering already running containers failed: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, netSettings := range container.NetworkSettings.Networks {\n\t\t\t\terr = e.createLoopbackIPtablesRules(ctx, container.ID, netSettings.IPAddress, portMap)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Errorf(\"creating iptable rules to update DNAT rule in DOCKER chain during container initialization failed: %v\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc createPortMapping(ports []containerapi.Port) (nat.PortMap, error) {\n\tportMap := make(nat.PortMap)\n\n\tfor _, port := range ports {\n\t\tif port.IP == \"\" || port.PublicPort == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tportMapKey, err := nat.NewPort(strings.ToLower(port.Type), strconv.Itoa(int(port.PrivatePort)))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tportBinding := nat.PortBinding{\n\t\t\tHostIP:   utils.NormalizeHostIP(port.IP),\n\t\t\tHostPort: strconv.Itoa(int(port.PublicPort)),\n\t\t}\n\n\t\tif pb, ok := portMap[portMapKey]; ok {\n\t\t\tportMap[portMapKey] = append(pb, portBinding)\n\t\t} else {\n\t\t\tportMap[portMapKey] = []nat.PortBinding{portBinding}\n\t\t}\n\t}\n\n\treturn portMap, nil\n}\n\n// Removes entries in port mapping that do not hold any values\n// for IP and Port e.g 9000/tcp:[].\nfunc validatePortMapping(portMap nat.PortMap) {\n\tfor k, v := range portMap {\n\t\tif len(v) == 0 {\n\t\t\tlog.Debugf(\"removing entry: %v from the portmappings: %v\", k, portMap)\n\t\t\tdelete(portMap, k)\n\t\t}\n\t}\n}\n\n// When the port binding is bound to 127.0.0.1, an additional DNAT rule is added to the\n// main DOCKER chain after the existing rule (using --append). This is necessary because\n// the initial DOCKER DNAT rule created by Docker only allows traffic to be routed to\n// localhost from localhost. To make the service discoverable through the namespaced\n// network's subnet, an additional rule is added to allow traffic to any destination IP\n// address.\n//\n// This is required because traffic is routed via the vm-switch over the tap network.\n//\n// The existing DNAT rule is as follows:\n//\n//\tDNAT       tcp  --  anywhere             localhost            tcp dpt:9119 to:10.4.0.22:80.\n//\n// The following rule is entered after the existing rule:\n//\n//\tDNAT       tcp  --  anywhere             anywhere             tcp dpt:9119 to:10.4.0.22:80.\nfunc (e *EventMonitor) createLoopbackIPtablesRules(ctx context.Context, containerID, containerIP string, portMappings nat.PortMap) error {\n\tvar errs []error\n\n\tfor portProto, portBindings := range portMappings {\n\t\tfor _, portBinding := range portBindings {\n\t\t\tif portBinding.HostIP != \"127.0.0.1\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t//nolint:gosec // no security concern with the potentially tainted command arguments\n\t\t\tiptableCmd := exec.CommandContext(ctx,\n\t\t\t\t\"iptables\",\n\t\t\t\t\"--table\", \"nat\",\n\t\t\t\t\"--append\", \"DOCKER\",\n\t\t\t\t\"--protocol\", portProto.Proto(),\n\t\t\t\t\"--destination\", \"0.0.0.0/0\",\n\t\t\t\t\"--jump\", \"DNAT\",\n\t\t\t\t\"--dport\", portBinding.HostPort,\n\t\t\t\t\"--to-destination\", fmt.Sprintf(\"%s:%s\", containerIP, portProto.Port()))\n\t\t\tvar stderr bytes.Buffer\n\t\t\tiptableCmd.Stderr = &stderr\n\t\t\tif err := iptableCmd.Run(); err != nil {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"creating loopback rule in DOCKER chain failed: %w [%s]\", err, stderr.String()))\n\t\t\t\tlog.Debugf(\"running the following iptables rule [%s] with the error(s):[%v]\", iptableCmd.String(), errs)\n\t\t\t}\n\t\t\te.iptablesRulesToDelete[containerID] = iptablesDeleteLoopbackRuleCmd(\n\t\t\t\tctx,\n\t\t\t\tportProto.Proto(),\n\t\t\t\tportBinding.HostPort,\n\t\t\t\tfmt.Sprintf(\"%s:%s\", containerIP, portProto.Port()))\n\t\t}\n\t}\n\n\tif len(errs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", utils.ErrExecIptablesRule, errs)\n\t}\n\n\treturn nil\n}\n\nfunc (e *EventMonitor) createIptablesRuleForContainer(ctx context.Context, container containerapi.InspectResponse) {\n\t// If the container's NetworkSettings.Networks map is not empty, it indicates that the container\n\t// is connected to a Docker Compose network. In this case, we should inspect the map and\n\t// configure the loopback address for each container's assigned IP address.\n\tif len(container.NetworkSettings.Networks) != 0 {\n\t\t// delete the IPv6 rule first\n\t\tif err := deleteComposeNetworkIPv6Rule(ctx, container.NetworkSettings.Ports); err != nil {\n\t\t\tlog.Errorf(\"removing docker compose IPv6 rule from DOCKER chain failed: %v\", err)\n\t\t}\n\t\tfor networkName, network := range container.NetworkSettings.Networks {\n\t\t\terr := e.createLoopbackIPtablesRules(\n\t\t\t\tctx,\n\t\t\t\tcontainer.ID,\n\t\t\t\tnetwork.IPAddress,\n\t\t\t\tcontainer.NetworkSettings.Ports)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"creating iptable rules to update DNAT rule in DOCKER chain for docker compose network: %s failed: %v\", networkName, err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tbridgeNetwork, ok := container.NetworkSettings.Networks[\"bridge\"]\n\t\tif !ok {\n\t\t\tlog.Errorf(\"creating iptable rules to update DNAT rule in DOCKER chain failed: failed to find bridge network\")\n\t\t} else {\n\t\t\terr := e.createLoopbackIPtablesRules(\n\t\t\t\tctx,\n\t\t\t\tcontainer.ID,\n\t\t\t\tbridgeNetwork.IPAddress,\n\t\t\t\tcontainer.NetworkSettings.Ports)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"creating iptable rules to update DNAT rule in DOCKER chain failed: %v\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc iptablesDeleteLoopbackRuleCmd(ctx context.Context, protocol, dport, toDestination string) *exec.Cmd {\n\treturn exec.CommandContext(ctx,\n\t\t\"iptables\",\n\t\t\"--table\", \"nat\",\n\t\t\"--delete\", \"DOCKER\",\n\t\t\"--protocol\", protocol,\n\t\t\"--destination\", \"0.0.0.0/0\",\n\t\t\"--jump\", \"DNAT\",\n\t\t\"--dport\", dport,\n\t\t\"--to-destination\", toDestination)\n}\n\n// Docker Compose, by default, creates the following rules in the DOCKER chain:\n//\n//\tDNAT       tcp  --  anywhere             localhost            tcp dpt:80 to:172.18.0.2:80\n//\tDNAT       tcp  --  anywhere             anywhere             tcp dpt:80 to::80\n//\n// The second rule can be problematic because it uses a wildcard IPv6 address (`::`), which can match\n// any incoming TCP traffic destined for port 80. Since there may be no service listening on IPv6,\n// this can lead to a TCP RST (reset) response sent back to the client.\n//\n// To prevent this issue, we must delete the IPv6 rule before adding our following custom rule:\n//\n//\tDNAT       tcp  --  anywhere             anywhere             tcp dpt:80 to:172.18.0.2:80\n//\n// Note: Even if the `enable_ipv6` property is set to `false` in Docker's compose configuration,\n// Docker still creates the wildcard IPv6 rule in iptables. Therefore, we need to manually\n// remove it to avoid this issue.\nfunc deleteComposeNetworkIPv6Rule(ctx context.Context, portMappings nat.PortMap) error {\n\tvar errs []error\n\n\tfor portProto, portBindings := range portMappings {\n\t\tfor _, portBinding := range portBindings {\n\t\t\tif portBinding.HostIP == \"127.0.0.1\" {\n\t\t\t\t//nolint:gosec // Inputs are fixed strings or numbers.\n\t\t\t\tiptableComposeDeleteCmd := exec.CommandContext(ctx,\n\t\t\t\t\t\"iptables\",\n\t\t\t\t\t\"--table\", \"nat\",\n\t\t\t\t\t\"--delete\", \"DOCKER\",\n\t\t\t\t\t\"--protocol\", portProto.Proto(),\n\t\t\t\t\t\"--jump\", \"DNAT\",\n\t\t\t\t\t\"--dport\", portBinding.HostPort,\n\t\t\t\t\t\"--to-destination\", fmt.Sprintf(\":%s\", portProto.Port()))\n\t\t\t\tvar stderr bytes.Buffer\n\t\t\t\tiptableComposeDeleteCmd.Stderr = &stderr\n\t\t\t\tif err := iptableComposeDeleteCmd.Run(); err != nil {\n\t\t\t\t\terrs = append(errs, fmt.Errorf(\"%w [%s]\", err, stderr.String()))\n\t\t\t\t\tlog.Debugf(\"running the following iptables rule [%s] with the error(s):[%v]\", iptableComposeDeleteCmd.String(), errs)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(errs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", utils.ErrExecIptablesRule, errs)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/forwarder/forwarder.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package forwarder implements a forwarding mechanism to forward\n// port mappings over the network.\npackage forwarder\n\nimport (\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n)\n\n// Forwarder is the interface that wraps the Send method which\n// to forward the port mappings.\ntype Forwarder interface {\n\t// Send sends the give port mappings to the Peer via\n\t// a tcp connection.\n\tSend(portMapping types.PortMapping) error\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/forwarder/serviceapi.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage forwarder\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n)\n\nconst (\n\texposeAPI   = \"/services/forwarder/expose\"\n\tunexposeAPI = \"/services/forwarder/unexpose\"\n)\n\nvar (\n\tErrAPI         = errors.New(\"error from API\")\n\tErrExposeAPI   = fmt.Errorf(\"error from %s API\", exposeAPI)\n\tErrUnexposeAPI = fmt.Errorf(\"error from %s API\", unexposeAPI)\n)\n\n// APIForwarder forwards the PortMappings to /services/forwarder/expose\n// or /services/forwarder/unexpose that is host in the host-switch.\ntype APIForwarder struct {\n\tbaseURL    string\n\thttpClient *http.Client\n}\n\n// NewAPIForwarder returns a new instance of APIForwarder.\nfunc NewAPIForwarder(baseURL string) *APIForwarder {\n\treturn &APIForwarder{\n\t\tbaseURL:    baseURL,\n\t\thttpClient: http.DefaultClient,\n\t}\n}\n\n// Expose calls /services/forwarder/expose with a given portMappings.\nfunc (a *APIForwarder) Expose(exposeReq *types.ExposeRequest) error {\n\tbin, err := json.Marshal(exposeReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Debugf(\"sending a HTTP POST to %s API with expose request: %v\", exposeAPI, exposeReq)\n\treq, err := http.NewRequestWithContext(\n\t\tcontext.Background(),\n\t\thttp.MethodPost,\n\t\ta.urlBuilder(exposeAPI),\n\t\tbytes.NewReader(bin))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := a.httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn verifyResponseBody(res)\n}\n\n// Unexpose calls /services/forwarder/unexpose with a given portMappings.\nfunc (a *APIForwarder) Unexpose(unexposeReq *types.UnexposeRequest) error {\n\tbin, err := json.Marshal(unexposeReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Debugf(\"sending a HTTP POST to %s API with unexpose request: %v\", unexposeAPI, unexposeReq)\n\treq, err := http.NewRequestWithContext(\n\t\tcontext.Background(),\n\t\thttp.MethodPost,\n\t\ta.urlBuilder(unexposeAPI),\n\t\tbytes.NewReader(bin))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := a.httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn verifyResponseBody(res)\n}\n\nfunc (a *APIForwarder) urlBuilder(api string) string {\n\treturn a.baseURL + api\n}\n\nfunc verifyResponseBody(res *http.Response) error {\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\tapiResponse, readErr := io.ReadAll(res.Body)\n\t\tif readErr != nil {\n\t\t\treturn fmt.Errorf(\"error while reading response body: %w\", readErr)\n\t\t}\n\n\t\terrMsg := strings.TrimSpace(string(apiResponse))\n\n\t\treturn fmt.Errorf(\"%w: %s\", ErrAPI, errMsg)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/forwarder/wslproxy.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package forwarder implements a forwarding mechanism to forward\n// port mappings to Rancher Desktop WSL Proxy.\npackage forwarder\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n)\n\n// WSLProxyForwarder forwards the PortMappings to Rancher Desktop WSLProxy process in\n// the default namespace over the unix socket.\n// For more information on Rancher Desktop WSL Proxy, refer to the source code at:\n// https://github.com/rancher-sandbox/rancher-desktop/blob/main/src/go/networking/cmd/proxy/wsl_integration_linux.go\ntype WSLProxyForwarder struct {\n\tctx         context.Context\n\tdialer      net.Dialer\n\tproxySocket string\n}\n\nfunc NewWSLProxyForwarder(ctx context.Context, proxySocket string) *WSLProxyForwarder {\n\treturn &WSLProxyForwarder{\n\t\tctx:         ctx,\n\t\tdialer:      net.Dialer{Timeout: 5 * time.Second},\n\t\tproxySocket: proxySocket,\n\t}\n}\n\n// Send forwards the port mappings to WSL Proxy.\nfunc (v *WSLProxyForwarder) Send(portMapping types.PortMapping) error {\n\tconn, err := v.dialer.DialContext(v.ctx, \"unix\", v.proxySocket)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\n\terr = json.NewEncoder(conn).Encode(portMapping)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/iptables/iptables.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package iptables handles forwarding ports found in iptables DNAT\npackage iptables\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/go-connections/nat\"\n\tlimaiptables \"github.com/lima-vm/lima/pkg/guestagent/iptables\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/utils\"\n)\n\n// Iptables manages port forwarding for ports identified in iptables DNAT rules.\n// It is primarily responsible for handling port mappings in Kubernetes environments that\n// are not exposed via the Kubernetes API. The package scans iptables for these port and uses\n// the k8sServiceListenerAddr setting for the hostIP property to create a port mapping and\n// forwards them to both the API tracker and the WSL Proxy for proper routing and handling.\ntype Iptables struct {\n\tcontext    context.Context\n\tapiTracker tracker.Tracker\n\tscanner    Scanner\n\tlistenerIP net.IP\n\t// time, in seconds, to wait between updating.\n\tupdateInterval time.Duration\n}\n\nfunc New(ctx context.Context, apiTracker tracker.Tracker, iptablesScanner Scanner, listenerIP net.IP, updateInterval time.Duration) *Iptables {\n\treturn &Iptables{\n\t\tcontext:        ctx,\n\t\tapiTracker:     apiTracker,\n\t\tscanner:        iptablesScanner,\n\t\tlistenerIP:     listenerIP,\n\t\tupdateInterval: updateInterval,\n\t}\n}\n\n// ForwardPorts forwards ports found in iptables DNAT. In some environments,\n// like WSL, ports defined using the CNI portmap plugin happen through iptables.\n// These ports are not sent to places like /proc/net/tcp and are not picked up\n// as part of the normal forwarding system. This function detects those ports\n// and binds them to k8sServiceListenerAddr so that they are picked up.\nfunc (i *Iptables) ForwardPorts() error {\n\tvar ports []limaiptables.Entry\n\n\tticker := time.NewTicker(i.updateInterval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-i.context.Done():\n\t\t\treturn nil\n\t\tcase <-ticker.C:\n\t\t}\n\t\t// Detect ports for forward\n\t\tnewPorts, err := i.scanner.GetPorts()\n\t\tif err != nil {\n\t\t\t// iptables exiting with an exit status of 4 means there\n\t\t\t// is a resource problem. For example, something else is\n\t\t\t// running iptables. In that case, we can skip trying it for\n\t\t\t// this loop. You can find the exit code in the iptables\n\t\t\t// source at https://git.netfilter.org/iptables/tree/include/xtables.h\n\t\t\tif strings.Contains(err.Error(), \"exit status 4\") {\n\t\t\t\tlog.Debug(\"iptables exited with status 4 (resource error). Retrying...\")\n\t\t\t\tcontinue // Retry in the next iteration\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\t// Diff from existing forwarded ports\n\t\tadded, removed := comparePorts(ports, newPorts)\n\t\tports = newPorts\n\n\t\t// Remove old forwards\n\t\tfor _, p := range removed {\n\t\t\tname := entryToString(p)\n\t\t\tif err := i.apiTracker.Remove(utils.GenerateID(name)); err != nil {\n\t\t\t\tlog.Warnf(\"iptables scanner failed to remove portmap for %s: %w\", name, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Infof(\"iptables scanner removed portmap for %s\", name)\n\t\t}\n\n\t\tportMap := make(nat.PortMap)\n\n\t\t// Add new forwards\n\t\tfor _, p := range added {\n\t\t\tif !p.TCP {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tport := strconv.Itoa(p.Port)\n\t\t\tportMapKey, err := nat.NewPort(\"tcp\", port)\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to create a corresponding key for the portMap: %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tportBinding := nat.PortBinding{\n\t\t\t\tHostIP:   i.listenerIP.String(),\n\t\t\t\tHostPort: port,\n\t\t\t}\n\t\t\tif _, ok := portMap[portMapKey]; !ok {\n\t\t\t\tportMap[portMapKey] = []nat.PortBinding{portBinding}\n\t\t\t}\n\t\t\tname := entryToString(p)\n\t\t\tif err := i.apiTracker.Add(utils.GenerateID(name), portMap); err != nil {\n\t\t\t\tlog.Errorf(\"iptables scanner failed to forward portmap for %s: %s\", name, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Infof(\"iptables scanner forwarded portmap for %s\", name)\n\t\t}\n\t}\n}\n\n// comparePorts compares the old and new ports to find those added or removed.\n// This function is mostly lifted from lima (github.com/lima-vm/lima) which is\n// licensed under the Apache 2.\nfunc comparePorts(oldPorts, newPorts []limaiptables.Entry) ([]limaiptables.Entry, []limaiptables.Entry) {\n\tvar added, removed []limaiptables.Entry\n\toldPortMap := make(map[string]limaiptables.Entry, len(oldPorts))\n\tportExistMap := make(map[string]bool, len(oldPorts))\n\tfor _, oldPort := range oldPorts {\n\t\tkey := entryToString(oldPort)\n\t\toldPortMap[key] = oldPort\n\t\tportExistMap[key] = false\n\t}\n\tfor _, newPort := range newPorts {\n\t\tkey := entryToString(newPort)\n\t\tportExistMap[key] = true\n\t\tif _, ok := oldPortMap[key]; !ok {\n\t\t\tadded = append(added, newPort)\n\t\t}\n\t}\n\tfor k, stillExist := range portExistMap {\n\t\tif !stillExist {\n\t\t\tif entry, ok := oldPortMap[k]; ok {\n\t\t\t\tremoved = append(removed, entry)\n\t\t\t}\n\t\t}\n\t}\n\treturn added, removed\n}\n\nfunc entryToString(ip limaiptables.Entry) string {\n\treturn net.JoinHostPort(ip.IP.String(), strconv.Itoa(ip.Port))\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/iptables/iptables_test.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage iptables_test\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/docker/go-connections/nat\"\n\tlimaiptables \"github.com/lima-vm/lima/pkg/guestagent/iptables\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/iptables\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/utils\"\n)\n\nfunc TestForwardPorts(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tremove             bool\n\t\tlistenerIP         net.IP\n\t\texpectedEntries    []limaiptables.Entry\n\t\tremovedEntries     []limaiptables.Entry\n\t\tupdateEntries      []limaiptables.Entry\n\t\texpectedAddFuncErr error\n\t}{\n\t\t{\n\t\t\tname:       \"With localhost listener and valid port mappings\",\n\t\t\tlistenerIP: net.IPv4(127, 0, 0, 1),\n\t\t\texpectedEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 20, 10), Port: 1080},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 20, 11), Port: 1081},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 20, 12), Port: 1082},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"With wildcard listener and valid port mappings\",\n\t\t\tlistenerIP: net.IPv4(0, 0, 0, 0),\n\t\t\texpectedEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 21, 10), Port: 1080},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 21, 11), Port: 1081},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 21, 12), Port: 1082},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"With entries removed\",\n\t\t\tremove:     true,\n\t\t\tlistenerIP: net.IPv4(0, 0, 0, 0),\n\t\t\texpectedEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 10), Port: 1080},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 11), Port: 1081},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 12), Port: 1082},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 13), Port: 1083},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 14), Port: 1084},\n\t\t\t},\n\t\t\tremovedEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 11), Port: 1081},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 12), Port: 1082},\n\t\t\t},\n\t\t\tupdateEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 10), Port: 1080},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 13), Port: 1083},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 14), Port: 1084},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 15), Port: 1085},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tiptablesScanner := fakeScanner{\n\t\t\t\texpectedEntries: tt.expectedEntries,\n\t\t\t\texpectedErr:     tt.expectedAddFuncErr,\n\t\t\t}\n\n\t\t\ttestTracker := fakeTracker{\n\t\t\t\treceivedID:          make(chan string),\n\t\t\t\treceivedRemoveID:    make(chan string),\n\t\t\t\treceivedPortMapping: make(chan nat.PortMap),\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tdefer cancel()\n\n\t\t\tinterval := time.Second\n\t\t\tiptablesHandler := iptables.New(ctx, &testTracker, &iptablesScanner, tt.listenerIP, interval)\n\n\t\t\tgo func() {\n\t\t\t\trequire.NoError(t, iptablesHandler.ForwardPorts())\n\t\t\t\tcancel()\n\t\t\t}()\n\n\t\t\tfor _, expectedEntry := range tt.expectedEntries {\n\t\t\t\tid := <-testTracker.receivedID\n\t\t\t\texpectedID := utils.GenerateID(entryToString(expectedEntry))\n\t\t\t\trequire.Equal(t, expectedID, id)\n\n\t\t\t\tpm := <-testTracker.receivedPortMapping\n\t\t\t\tportProto, err := nat.NewPort(\"tcp\", strconv.Itoa(expectedEntry.Port))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\texpectedPortBinding := nat.PortBinding{\n\t\t\t\t\tHostIP:   tt.listenerIP.String(),\n\t\t\t\t\tHostPort: strconv.Itoa(expectedEntry.Port),\n\t\t\t\t}\n\t\t\t\trequire.Contains(t, pm[portProto], expectedPortBinding)\n\t\t\t}\n\n\t\t\tif tt.remove {\n\t\t\t\tiptablesScanner.expectedEntries = tt.updateEntries\n\n\t\t\t\t// Collect all removed IDs.\n\t\t\t\tvar actualRemovedIDs []string\n\t\t\t\tfor _, removedEntry := range tt.removedEntries {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase id := <-testTracker.receivedRemoveID:\n\t\t\t\t\t\tactualRemovedIDs = append(actualRemovedIDs, id)\n\t\t\t\t\tcase <-time.After(5 * time.Second):\n\t\t\t\t\t\tt.Fatalf(\"Timeout waiting for remove ID for entry %v\", removedEntry)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tfor _, removedEntry := range tt.removedEntries {\n\t\t\t\t\trequire.Contains(t, actualRemovedIDs, utils.GenerateID(entryToString(removedEntry)))\n\t\t\t\t}\n\t\t\t\taddedElement := tt.updateEntries[len(tt.updateEntries)-1]\n\t\t\t\tid := <-testTracker.receivedID\n\t\t\t\texpectedID := utils.GenerateID(entryToString(addedElement))\n\t\t\t\trequire.Equal(t, expectedID, id)\n\n\t\t\t\tpm := <-testTracker.receivedPortMapping\n\t\t\t\tportProto, err := nat.NewPort(\"tcp\", strconv.Itoa(addedElement.Port))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\texpectedPortMap := nat.PortMap{\n\t\t\t\t\tportProto: []nat.PortBinding{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tHostIP:   tt.listenerIP.String(),\n\t\t\t\t\t\t\tHostPort: strconv.Itoa(addedElement.Port),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\trequire.ElementsMatch(t, pm[portProto], expectedPortMap[portProto])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestForwardPortsSamePortDifferentIP(t *testing.T) {\n\tduplicatedPort := 1084\n\ttests := []struct {\n\t\tname               string\n\t\tlistenerIP         net.IP\n\t\texpectedEntries    []limaiptables.Entry\n\t\texpectedAddFuncErr error\n\t}{\n\t\t{\n\t\t\tname:       \"Same Port with different IP\",\n\t\t\tlistenerIP: net.IPv4(0, 0, 0, 0),\n\t\t\texpectedEntries: []limaiptables.Entry{\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 10), Port: 1080},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 11), Port: 1081},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 12), Port: 1082},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 13), Port: 1083},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 14), Port: duplicatedPort},\n\t\t\t\t{TCP: true, IP: net.IPv4(192, 168, 22, 15), Port: duplicatedPort},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tiptablesScanner := fakeScanner{\n\t\t\t\texpectedEntries: tt.expectedEntries,\n\t\t\t\texpectedErr:     tt.expectedAddFuncErr,\n\t\t\t}\n\n\t\t\ttestTracker := fakeTracker{\n\t\t\t\treceivedID:          make(chan string),\n\t\t\t\treceivedRemoveID:    make(chan string),\n\t\t\t\treceivedPortMapping: make(chan nat.PortMap),\n\t\t\t}\n\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tdefer cancel()\n\n\t\t\tinterval := time.Second\n\t\t\tiptablesHandler := iptables.New(ctx, &testTracker, &iptablesScanner, tt.listenerIP, interval)\n\n\t\t\tgo func() {\n\t\t\t\trequire.NoError(t, iptablesHandler.ForwardPorts())\n\t\t\t\tcancel()\n\t\t\t}()\n\n\t\t\tfor _, expectedEntry := range tt.expectedEntries {\n\t\t\t\tid := <-testTracker.receivedID\n\t\t\t\texpectedID := utils.GenerateID(entryToString(expectedEntry))\n\t\t\t\trequire.Equal(t, expectedID, id)\n\n\t\t\t\tpm := <-testTracker.receivedPortMapping\n\t\t\t\tportProto, err := nat.NewPort(\"tcp\", strconv.Itoa(expectedEntry.Port))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t// Port bindings for the same port on different IP addresses should appear only once\n\t\t\t\t// in the port mapping. This is because the HostIP is always controlled by the\n\t\t\t\t// k8sServiceListenerAddr, which means that duplicate entries with the same port\n\t\t\t\t// but different IPs are unnecessary and should not be handled.\n\t\t\t\tif expectedEntry.Port == duplicatedPort {\n\t\t\t\t\trequire.Len(t, pm[portProto], 1)\n\t\t\t\t}\n\n\t\t\t\texpectedPortBinding := nat.PortBinding{\n\t\t\t\t\tHostIP:   tt.listenerIP.String(),\n\t\t\t\t\tHostPort: strconv.Itoa(expectedEntry.Port),\n\t\t\t\t}\n\t\t\t\trequire.Contains(t, pm[portProto], expectedPortBinding)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Fake Tracker implementation for mocking behavior\ntype fakeTracker struct {\n\treceivedID          chan string\n\treceivedRemoveID    chan string\n\treceivedPortMapping chan nat.PortMap\n\texpectedAddFuncErr  error\n}\n\nfunc (f *fakeTracker) Get(containerID string) nat.PortMap {\n\treturn nil\n}\n\nfunc (f *fakeTracker) Add(containerID string, portMapping nat.PortMap) error {\n\tf.receivedID <- containerID\n\tf.receivedPortMapping <- portMapping\n\treturn f.expectedAddFuncErr\n}\n\nfunc (f *fakeTracker) Remove(containerID string) error {\n\tf.receivedRemoveID <- containerID\n\treturn nil\n}\n\nfunc (f *fakeTracker) RemoveAll() error {\n\treturn nil\n}\n\n// Fake Scanner to simulate iptables entries\ntype fakeScanner struct {\n\texpectedEntries []limaiptables.Entry\n\texpectedErr     error\n}\n\nfunc (f *fakeScanner) GetPorts() ([]limaiptables.Entry, error) {\n\treturn f.expectedEntries, f.expectedErr\n}\n\n// Utility function to convert iptables entry to string\nfunc entryToString(ip limaiptables.Entry) string {\n\treturn net.JoinHostPort(ip.IP.String(), strconv.Itoa(ip.Port))\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/iptables/scanner.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package iptables handles forwarding ports found in iptables DNAT\npackage iptables\n\nimport \"github.com/lima-vm/lima/pkg/guestagent/iptables\"\n\n// Scanner is the interface that wraps the GetPorts method which\n// is used to scan the iptables.\ntype Scanner interface {\n\tGetPorts() ([]iptables.Entry, error)\n}\n\ntype IptablesScanner struct{}\n\nfunc NewIptablesScanner() *IptablesScanner {\n\treturn &IptablesScanner{}\n}\n\nfunc (i *IptablesScanner) GetPorts() ([]iptables.Entry, error) {\n\treturn iptables.GetPorts()\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/kube/servicewatcher_linux.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage kube\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"golang.org/x/sys/unix\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\tapierrors \"k8s.io/apimachinery/pkg/api/errors\"\n\tv1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"k8s.io/client-go/informers\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/tools/cache\"\n)\n\n// event occurs when a NodePort in a service is added or removed.\ntype event struct {\n\tUID         types.UID\n\tnamespace   string\n\tname        string\n\tportMapping map[int32]corev1.Protocol\n\tdeleted     bool\n}\n\n// watchServices monitors for NodePort and LoadBalancer services; after listing all service ports\n// initially, it reports service ports being added or deleted.\nfunc watchServices(ctx context.Context, client *kubernetes.Clientset) (<-chan event, <-chan error, error) {\n\teventCh := make(chan event)\n\terrorCh := make(chan error)\n\tinformerFactory := informers.NewSharedInformerFactory(client, 1*time.Hour)\n\tserviceInformer := informerFactory.Core().V1().Services()\n\tsharedInformer := serviceInformer.Informer()\n\t_, _ = sharedInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{\n\t\tAddFunc: func(obj interface{}) {\n\t\t\tlog.Tracef(\"Service Informer: Add func called with: %+v\", obj)\n\t\t\thandleUpdate(nil, obj, eventCh)\n\t\t},\n\t\tDeleteFunc: func(obj interface{}) {\n\t\t\tlog.Tracef(\"Service Informer: Del func called with: %+v\", obj)\n\t\t\thandleUpdate(obj, nil, eventCh)\n\t\t},\n\t\tUpdateFunc: func(oldObj, newObj interface{}) {\n\t\t\tlog.Tracef(\"Service Informer: Update func called with old object %+v and new Object: %+v\", oldObj, newObj)\n\t\t\thandleUpdate(oldObj, newObj, eventCh)\n\t\t},\n\t})\n\n\terr := sharedInformer.SetWatchErrorHandler(func(_ *cache.Reflector, err error) {\n\t\tlog.Debugw(\"kubernetes: error watching\", log.Fields{\n\t\t\t\"error\": err,\n\t\t})\n\t\tswitch {\n\t\tcase apierrors.IsResourceExpired(err):\n\t\t\t// resource expired; the informer will adapt, ignore.\n\t\tcase apierrors.IsGone(err):\n\t\t\t// resource is gone; the informer will adapt, ignore.\n\t\tcase apierrors.IsServiceUnavailable(err):\n\t\t\t// service unavailable; it should come back later.\n\t\tcase errors.Is(err, io.EOF):\n\t\t\t// watch closed normally; ignore.\n\t\tcase errors.Is(err, io.ErrUnexpectedEOF):\n\t\t\t// connection interrupted; informer will retry, ignore.\n\t\tcase isTimeout(err):\n\t\t\t// connection is a time out of some sort, this is fine\n\t\tcase errors.Is(err, unix.ECONNREFUSED):\n\t\t\t// connection refused; the server is down.\n\t\t\t// Note that \"failed to list\" errors need k8s.io/client-go 0.25.0\n\t\t\terrorCh <- err\n\t\tdefault:\n\t\t\tvar statusError *apierrors.StatusError\n\t\t\tif errors.As(err, &statusError) {\n\t\t\t\tlog.Debugw(\"kubernetes: got status error\", log.Fields{\n\t\t\t\t\t\"status\": statusError.Status(),\n\t\t\t\t\t//nolint:govet // .DebugError() returns a format string plus fields.\n\t\t\t\t\t\"debug\": fmt.Sprintf(statusError.DebugError()),\n\t\t\t\t})\n\t\t\t}\n\t\t\tlog.Errorw(\"kubernetes: unexpected error watching\", log.Fields{\n\t\t\t\t\"error\": err,\n\t\t\t})\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error watching services: %w\", err)\n\t}\n\n\tinformerFactory.WaitForCacheSync(ctx.Done())\n\tinformerFactory.Start(ctx.Done())\n\n\tservices, err := client.CoreV1().Services(corev1.NamespaceAll).List(ctx, v1.ListOptions{})\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error listing services: %w\", err)\n\t}\n\tlog.Debugf(\"coreV1 services list :%+v\", services.Items)\n\n\t// List the initial set of services asynchronously, so that we don't have to\n\t// worry about the channel blocking.\n\tgo func() {\n\t\tfor i := range services.Items {\n\t\t\thandleUpdate(nil, &services.Items[i], eventCh)\n\t\t}\n\t}()\n\n\treturn eventCh, errorCh, nil\n}\n\n// handleUpdate examines the old and new services, calculating the difference\n// and emitting events to the given channel.\nfunc handleUpdate(oldObj, newObj interface{}, eventCh chan<- event) {\n\tdeleted := make(map[int32]corev1.Protocol)\n\tadded := make(map[int32]corev1.Protocol)\n\toldSvc, _ := oldObj.(*corev1.Service)\n\tnewSvc, _ := newObj.(*corev1.Service)\n\tnamespace := \"<unknown>\"\n\tname := \"<unknown>\"\n\n\tif oldSvc != nil {\n\t\tnamespace = oldSvc.Namespace\n\t\tname = oldSvc.Name\n\n\t\tif oldSvc.Spec.Type == corev1.ServiceTypeNodePort {\n\t\t\tfor _, port := range oldSvc.Spec.Ports {\n\t\t\t\tdeleted[port.NodePort] = port.Protocol\n\t\t\t}\n\t\t}\n\n\t\tif oldSvc.Spec.Type == corev1.ServiceTypeLoadBalancer {\n\t\t\tfor _, port := range oldSvc.Spec.Ports {\n\t\t\t\tdeleted[port.Port] = port.Protocol\n\t\t\t}\n\t\t}\n\t}\n\n\tif newSvc != nil {\n\t\tnamespace = newSvc.Namespace\n\t\tname = newSvc.Name\n\n\t\tif newSvc.Spec.Type == corev1.ServiceTypeNodePort {\n\t\t\tfor _, port := range newSvc.Spec.Ports {\n\t\t\t\tdelete(deleted, port.NodePort)\n\t\t\t\tadded[port.NodePort] = port.Protocol\n\t\t\t}\n\t\t}\n\n\t\tif newSvc.Spec.Type == corev1.ServiceTypeLoadBalancer {\n\t\t\tfor _, port := range newSvc.Spec.Ports {\n\t\t\t\tdelete(deleted, port.Port)\n\t\t\t\tadded[port.Port] = port.Protocol\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(deleted) > 0 {\n\t\tsendEvents(deleted, oldSvc, true, eventCh)\n\t}\n\n\tif len(added) > 0 {\n\t\tsendEvents(added, newSvc, false, eventCh)\n\t}\n\n\tlog.Debugf(\"kubernetes service update: %s/%s has -%d +%d service port\",\n\t\tnamespace, name, len(deleted), len(added))\n}\n\nfunc sendEvents(mapping map[int32]corev1.Protocol, svc *corev1.Service, deleted bool, eventCh chan<- event) {\n\tif svc != nil {\n\t\teventCh <- event{\n\t\t\tUID:         svc.UID,\n\t\t\tnamespace:   svc.Namespace,\n\t\t\tname:        svc.Name,\n\t\t\tportMapping: mapping,\n\t\t\tdeleted:     deleted,\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/kube/watcher_linux.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package kube watches Kubernetes for NodePort and LoadBalancer service types.\n// It exposes the services as follows:\n// - [namespaced network - admin install]: It uses API tracker to expose the ports\n// on the host through host-switch.exe\n// - [namespaced network - non-admin install]: It uses API tracker to expose the ports\n// on the host through host-switch.exe; however, the exposed ports are only bound to\n// 127.0.0.1 on the host machine.\npackage kube\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"golang.org/x/sys/unix\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/client-go/kubernetes\"\n\trestclient \"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n)\n\n// watcherState is an enumeration to track the state of the watcher.\ntype watcherState int\n\nconst (\n\t// stateNoConfig is before the configuration has been loaded.\n\tstateNoConfig watcherState = iota\n\t// stateDisconnected is when the configuration has been loaded, but not connected.\n\tstateDisconnected\n\tstateWatching\n)\n\n// WatchForServices watches Kubernetes for NodePort and LoadBalancer services\n// and create listeners on 0.0.0.0 matching them.\n// Any connection errors are ignored and retried.\nfunc WatchForServices(\n\tctx context.Context,\n\tconfigPath string,\n\tk8sServiceListenerIP net.IP,\n\tportTracker tracker.Tracker,\n) error {\n\t// These variables are shared across the different states\n\tvar (\n\t\tstate     = stateNoConfig\n\t\terr       error\n\t\tconfig    *restclient.Config\n\t\tclientset *kubernetes.Clientset\n\t\teventCh   <-chan event\n\t\terrorCh   <-chan error\n\t)\n\n\twatchContext, watchCancel := context.WithCancel(ctx)\n\n\t// Always cancel if we failed.\n\tdefer watchCancel()\n\n\tfor {\n\t\tswitch state {\n\t\tcase stateNoConfig:\n\t\t\tconfig, err = getClientConfig(configPath)\n\t\t\tif err != nil {\n\t\t\t\tlog.Debugw(\"kubernetes: failed to read kubeconfig\", log.Fields{\n\t\t\t\t\t\"config-path\": configPath,\n\t\t\t\t\t\"error\":       err,\n\t\t\t\t})\n\n\t\t\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t\t\t// Wait for the file to exist\n\t\t\t\t\ttime.Sleep(time.Second)\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tlog.Debugf(\"kubernetes: loaded kubeconfig %s\", configPath)\n\n\t\t\tstate = stateDisconnected\n\t\tcase stateDisconnected:\n\t\t\tclientset, err = kubernetes.NewForConfig(config)\n\t\t\tif err != nil {\n\t\t\t\t// There should be no transient errors here\n\t\t\t\tlog.Errorw(\"failed to load kubeconfig\", log.Fields{\n\t\t\t\t\t\"config-path\": configPath,\n\t\t\t\t\t\"error\":       err,\n\t\t\t\t})\n\n\t\t\t\treturn fmt.Errorf(\"failed to create Kubernetes client: %w\", err)\n\t\t\t}\n\n\t\t\teventCh, errorCh, err = watchServices(watchContext, clientset)\n\t\t\tif err != nil {\n\t\t\t\tswitch {\n\t\t\t\tdefault:\n\t\t\t\t\treturn err\n\t\t\t\tcase isTimeout(err):\n\t\t\t\tcase errors.Is(err, unix.ENETUNREACH):\n\t\t\t\tcase errors.Is(err, unix.ECONNREFUSED):\n\t\t\t\tcase isAPINotReady(err):\n\t\t\t\t}\n\t\t\t\t// sleep and continue for all the expected case\n\t\t\t\ttime.Sleep(time.Second)\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Debugf(\"watching kubernetes services\")\n\n\t\t\tstate = stateWatching\n\t\tcase stateWatching:\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tlog.Debugw(\"kubernetes watcher: context closed\", log.Fields{\n\t\t\t\t\t\"error\": ctx.Err(),\n\t\t\t\t})\n\n\t\t\t\treturn ctx.Err()\n\t\t\tcase err = <-errorCh:\n\t\t\t\tlog.Debugw(\"kubernetes: got error, rolling back\", log.Fields{\n\t\t\t\t\t\"error\": err,\n\t\t\t\t})\n\t\t\t\twatchCancel()\n\n\t\t\t\tstate = stateNoConfig\n\n\t\t\t\ttime.Sleep(time.Second)\n\n\t\t\t\tcontinue\n\t\t\tcase event := <-eventCh:\n\t\t\t\tif event.deleted {\n\t\t\t\t\tif err := portTracker.Remove(string(event.UID)); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"failed to delete port mapping: %v from tracker UID: %v namespace: %s name: %s failed: %s\",\n\t\t\t\t\t\t\tevent.portMapping,\n\t\t\t\t\t\t\tevent.UID,\n\t\t\t\t\t\t\tevent.namespace,\n\t\t\t\t\t\t\tevent.name,\n\t\t\t\t\t\t\terr)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Debugf(\"kubernetes service: port mapping deleted %s/%s:%v\",\n\t\t\t\t\t\t\tevent.namespace, event.name, event.portMapping)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tportMapping, err := createPortMapping(event.portMapping, k8sServiceListenerIP)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Errorf(\"failed to create port mapping: %v from tracker UID: %v namespace: %s name: %s failed: %s\",\n\t\t\t\t\t\t\tevent.portMapping,\n\t\t\t\t\t\t\tevent.UID,\n\t\t\t\t\t\t\tevent.namespace,\n\t\t\t\t\t\t\tevent.name,\n\t\t\t\t\t\t\terr)\n\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif err := portTracker.Add(string(event.UID), portMapping); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"failed to add port mapping: %v from tracker UID: %v namespace: %s name: %s failed: %s\",\n\t\t\t\t\t\t\tevent.portMapping,\n\t\t\t\t\t\t\tevent.UID,\n\t\t\t\t\t\t\tevent.namespace,\n\t\t\t\t\t\t\tevent.name,\n\t\t\t\t\t\t\terr)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tlog.Debugf(\"kubernetes service: port mapping added %s/%s:%v\",\n\t\t\t\t\t\t\tevent.namespace, event.name, event.portMapping)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// getClientConfig returns a rest config.\nfunc getClientConfig(configPath string) (*restclient.Config, error) {\n\tloadingRules := clientcmd.ClientConfigLoadingRules{\n\t\tExplicitPath: configPath,\n\t}\n\tclientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, nil)\n\tconfig, err := clientConfig.ClientConfig()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not load Kubernetes client config from %s: %w\", configPath, err)\n\t}\n\n\treturn config, nil\n}\n\nfunc isTimeout(err error) bool {\n\ttype timeout interface {\n\t\tTimeout() bool\n\t}\n\n\tvar timeoutError timeout\n\n\tif !errors.As(err, &timeoutError) {\n\t\treturn timeoutError != nil && timeoutError.Timeout()\n\t}\n\n\treturn false\n}\n\n// This is a k3s error that is received over\n// the HTTP, Also, it is worth noting that this\n// error is wrapped. This is why we are not testing\n// against the real error object using errors.Is().\nfunc isAPINotReady(err error) bool {\n\treturn strings.Contains(err.Error(), \"apiserver not ready\")\n}\n\nfunc createPortMapping(ports map[int32]corev1.Protocol, k8sServiceListenerIP net.IP) (nat.PortMap, error) {\n\tportMap := make(nat.PortMap)\n\n\tfor port, proto := range ports {\n\t\tprotocol := strings.ToLower(string(proto))\n\t\tlog.Debugf(\"create port mapping for port %d, protocol %s\", port, protocol)\n\t\tportMapKey, err := nat.NewPort(protocol, strconv.Itoa(int(port)))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tportBinding := nat.PortBinding{\n\t\t\tHostIP:   k8sServiceListenerIP.String(),\n\t\t\tHostPort: strconv.Itoa(int(port)),\n\t\t}\n\n\t\tif pb, ok := portMap[portMapKey]; ok {\n\t\t\tportMap[portMapKey] = append(pb, portBinding)\n\t\t} else {\n\t\t\tportMap[portMapKey] = []nat.PortBinding{portBinding}\n\t\t}\n\t}\n\n\treturn portMap, nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/kube/watcher_stub.go",
    "content": "//go:build !linux\n\n/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage kube\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n)\n\nfunc WatchForServices(\n\tctx context.Context,\n\tconfigPath string,\n\tk8sServiceListenerIP net.IP,\n\tportTracker tracker.Tracker,\n) error {\n\treturn fmt.Errorf(\"not implemented for non-linux\")\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/procnet/scanner_linux.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n/*\nPackage procnet provides functionality to scan and manage network ports based on the system's\n/proc/net/{tcp,udp} entries. It monitors for new and removed ports and handles port forwarding\nvia host switch's API. Also, it creates iptables PREROUTING rules, specifically for containers\nusing the host network driver. This package is designed to work with the Linux-based WSL\nenvironment, enabling localnet routing and managing port mappings.\n*/\npackage procnet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"github.com/lima-vm/lima/pkg/guestagent/procnettcp\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/utils\"\n)\n\ntype action string\n\nconst (\n\tAppend action = \"append\"\n\tDelete action = \"delete\"\n)\n\nconst routeLocalnet = \"/proc/sys/net/ipv4/conf/eth0/route_localnet\"\n\ntype ProcNetScanner struct {\n\tcontext       context.Context\n\tLocalnetRoute bool\n\ttracker       tracker.Tracker\n\tscanInterval  time.Duration\n}\n\nfunc NewProcNetScanner(ctx context.Context, t tracker.Tracker, scanInterval time.Duration) (*ProcNetScanner, error) {\n\treturn &ProcNetScanner{\n\t\tcontext:      ctx,\n\t\ttracker:      t,\n\t\tscanInterval: scanInterval,\n\t}, enableLocalnetRouting()\n}\n\nfunc (p *ProcNetScanner) ForwardPorts() error {\n\tticker := time.NewTicker(p.scanInterval)\n\tdefer ticker.Stop()\n\n\tvar previousPortMap nat.PortMap\n\n\tfor {\n\t\tselect {\n\t\tcase <-p.context.Done():\n\t\t\treturn fmt.Errorf(\"/proc/net scanner context cancelled: %w\", p.context.Err())\n\t\tcase <-ticker.C:\n\t\t\tentries, err := procnettcp.ParseFiles()\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"failed to parse /proc/net/{tcp, udp} files: %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tnewPortMap := make(nat.PortMap)\n\t\t\tfor _, entry := range entries {\n\t\t\t\tif err := addValidProtoEntryToPortMap(entry, newPortMap); err != nil {\n\t\t\t\t\tlog.Errorf(\"failed to create portMapping for entry: %w\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Add new ports\n\t\t\tfor port, bindings := range newPortMap {\n\t\t\t\tif _, exists := previousPortMap[port]; !exists {\n\t\t\t\t\tlog.Infof(\"/proc/net scanner added port: %s -> %+v\", port, bindings)\n\t\t\t\t\terr := p.tracker.Add(utils.GenerateID(fmt.Sprintf(\"%s/%s\", port.Proto(), port.Port())), nat.PortMap{\n\t\t\t\t\t\tport: bindings,\n\t\t\t\t\t})\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Errorf(\"/proc/net scanner failed to add port: %s\", err)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif err = p.execLoopbackIPtablesRule(bindings, port, Append); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"/proc/net scanner creating loopback iptable rules for portbinding: %v failed: %s\", bindings, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Remove old ports\n\t\t\tfor port, previousBindings := range previousPortMap {\n\t\t\t\tif _, exists := newPortMap[port]; !exists {\n\t\t\t\t\tlog.Infof(\"/proc/net scanner removed port: %s -> %+v\", port, previousBindings)\n\t\t\t\t\terr := p.tracker.Remove(utils.GenerateID(fmt.Sprintf(\"%s/%s\", port.Proto(), port.Port())))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Errorf(\"/proc/net scanner failed to remove port: %s\", err)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\t\tif err = p.execLoopbackIPtablesRule(previousBindings, port, Delete); err != nil {\n\t\t\t\t\t\tlog.Errorf(\"/proc/net scanner deleting loopback iptable rules for portbinding: %v failed: %s\", previousBindings, err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpreviousPortMap = newPortMap\n\t\t}\n\t}\n}\n\n// execLoopbackIPtablesRule modifies iptables NAT rules to handle loopback traffic for a specified port\n// and protocol. This function is only necessary when the container is using the host network driver\n// (i.e., with --network=host), as in this case the container shares the host's network namespace.\n//\n// When using the host network driver, network traffic bound to 127.0.0.1 needs to be redirected from\n// outside the network namespace to the localhost (127.0.0.1). This function adds or removes DNAT rules\n// that allow traffic to be forwarded to the specified port on localhost, based on the provided 'action'\n// ('append' or 'delete').\n//\n// The function iterates over the provided list of port bindings. For each binding where the HostIP is set\n// to 127.0.0.1, it constructs and executes the corresponding iptables command to either add or delete the\n// appropriate DNAT rule.\n//\n// The iptables rule ensures that incoming traffic from outside the network namespace (i.e., from the\n// host machine) on the specified port and protocol is redirected to the same port on localhost, where the\n// container's service can be accessed.\n//\n// Example iptables rule when 'action' is \"append\":\n//\n//\tiptables -t nat -A PREROUTING -p tcp --dport 8009 -j DNAT --to-destination 127.0.0.1:8009\n//\n// Example iptables rule when 'action' is \"delete\":\n//\n//\tiptables -t nat -D PREROUTING -p tcp --dport 8009 -j DNAT --to-destination 127.0.0.1:8009\nfunc (p *ProcNetScanner) execLoopbackIPtablesRule(bindings []nat.PortBinding, portProto nat.Port, action action) error {\n\tfor _, binding := range bindings {\n\t\tif binding.HostIP == \"127.0.0.1\" {\n\t\t\t// iptables -t nat -D PREROUTING -p tcp --dport 8009 -j DNAT --to-destination 127.0.0.1:8009\n\t\t\t//nolint:gosec // None of the arguments are user-supplied.\n\t\t\tiptablesCmd := exec.CommandContext(p.context,\n\t\t\t\t\"iptables\",\n\t\t\t\t\"--table\", \"nat\",\n\t\t\t\tfmt.Sprintf(\"--%s\", action), \"PREROUTING\",\n\t\t\t\t\"--protocol\", portProto.Proto(),\n\t\t\t\t\"--dport\", binding.HostPort,\n\t\t\t\t\"--jump\", \"DNAT\",\n\t\t\t\t\"--to-destination\", fmt.Sprintf(\"%s:%s\", binding.HostIP, binding.HostPort),\n\t\t\t)\n\t\t\tif err := iptablesCmd.Run(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tlog.Debugf(\"running the following iptables rule [%s] for port bindings: %v\", iptablesCmd.String(), binding)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc addValidProtoEntryToPortMap(entry procnettcp.Entry, portMap nat.PortMap) error {\n\tswitch entry.Kind {\n\tcase procnettcp.TCP:\n\t\tif entry.State == procnettcp.TCPListen {\n\t\t\treturn addEntryToPortMap(entry, portMap)\n\t\t}\n\tcase procnettcp.UDP:\n\t\tif entry.State == procnettcp.UDPEstablished {\n\t\t\treturn addEntryToPortMap(entry, portMap)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc addEntryToPortMap(entry procnettcp.Entry, portMap nat.PortMap) error {\n\tport := strconv.Itoa(int(entry.Port))\n\tportMapKey, err := nat.NewPort(strings.ToLower(entry.Kind), port)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"generating portMapKey protocol: %s, port: %d failed: %w\",\n\t\t\tentry.Kind,\n\t\t\tentry.Port,\n\t\t\terr)\n\t}\n\n\t// It's important not to use entry.IP directly here, as any IP\n\t// other than 127.0.0.1 (localhost) or 0.0.0.0 may not be accessible\n\t// from the host. To ensure consistent behavior, we always set the\n\t// HostIP to INADDR_ANY (0.0.0.0) unless the IP is localhost or 0.0.0.0.\n\t// The API tracker will then adjust the address as necessary:\n\t// - If admin privileges are enabled, the address will remain 0.0.0.0.\n\t// - Otherwise, it will be changed to 127.0.0.1 to ensure proper local binding.\n\tvar hostIP net.IP\n\tinAddrAny := net.IPv4(0, 0, 0, 0)\n\tif entry.IP.IsLoopback() || entry.IP.Equal(inAddrAny) {\n\t\thostIP = entry.IP\n\t} else {\n\t\thostIP = inAddrAny\n\t}\n\tportBinding := nat.PortBinding{\n\t\tHostIP:   hostIP.String(),\n\t\tHostPort: port,\n\t}\n\tportMap[portMapKey] = append(portMap[portMapKey], portBinding)\n\treturn nil\n}\n\nfunc enableLocalnetRouting() error {\n\tconst enable = \"1\"\n\treturn writeSysctl(routeLocalnet, enable)\n}\n\nfunc writeSysctl(path, value string) error {\n\tf, err := os.OpenFile(path, os.O_WRONLY, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not open the sysctl file %s: %w\", path, err)\n\t}\n\tdefer f.Close()\n\tif _, err := f.WriteString(value); err != nil {\n\t\treturn fmt.Errorf(\"could not write to the sysctl file %s: %w\", path, err)\n\t}\n\tlog.Infof(\"/proc/net scanner enabled %s\", routeLocalnet)\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/procnet/scanner_stub.go",
    "content": "//go:build !linux\n\n/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage procnet\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n)\n\ntype ProcNetScanner struct{}\n\nfunc NewProcNetScanner(context.Context, tracker.Tracker, time.Duration) (*ProcNetScanner, error) {\n\tpanic(\"only implemented for Linux\")\n}\n\nfunc (p *ProcNetScanner) ForwardPorts() error {\n\treturn fmt.Errorf(\"only implemented for Linux\")\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/tracker/apitracker.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage tracker\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\t\"github.com/docker/go-connections/nat\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/forwarder\"\n\tguestagentTypes \"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n)\n\nconst (\n\t// The gateway represents the hostname where the hostSwitch API is hosted.\n\tgateway        = \"gateway.rancher-desktop.internal\"\n\tGatewayBaseURL = \"http://\" + gateway + \":80\"\n)\n\nvar (\n\tErrAPI         = errors.New(\"error from API\")\n\tErrInvalidIPv4 = errors.New(\"not an IPv4 address\")\n\tErrWSLProxy    = errors.New(\"error from Rancher Desktop WSL Proxy\")\n)\n\n// APITracker keeps track of the port mappings and calls the\n// corresponding API endpoints that is responsible for exposing\n// and unexposing the ports on the host. This should only be used when\n// the Rancher Desktop networking is enabled and the privileged service is disabled.\ntype APITracker struct {\n\tcontext           context.Context\n\twslProxyForwarder forwarder.Forwarder\n\tisAdmin           bool\n\tbaseURL           string\n\ttapInterfaceIP    string\n\tportStorage       *portStorage\n\tapiForwarder      *forwarder.APIForwarder\n}\n\n// NewAPITracker creates a new instance of APITracker with the specified configuration.\n//   - ctx: The context to manage the lifecycle and cancellation of operations. It allows the APITracker\n//     to be aware of broader request timeouts or cancellation signals.\n//   - wslProxyForwarder: An interface or struct responsible for forwarding API calls to the Rancher Desktop's WSL proxy.\n//     It handles sending port mapping updates and removals from other WSL distros.\n//   - baseURL: The base URL of the API server that the APITracker will communicate with to expose or unexpose\n//     ports. This URL is used by the APIForwarder to construct API requests.\n//   - tapIfaceIP: The IP address of the tap interface that the API calls will use for port forwarding. This address\n//     is used to route traffic from the host to the container.\n//   - isAdmin: Indicates whether the application is running with administrative privileges. This flag determines\n//     whether the APITracker should use the localhost IP address (127.0.0.1) for operations if not running as an\n//     administrator.\nfunc NewAPITracker(ctx context.Context, wslProxyForwarder forwarder.Forwarder, baseURL, tapIfaceIP string, isAdmin bool) *APITracker {\n\treturn &APITracker{\n\t\tcontext:           ctx,\n\t\twslProxyForwarder: wslProxyForwarder,\n\t\tisAdmin:           isAdmin,\n\t\tbaseURL:           baseURL,\n\t\ttapInterfaceIP:    tapIfaceIP,\n\t\tportStorage:       newPortStorage(),\n\t\tapiForwarder:      forwarder.NewAPIForwarder(baseURL),\n\t}\n}\n\n// Add a container ID and port mapping to the tracker and calls the\n// /services/forwarder/expose endpoint to forward the port mappings.\nfunc (a *APITracker) Add(containerID string, portMap nat.PortMap) error {\n\tvar errs []error\n\n\tsuccessfullyForwarded := make(nat.PortMap)\n\n\tfor portProto, portBindings := range portMap {\n\t\tvar tmpPortBinding []nat.PortBinding\n\n\t\tlog.Debugf(\"called add with portProto: %+v, portBindings: %+v\\n\", portProto, portBindings)\n\n\t\tfor _, portBinding := range portBindings {\n\t\t\t// The expose API only supports IPv4\n\t\t\tipv4, err := isIPv4(portBinding.HostIP)\n\t\t\tif !ipv4 || err != nil {\n\t\t\t\tlog.Errorf(\"did not receive IPv4 for HostIP: %s\", portBinding.HostIP)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Debugf(\"exposing the following port binding: %+v\", portBinding)\n\n\t\t\terr = a.apiForwarder.Expose(\n\t\t\t\t&types.ExposeRequest{\n\t\t\t\t\tLocal:    ipPortBuilder(a.determineHostIP(portBinding.HostIP), portBinding.HostPort),\n\t\t\t\t\tRemote:   ipPortBuilder(a.tapInterfaceIP, portBinding.HostPort),\n\t\t\t\t\tProtocol: types.TransportProtocol(strings.ToLower(portProto.Proto())),\n\t\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\terrs = append(errs, fmt.Errorf(\"exposing %+v failed: %w\", portBinding, err))\n\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttmpPortBinding = append(tmpPortBinding, portBinding)\n\t\t}\n\n\t\tif len(tmpPortBinding) != 0 {\n\t\t\tsuccessfullyForwarded[portProto] = tmpPortBinding\n\t\t}\n\t}\n\n\tif len(successfullyForwarded) != 0 {\n\t\ta.portStorage.add(containerID, successfullyForwarded)\n\t\tportMapping := guestagentTypes.PortMapping{\n\t\t\tRemove: false,\n\t\t\tPorts:  successfullyForwarded,\n\t\t}\n\t\tlog.Debugf(\"forwarding to wsl-proxy to add port mapping: %+v\", portMapping)\n\t\terr := a.wslProxyForwarder.Send(portMapping)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"sending port mappings to wsl proxy error: %w\", err)\n\t\t}\n\t}\n\n\tif len(errs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", forwarder.ErrExposeAPI, errs)\n\t}\n\n\treturn nil\n}\n\n// Get looks up the port mapping by containerID and returns the result.\nfunc (a *APITracker) Get(containerID string) nat.PortMap {\n\treturn a.portStorage.get(containerID)\n}\n\n// Remove a single entry from the port storage and calls the\n// /services/forwarder/unexpose endpoint to remove the forwarded port mappings.\nfunc (a *APITracker) Remove(containerID string) error {\n\tportMap := a.portStorage.get(containerID)\n\tdefer a.portStorage.remove(containerID)\n\n\tvar errs []error\n\n\tfor portProto, portBindings := range portMap {\n\t\tfor _, portBinding := range portBindings {\n\t\t\t// The unexpose API only supports IPv4\n\t\t\tipv4, err := isIPv4(portBinding.HostIP)\n\t\t\tif !ipv4 || err != nil {\n\t\t\t\tlog.Errorf(\"did not receive IPv4 for HostIP: %s\", portBinding.HostIP)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tlog.Debugf(\"unexposing the following port binding: %+v\", portBinding)\n\n\t\t\terr = a.apiForwarder.Unexpose(\n\t\t\t\t&types.UnexposeRequest{\n\t\t\t\t\tLocal:    ipPortBuilder(a.determineHostIP(portBinding.HostIP), portBinding.HostPort),\n\t\t\t\t\tProtocol: types.TransportProtocol(strings.ToLower(portProto.Proto())),\n\t\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\terrs = append(errs,\n\t\t\t\t\tfmt.Errorf(\"unexposing %+v failed: %w\", portBinding, err))\n\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(portMap) != 0 {\n\t\tportMapping := guestagentTypes.PortMapping{\n\t\t\tRemove: true,\n\t\t\tPorts:  portMap,\n\t\t}\n\t\tlog.Debugf(\"forwarding to wsl-proxy to remove port mapping: %+v\", portMapping)\n\t\terr := a.wslProxyForwarder.Send(portMapping)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"sending port mappings to wsl proxy error: %w\", err)\n\t\t}\n\t}\n\n\tif len(errs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", forwarder.ErrUnexposeAPI, errs)\n\t}\n\n\treturn nil\n}\n\n// RemoveAll calls the /services/forwarder/unexpose\n// and removes all the port bindings from the tracker.\nfunc (a *APITracker) RemoveAll() error {\n\tvar apiErrs, wslProxyErrs []error\n\n\tfor _, portMapping := range a.portStorage.getAll() {\n\t\tfor _, portBindings := range portMapping {\n\t\t\tfor _, portBinding := range portBindings {\n\t\t\t\t// The unexpose API only supports IPv4\n\t\t\t\tipv4, err := isIPv4(portBinding.HostIP)\n\t\t\t\tif !ipv4 || err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tlog.Debugf(\"unexposing the following port binding: %+v\", portBinding)\n\n\t\t\t\terr = a.apiForwarder.Unexpose(\n\t\t\t\t\t&types.UnexposeRequest{\n\t\t\t\t\t\tLocal: ipPortBuilder(a.determineHostIP(portBinding.HostIP), portBinding.HostPort),\n\t\t\t\t\t})\n\t\t\t\tif err != nil {\n\t\t\t\t\tapiErrs = append(apiErrs,\n\t\t\t\t\t\tfmt.Errorf(\"RemoveAll unexposing %+v failed: %w\", portBinding, err))\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tportMapping := guestagentTypes.PortMapping{\n\t\t\tRemove: true,\n\t\t\tPorts:  portMapping,\n\t\t}\n\n\t\tlog.Debugf(\"forwarding to wsl-proxy to remove port mapping: %+v\", portMapping)\n\t\twslProxyError := a.wslProxyForwarder.Send(portMapping)\n\t\tif wslProxyError != nil {\n\t\t\twslProxyErrs = append(wslProxyErrs,\n\t\t\t\tfmt.Errorf(\"sending port mappings to wsl proxy error: %w\", wslProxyError))\n\t\t}\n\t}\n\n\ta.portStorage.removeAll()\n\n\tif len(apiErrs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", forwarder.ErrUnexposeAPI, apiErrs)\n\t}\n\n\tif len(wslProxyErrs) != 0 {\n\t\treturn fmt.Errorf(\"%w: %+v\", ErrWSLProxy, wslProxyErrs)\n\t}\n\n\treturn nil\n}\n\nfunc (a *APITracker) determineHostIP(hostIP string) string {\n\t// If Rancher Desktop is installed as non-admin, we use the\n\t// localhost IP address since binding to a port on 127.0.0.1\n\t// does not require administrative privileges on Windows.\n\tif !a.isAdmin {\n\t\treturn \"127.0.0.1\"\n\t}\n\n\treturn hostIP\n}\n\nfunc ipPortBuilder(ip, port string) string {\n\treturn ip + \":\" + port\n}\n\nfunc isIPv4(addr string) (bool, error) {\n\tip := net.ParseIP(addr)\n\tif ip == nil {\n\t\treturn false, fmt.Errorf(\"%w: %s\", ErrInvalidIPv4, addr)\n\t}\n\n\tif ip.To4() != nil {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/tracker/apitracker_test.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage tracker_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/forwarder\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/tracker\"\n\tguestagentType \"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n)\n\nconst (\n\thostSwitchIP   = \"192.168.127.2\"\n\tcontainerID    = \"containerID_1\"\n\tcontainerID2   = \"containerID_2\"\n\thostIP         = \"127.0.0.1\"\n\thostIP2        = \"127.0.0.2\"\n\thostIP3        = \"127.0.0.3\"\n\thostPort       = \"80\"\n\thostPort2      = \"443\"\n\tadditionalPort = \"8080\"\n\tprotocolTCP    = \"tcp\"\n\tprotocolUDP    = \"udp\"\n)\n\nfunc TestBasicAdd(t *testing.T) {\n\tt.Parallel()\n\n\tvar expectedExposeReq *types.ExposeRequest\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(_ http.ResponseWriter, r *http.Request) {\n\t\terr := json.NewDecoder(r.Body).Decode(&expectedExposeReq)\n\t\trequire.NoError(t, err)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expectedExposeReq.Local, ipPortBuilder(hostIP, hostPort))\n\tassert.Equal(t, expectedExposeReq.Remote, ipPortBuilder(hostSwitchIP, hostPort))\n\n\tactualPortMapping := apiTracker.Get(containerID)\n\tassert.Equal(t, portMapping, actualPortMapping)\n}\n\nfunc TestAddOverride(t *testing.T) {\n\tt.Parallel()\n\n\tvar expectedExposeReq []*types.ExposeRequest\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(_ http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.ExposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\texpectedExposeReq = append(expectedExposeReq, tmpReq)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tprotoPort2, err := nat.NewPort(protocolTCP, hostPort2)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t\tprotoPort2: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t},\n\t}\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, expectedExposeReq,\n\t\t[]*types.ExposeRequest{\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP, hostPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, hostPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP2, hostPort2),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, hostPort2),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t})\n\n\tactualPortMapping := apiTracker.Get(containerID)\n\tassert.Equal(t, portMapping, actualPortMapping)\n\n\t// reset the exposeReq slice\n\texpectedExposeReq = nil\n\n\tprotoPort3, err := nat.NewPort(protocolUDP, additionalPort)\n\trequire.NoError(t, err)\n\n\tportMapping2 := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t\tprotoPort3: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: additionalPort,\n\t\t\t},\n\t\t},\n\t}\n\terr = apiTracker.Add(containerID, portMapping2)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, expectedExposeReq,\n\t\t[]*types.ExposeRequest{\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP, hostPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, hostPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP2, additionalPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, additionalPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolUDP),\n\t\t\t},\n\t\t})\n\n\tactualPortMapping = apiTracker.Get(containerID)\n\tassert.Equal(t, portMapping2, actualPortMapping)\n}\n\nfunc TestAddWithError(t *testing.T) {\n\tt.Parallel()\n\n\tvar expectedExposeReq []*types.ExposeRequest\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.ExposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\tif tmpReq.Local == ipPortBuilder(hostIP2, hostPort) {\n\t\t\thttp.Error(w, \"Bad API error\", http.StatusRequestTimeout)\n\n\t\t\treturn\n\t\t}\n\t\texpectedExposeReq = append(expectedExposeReq, tmpReq)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.Error(t, err)\n\n\terrPortBinding := nat.PortBinding{\n\t\tHostIP:   hostIP2,\n\t\tHostPort: hostPort,\n\t}\n\tnestedErr := fmt.Errorf(\"%w: Bad API error\", tracker.ErrAPI)\n\terrs := []error{\n\t\tfmt.Errorf(\"exposing %+v failed: %w\", errPortBinding, nestedErr),\n\t}\n\texpectedErr := fmt.Errorf(\"%w: %+v\", forwarder.ErrExposeAPI, errs)\n\trequire.EqualError(t, err, expectedErr.Error())\n\n\tassert.Len(t, expectedExposeReq, 2)\n\tassert.ElementsMatch(t, expectedExposeReq,\n\t\t[]*types.ExposeRequest{\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP, hostPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, hostPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(hostIP3, hostPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, hostPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t},\n\t)\n\tassert.NotContains(t, expectedExposeReq,\n\t\t&types.ExposeRequest{\n\t\t\tLocal:  ipPortBuilder(hostIP2, hostPort),\n\t\t\tRemote: ipPortBuilder(hostSwitchIP, hostPort),\n\t\t},\n\t)\n\n\tactualPortMapping := apiTracker.Get(containerID)\n\tassert.Len(t, actualPortMapping[protoPort], 2)\n\tassert.NotContains(t, actualPortMapping[protoPort], nat.PortBinding{\n\t\tHostIP:   hostIP2,\n\t\tHostPort: hostPort,\n\t})\n\tassert.Equal(t,\n\t\t[]nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t}, actualPortMapping[protoPort])\n}\n\nfunc TestGet(t *testing.T) {\n\tt.Parallel()\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort2)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t},\n\t}\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\tactualPortMappings := apiTracker.Get(containerID)\n\tassert.Len(t, actualPortMappings, len(portMapping))\n\tassert.Equal(t, actualPortMappings[\"443/tcp\"], portMapping[\"443/tcp\"])\n}\n\nfunc TestRemove(t *testing.T) {\n\tt.Parallel()\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tvar expectedUnexposeReq *types.UnexposeRequest\n\n\tmux.HandleFunc(\"/services/forwarder/unexpose\", func(_ http.ResponseWriter, r *http.Request) {\n\t\terr := json.NewDecoder(r.Body).Decode(&expectedUnexposeReq)\n\t\trequire.NoError(t, err)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tprotoPort2, err := nat.NewPort(protocolTCP, hostPort2)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\tportMapping2 := nat.PortMap{\n\t\tprotoPort2: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.Add(containerID2, portMapping2)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.Remove(containerID)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, expectedUnexposeReq.Local, ipPortBuilder(hostIP, hostPort))\n\n\texpectedPortMapping1 := apiTracker.Get(containerID)\n\tassert.Nil(t, expectedPortMapping1)\n\n\texpectedPortMapping2 := apiTracker.Get(containerID2)\n\tassert.Equal(t, expectedPortMapping2, portMapping2)\n}\n\nfunc TestRemoveWithError(t *testing.T) {\n\tt.Parallel()\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tvar expectedUnexposeReq []*types.UnexposeRequest\n\n\tmux.HandleFunc(\"/services/forwarder/unexpose\", func(w http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.UnexposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\tif tmpReq.Local == ipPortBuilder(hostIP2, hostPort) {\n\t\t\thttp.Error(w, \"Test API error\", http.StatusRequestTimeout)\n\n\t\t\treturn\n\t\t}\n\t\texpectedUnexposeReq = append(expectedUnexposeReq, tmpReq)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.Remove(containerID)\n\trequire.Error(t, err)\n\n\terrPortBinding := nat.PortBinding{\n\t\tHostIP:   hostIP2,\n\t\tHostPort: hostPort,\n\t}\n\tnestedErr := fmt.Errorf(\"%w: Test API error\", tracker.ErrAPI)\n\terrs := []error{\n\t\tfmt.Errorf(\"unexposing %+v failed: %w\", errPortBinding, nestedErr),\n\t}\n\texpectedErr := fmt.Errorf(\"%w: %+v\", forwarder.ErrUnexposeAPI, errs)\n\trequire.EqualError(t, err, expectedErr.Error())\n\n\tassert.ElementsMatch(t, expectedUnexposeReq, []*types.UnexposeRequest{\n\t\t{\n\t\t\tLocal:    ipPortBuilder(hostIP, hostPort),\n\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t},\n\t\t{\n\t\t\tLocal:    ipPortBuilder(hostIP3, hostPort),\n\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t},\n\t})\n\n\tactualPortMapping := apiTracker.Get(containerID)\n\tassert.Nil(t, actualPortMapping)\n}\n\nfunc TestRemoveAll(t *testing.T) {\n\tt.Parallel()\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tmux.HandleFunc(\"/services/forwarder/unexpose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tprotoPort2, err := nat.NewPort(protocolTCP, hostPort2)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\tportMapping2 := nat.PortMap{\n\t\tprotoPort2: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.Add(containerID2, portMapping2)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.RemoveAll()\n\trequire.NoError(t, err)\n\n\texpectedPortMapping1 := apiTracker.Get(containerID)\n\tassert.Nil(t, expectedPortMapping1)\n\n\texpectedPortMapping2 := apiTracker.Get(containerID2)\n\tassert.Nil(t, expectedPortMapping2)\n}\n\nfunc TestRemoveAllWithError(t *testing.T) {\n\tt.Parallel()\n\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tvar expectedUnexposeReq []*types.UnexposeRequest\n\n\tmux.HandleFunc(\"/services/forwarder/unexpose\", func(w http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.UnexposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\tif tmpReq.Local == ipPortBuilder(hostIP2, hostPort2) {\n\t\t\thttp.Error(w, \"RemoveAll API error\", http.StatusRequestTimeout)\n\n\t\t\treturn\n\t\t}\n\t\texpectedUnexposeReq = append(expectedUnexposeReq, tmpReq)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, true)\n\n\tprotoPort, err := nat.NewPort(protocolTCP, hostPort)\n\trequire.NoError(t, err)\n\n\tprotoPort2, err := nat.NewPort(protocolTCP, hostPort2)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP,\n\t\t\t\tHostPort: hostPort,\n\t\t\t},\n\t\t},\n\t}\n\tportMapping2 := nat.PortMap{\n\t\tprotoPort2: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   hostIP2,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t\t{\n\t\t\t\tHostIP:   hostIP3,\n\t\t\t\tHostPort: hostPort2,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.Add(containerID2, portMapping2)\n\trequire.NoError(t, err)\n\n\terr = apiTracker.RemoveAll()\n\trequire.Error(t, err)\n\n\terrPortBinding := nat.PortBinding{\n\t\tHostIP:   hostIP2,\n\t\tHostPort: hostPort2,\n\t}\n\tnestedErr := fmt.Errorf(\"%w: RemoveAll API error\", tracker.ErrAPI)\n\terrs := []error{\n\t\tfmt.Errorf(\"RemoveAll unexposing %+v failed: %w\", errPortBinding, nestedErr),\n\t}\n\texpectedErr := fmt.Errorf(\"%w: %+v\", forwarder.ErrUnexposeAPI, errs)\n\trequire.EqualError(t, err, expectedErr.Error())\n\n\tassert.ElementsMatch(t, expectedUnexposeReq, []*types.UnexposeRequest{\n\t\t{Local: ipPortBuilder(hostIP, hostPort)},\n\t\t{Local: ipPortBuilder(hostIP3, hostPort2)},\n\t})\n\n\texpectedPortMapping1 := apiTracker.Get(containerID)\n\tassert.Nil(t, expectedPortMapping1)\n\n\texpectedPortMapping2 := apiTracker.Get(containerID2)\n\tassert.Nil(t, expectedPortMapping2)\n}\n\nfunc TestNonAdminInstall(t *testing.T) {\n\tt.Parallel()\n\n\tmux := http.NewServeMux()\n\n\tvar expectedExposeReq []*types.ExposeRequest\n\n\tmux.HandleFunc(\"/services/forwarder/expose\", func(_ http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.ExposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\texpectedExposeReq = append(expectedExposeReq, tmpReq)\n\t})\n\n\tvar expectedUnexposeReq []*types.UnexposeRequest\n\n\tmux.HandleFunc(\"/services/forwarder/unexpose\", func(_ http.ResponseWriter, r *http.Request) {\n\t\tvar tmpReq *types.UnexposeRequest\n\t\terr := json.NewDecoder(r.Body).Decode(&tmpReq)\n\t\trequire.NoError(t, err)\n\t\texpectedUnexposeReq = append(expectedUnexposeReq, tmpReq)\n\t})\n\n\ttestSrv := httptest.NewServer(mux)\n\tdefer testSrv.Close()\n\n\tapiTracker := tracker.NewAPITracker(context.Background(), &testForwarder{}, testSrv.URL, hostSwitchIP, false)\n\n\tpublishedPort := \"1025\"\n\tprotoPort, err := nat.NewPort(protocolTCP, publishedPort)\n\trequire.NoError(t, err)\n\n\tportMapping := nat.PortMap{\n\t\tprotoPort: []nat.PortBinding{\n\t\t\t{\n\t\t\t\tHostIP:   \"192.168.0.124\",\n\t\t\t\tHostPort: publishedPort,\n\t\t\t},\n\t\t},\n\t}\n\n\terr = apiTracker.Add(containerID, portMapping)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, expectedExposeReq,\n\t\t[]*types.ExposeRequest{\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(\"127.0.0.1\", publishedPort),\n\t\t\t\tRemote:   ipPortBuilder(hostSwitchIP, publishedPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t},\n\t)\n\n\terr = apiTracker.Remove(containerID)\n\trequire.NoError(t, err)\n\n\tassert.ElementsMatch(t, expectedUnexposeReq,\n\n\t\t[]*types.UnexposeRequest{\n\t\t\t{\n\t\t\t\tLocal:    ipPortBuilder(\"127.0.0.1\", publishedPort),\n\t\t\t\tProtocol: types.TransportProtocol(protocolTCP),\n\t\t\t},\n\t\t})\n\n\tportMapping = apiTracker.Get(containerID)\n\tassert.Nil(t, portMapping)\n}\n\nfunc ipPortBuilder(ip, port string) string {\n\treturn ip + \":\" + port\n}\n\ntype testForwarder struct {\n\treceivedPortMappings []guestagentType.PortMapping\n\tsendErr              error\n\tfailCondition        func(guestagentType.PortMapping) error\n}\n\nfunc (v *testForwarder) Send(portMapping guestagentType.PortMapping) error {\n\tif v.failCondition != nil {\n\t\tif err := v.failCondition(portMapping); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tv.receivedPortMappings = append(v.receivedPortMappings, portMapping)\n\n\treturn v.sendErr\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/tracker/portstorage.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage tracker\n\nimport (\n\t\"maps\"\n\t\"sync\"\n\n\t\"github.com/Masterminds/log-go\"\n\t\"github.com/docker/go-connections/nat\"\n)\n\n// portStorage is responsible for storing all the port mappings.\ntype portStorage struct {\n\t// container ID is the key for both docker and containerd\n\tportmap map[string]nat.PortMap\n\tmutex   sync.Mutex\n}\n\nfunc newPortStorage() *portStorage {\n\treturn &portStorage{\n\t\tportmap: make(map[string]nat.PortMap),\n\t}\n}\n\nfunc (p *portStorage) add(containerID string, portMap nat.PortMap) {\n\tp.mutex.Lock()\n\tp.portmap[containerID] = portMap\n\tp.mutex.Unlock()\n\tlog.Debugf(\"portStorage add status: %+v\", p.portmap)\n}\n\nfunc (p *portStorage) get(containerID string) nat.PortMap {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\n\tlog.Debugf(\"portStorage get status: %+v\", p.portmap)\n\n\tif portMap, ok := p.portmap[containerID]; ok {\n\t\treturn portMap\n\t}\n\n\treturn nil\n}\n\nfunc (p *portStorage) removeAll() {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\n\tfor containerID, portMap := range p.portmap {\n\t\tlog.Debugf(\"removing the following container [%s] port binding: %+v\", containerID, portMap)\n\t\tdelete(p.portmap, containerID)\n\t}\n}\n\nfunc (p *portStorage) getAll() map[string]nat.PortMap {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\n\tportMappings := make(map[string]nat.PortMap, len(p.portmap))\n\n\tfor k, v := range p.portmap {\n\t\tportMappings[k] = maps.Clone(v)\n\t}\n\n\treturn portMappings\n}\n\nfunc (p *portStorage) remove(containerID string) {\n\tp.mutex.Lock()\n\tdefer p.mutex.Unlock()\n\tdelete(p.portmap, containerID)\n\tlog.Debugf(\"portStorage remove status: %+v\", p.portmap)\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/tracker/tracker.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package tracker implements a tracking mechanism to keep track\n// of the ports during various container event types e.g start, stop\npackage tracker\n\nimport \"github.com/docker/go-connections/nat\"\n\n// Tracker is the interface that includes all the functions that\n// are used to keep track of the port mappings plus NetTracker methods\n// that are used to keep track of the network listener creation and removal.\ntype Tracker interface {\n\t// Get returns a portMap using the containerID as a lookup Key.\n\tGet(containerID string) nat.PortMap\n\n\t// Add adds a portMap to the storage using the containerID as a Key.\n\t// It replaces all existing portMappings, without attempting to unbind listeners,\n\t// so the caller is responsible for calling Remove first if necessary.\n\tAdd(containerID string, portMapping nat.PortMap) error\n\n\t// Remove removes a portMap using the containerID as a key.\n\tRemove(containerID string) error\n\n\t// RemoveAll removes all the available portMappings in the storage.\n\tRemoveAll() error\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/types/README.md",
    "content": "# Rancher Desktop Agent Types\n\nThe Rancher Desktop types package represent the shared contract (json structure) that is used for communicating to the upstream Rancher Desktop Privileged Service.\n\nBelow is the json schema for PortMapping:\n\n## schema\n```json\n{\n  \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n  \"$ref\": \"#/$defs/PortMapping\",\n  \"$defs\": {\n    \"ConnectAddrs\": {\n      \"properties\": {\n        \"network\": {\n          \"type\": \"string\"\n        },\n        \"addr\": {\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"type\": \"object\",\n      \"required\": [\n        \"network\",\n        \"addr\"\n      ]\n    },\n    \"PortBinding\": {\n      \"properties\": {\n        \"HostIp\": {\n          \"type\": \"string\"\n        },\n        \"HostPort\": {\n          \"type\": \"string\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"type\": \"object\",\n      \"required\": [\n        \"HostIp\",\n        \"HostPort\"\n      ]\n    },\n    \"PortMap\": {\n      \"patternProperties\": {\n        \".*\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/PortBinding\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"type\": \"object\"\n    },\n    \"PortMapping\": {\n      \"properties\": {\n        \"remove\": {\n          \"type\": \"boolean\"\n        },\n        \"ports\": {\n          \"$ref\": \"#/$defs/PortMap\"\n        },\n        \"connectAddrs\": {\n          \"items\": {\n            \"$ref\": \"#/$defs/ConnectAddrs\"\n          },\n          \"type\": \"array\"\n        }\n      },\n      \"additionalProperties\": false,\n      \"type\": \"object\",\n      \"required\": [\n        \"remove\",\n        \"ports\",\n        \"connectAddrs\"\n      ]\n    }\n  }\n}\n```"
  },
  {
    "path": "src/go/guestagent/pkg/types/portmapping.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package types maintains common types that are used across\n// different packages.\npackage types\n\nimport \"github.com/docker/go-connections/nat\"\n\n// PortMapping represents the mapping of ports and addresses to be communicated\n// over the network. It includes a flag (remove) on whether to add or remove port mappings\n// and specifies the backend addresses to connect to.\ntype PortMapping struct {\n\t// Remove indicates whether the port mappings should be removed (true) or added (false)\n\tRemove bool `json:\"remove\"`\n\t// Ports contains the port mappings for both IPv4 and IPv6 addresses.  The host address\n\t// listed refers to the machine running the VM, i.e. the Windows machine.\n\tPorts nat.PortMap `json:\"ports\"`\n\t// ConnectAddrs lists the backend addresses for connections; the addresses are recorded\n\t// in terms of the network namespace the container engine is running in (i.e. the\n\t// \"Rancher Desktop\" network namespace).\n\tConnectAddrs []ConnectAddrs `json:\"connectAddrs\"`\n}\n\n// ConnectAddrs defines a network address used for the WSL interface inside\n// the VM. Typically, this address is found on the eth0 interface.\ntype ConnectAddrs struct {\n\t// Network specifies the protocol or network type for the address (e.g., \"tcp\", \"udp\")\n\tNetwork string `json:\"network\"`\n\t// Addr is the network address, which can be either IPv4 or IPv6 (e.g., \"192.0.2.1:25\", \"[2001:db8::1]:80\")\n\tAddr string `json:\"addr\"`\n}\n"
  },
  {
    "path": "src/go/guestagent/pkg/utils/utils.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage utils\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n)\n\nvar (\n\tErrExecIptablesRule  = errors.New(\"failed updating iptables rules\")\n\tErrIPAddressNotFound = errors.New(\"IP address not found in line\")\n)\n\n// NormalizeHostIP checks if the provided IP address is valid.\n// The valid options are \"127.0.0.1\" and \"0.0.0.0\". If the input is \"127.0.0.1\",\n// it returns \"127.0.0.1\". Any other address will be mapped to \"0.0.0.0\".\nfunc NormalizeHostIP(ip string) string {\n\tif ip == \"127.0.0.1\" || ip == \"localhost\" {\n\t\treturn ip\n\t}\n\treturn \"0.0.0.0\"\n}\n\nfunc GenerateID(entry string) string {\n\thasher := sha256.New()\n\thasher.Write([]byte(entry))\n\treturn hex.EncodeToString(hasher.Sum(nil))\n}\n"
  },
  {
    "path": "src/go/mock-wsl/README.md",
    "content": "# mock-wsl\n\nThis is a mock `wsl.exe` that is used in the E2E tests, used to stub out\ninteraction with the real WSL.\n\n## Configuration\n\nThe environment variable `RD_MOCK_WSL_DATA` should be set to the absolute path\nof a JSON file describing how the executable should act.  This file will be\nmodified as part of the run to contain the results and errors.\n\nPlease see [`schema.json`](./schema.json) for the JSON schema for the file.\n"
  },
  {
    "path": "src/go/mock-wsl/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/e2e/assets/mock-wsl\n\ngo 1.25.0\n\nrequire (\n\tgolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/text v0.35.0\n)\n"
  },
  {
    "path": "src/go/mock-wsl/go.sum",
    "content": "golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\n"
  },
  {
    "path": "src/go/mock-wsl/lock_file_other.go",
    "content": "//go:build !windows\n\n/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\nfunc lockFile(_ *os.File) error {\n\treturn fmt.Errorf(\"not implemented\")\n}\n"
  },
  {
    "path": "src/go/mock-wsl/lock_file_windows.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"os\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n// lockFile locks the given file for exclusive access; if the file is already\n// locked, this function will wait until it is unlocked.\nfunc lockFile(f *os.File) error {\n\terr := windows.LockFileEx(\n\t\twindows.Handle(f.Fd()),\n\t\twindows.LOCKFILE_EXCLUSIVE_LOCK,\n\t\t0,\n\t\tmath.MaxUint32, math.MaxUint32,\n\t\t&windows.Overlapped{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to lock file: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/mock-wsl/mock-wsl.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"golang.org/x/exp/slices\"\n\t\"golang.org/x/text/encoding/unicode\"\n)\n\nconst (\n\tModeSequential = \"sequential\"\n\tModeRepeated   = \"repeated\"\n\tModeDefault    = \"\"\n)\n\ntype commandEntry struct {\n\tArgs    []string `json:\"args\"`\n\tMode    string   `json:\"mode,omitempty\"`\n\tStdout  string   `json:\"stdout,omitempty\"`\n\tStderr  string   `json:\"stderr,omitempty\"`\n\tUTF16LE bool     `json:\"utf16le,omitempty\"`\n\tCode    int      `json:\"code,omitempty\"`\n}\n\ntype configStruct struct {\n\tCommands []commandEntry `json:\"commands\"`\n\tResults  []bool         `json:\"results,omitempty\"`\n\tErrors   []string       `json:\"errors,omitempty\"`\n}\n\nfunc writeFile(file *os.File, config *configStruct, errFmt string, v ...any) {\n\terrString := \"\"\n\tif errFmt != \"\" {\n\t\terrString = fmt.Sprintf(errFmt, v...)\n\t\tconfig.Errors = append(config.Errors, errString)\n\t}\n\tif _, err := file.Seek(0, io.SeekStart); err != nil {\n\t\tlog.Fatalf(\"Could not seek in config file: %s\", err)\n\t}\n\t// Don't bother truncating: we allow for junk at end of file.\n\tenc := json.NewEncoder(file)\n\tenc.SetIndent(\"\", \"    \")\n\tif err := enc.Encode(&config); err != nil {\n\t\tlog.Fatalf(\"Could not marshal results: %s\", err)\n\t}\n\tif err := file.Close(); err != nil {\n\t\tlog.Fatalf(\"Could not flush results: %s\", err)\n\t}\n\tif errString != \"\" {\n\t\tlog.Fatal(errString)\n\t}\n}\n\nfunc main() {\n\tcode, err := func() (int, error) {\n\t\tconfPath, ok := os.LookupEnv(\"RD_MOCK_WSL_DATA\")\n\t\tif !ok {\n\t\t\treturn 0, fmt.Errorf(\"RD_MOCK_WSL_DATA not set\")\n\t\t}\n\n\t\tvar config configStruct\n\t\tfile, err := os.OpenFile(confPath, os.O_RDWR, 0)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to open config file %s: %w\", confPath, err)\n\t\t}\n\t\tdefer file.Close()\n\t\tif err = lockFile(file); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to lock config file %s: %w\", confPath, err)\n\t\t}\n\t\tdecoder := json.NewDecoder(file)\n\t\tdecoder.DisallowUnknownFields()\n\t\tif err = decoder.Decode(&config); err != nil {\n\t\t\treturn 0, fmt.Errorf(\"failed to unmarshal config file %s: %w\", confPath, err)\n\t\t}\n\n\t\tif len(config.Commands) < 1 {\n\t\t\twriteFile(file, &config, \"Could not find any commands\")\n\t\t}\n\t\tif len(config.Results) < len(config.Commands) {\n\t\t\tnewResults := make([]bool, len(config.Commands)-len(config.Results))\n\t\t\tconfig.Results = append(config.Results, newResults...)\n\t\t}\n\n\t\tindex := 0\n\t\tmatched := false\n\t\tcmd := commandEntry{}\n\t\targs := os.Args[1:]\n\n\t\tmatchSequential := true\n\t\tfor index, cmd = range config.Commands {\n\t\t\tif !slices.Equal(args, cmd.Args) {\n\t\t\t\tif !config.Results[index] {\n\t\t\t\t\tmatchSequential = false\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch cmd.Mode {\n\t\t\tcase ModeSequential:\n\t\t\t\tif matchSequential && !config.Results[index] {\n\t\t\t\t\tmatched = true\n\t\t\t\t}\n\t\t\tcase ModeRepeated:\n\t\t\t\tmatched = true\n\t\t\tcase ModeDefault:\n\t\t\t\tif !config.Results[index] {\n\t\t\t\t\tmatched = true\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\twriteFile(file, &config, \"Command %d (%s) has invalid mode %s\",\n\t\t\t\t\tindex, strings.Join(cmd.Args, \" \"), cmd.Mode)\n\t\t\t}\n\t\t\tif matched {\n\t\t\t\tbreak\n\t\t\t} else if !config.Results[index] {\n\t\t\t\tmatchSequential = false\n\t\t\t}\n\t\t}\n\n\t\tif !matched {\n\t\t\twriteFile(file, &config, \"Could not find command with args %s\",\n\t\t\t\tstrings.Join(args, \" \"))\n\t\t}\n\t\tconfig.Results[index] = true\n\n\t\tencoding := unicode.UTF8\n\t\tif cmd.UTF16LE {\n\t\t\tencoding = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)\n\t\t}\n\t\tencoder := encoding.NewEncoder()\n\n\t\tif cmd.Stdout != \"\" {\n\t\t\ttext, err := encoder.String(cmd.Stdout)\n\t\t\tif err != nil {\n\t\t\t\twriteFile(file, &config, \"failed to encode stdout: %s\", err)\n\t\t\t}\n\t\t\tfmt.Fprint(os.Stdout, text)\n\t\t}\n\t\tif cmd.Stderr != \"\" {\n\t\t\ttext, err := encoder.String(cmd.Stderr)\n\t\t\tif err != nil {\n\t\t\t\twriteFile(file, &config, \"failed to encode stderr: %s\", err)\n\t\t\t}\n\t\t\tfmt.Fprint(os.Stderr, text)\n\t\t}\n\n\t\twriteFile(file, &config, \"\")\n\t\treturn cmd.Code, nil\n\t}()\n\tif err != nil {\n\t\tlog.Fatalf(\"%s\", err)\n\t}\n\tos.Exit(code)\n}\n"
  },
  {
    "path": "src/go/mock-wsl/schema.json",
    "content": "{\n    \"$schema\": \"http://json-schema.org/schema\",\n    \"$id\": \"https://github.com/rancher-sandbox/rancher-desktop/src/go/mock-wsl/schema.json\",\n    \"title\": \"Rancher Desktop Mock WSL stub\",\n    \"description\": \"mock-wsl configuration\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"commands\": {\n            \"description\": \"Commands that can be matched\",\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"object\",\n                \"description\": \"One command to be matched\",\n                \"properties\": {\n                    \"args\": {\n                        \"description\": \"Arguments to match, excluding the executable name.\",\n                        \"type\": \"array\",\n                        \"items\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"mode\": {\n                        \"description\": \"If not given, this command can be matched at most once; however, it is permissible for some of the previous commands to not have been already matched.\",\n                        \"oneOf\": [\n                            {\n                                \"type\": \"string\",\n                                \"const\": \"sequential\",\n                                \"description\": \"All previous commands must have been matched (at least once) before this command will match.\"\n                            },\n                            {\n                                \"type\": \"string\",\n                                \"const\": \"repeated\",\n                                \"description\": \"This command may match multiple times; only the results from the last match will be stored. Note: This means that no further commands with the same args will ever match.\",\n                                \"markdownDescription\": \"This command may match multiple times; only the results from the last match will be stored. *Note:* This means that no further commands with the same `args` will ever match.\"\n                            }\n                        ]\n                    },\n                    \"stdout\": {\n                        \"description\": \"This will be emitted on standard output.\",\n                        \"type\": \"string\"\n                    },\n                    \"stderr\": {\n                        \"description\": \"This will be emitted on standard error, after the standard output (if any).\",\n                        \"type\": \"string\"\n                    },\n                    \"utf16le\": {\n                        \"description\": \"If given, stdout and stderr will be converted to UTF-16 LE before output.\",\n                        \"type\": \"boolean\",\n                        \"default\": false\n                    },\n                    \"code\": {\n                        \"description\": \"This will be the exit code of the process; if not given, 0 is assumed.\",\n                        \"type\": \"number\",\n                        \"default\": 0\n                    }\n                }\n            },\n            \"required\": [\n                \"args\"\n            ]\n        },\n        \"results\": {\n            \"description\": \"The results are a sequence of booleans. This does not need to be given initially; it will be added.\",\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"boolean\",\n                \"description\": \"Set to true if the command was run, and false otherwise.\",\n                \"markdownDescription\": \"Set to `true` if the command was run, and `false` otherwise.\"\n            }\n        },\n        \"errors\": {\n            \"description\": \"A sequence of commands that failed to match. This does not need to be given initially; it will be added as needed.\",\n            \"type\": \"array\",\n            \"items\": {\n                \"type\": \"string\"\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/README.md",
    "content": "# nerdctl-stub\n\nThis is a stub executable used to launch nerdctl on Windows (and WSL).\n\n## Usage\n\nUse like normal nerdctl, except that some things can be controlled via\nenvironment variables:\n\nVariable | Meaning | Default\n--- | --- | ---\nRD_WSL_DISTRO | WSL distribution to run in | `rancher-desktop`\nRD_NERDCTL | `nerdctl` executable | `/usr/local/bin/nerdctl`\n"
  },
  {
    "path": "src/go/nerdctl-stub/command_handlers.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-multierror\"\n)\n\n// This file contains handlers for specific commands.\n\n// fileOrUrlOrStdin handles arguments of kind `file|URL|-`.  It returns a\n// mounted path (or the arg as-is), any cleanups, and errors.\nfunc fileOrUrlOrStdin(input string, argHandlers argHandlersType) (string, []cleanupFunc, error) {\n\tif input == \"-\" {\n\t\treturn input, nil, nil\n\t}\n\tif match, _ := regexp.MatchString(`^[^:/]*://`, input); match {\n\t\t// input is a URL\n\t\treturn input, nil, nil\n\t}\n\tnewPath, cleanups, err := argHandlers.filePathArgHandler(input)\n\tif err != nil {\n\t\tif cleanupErr := runCleanups(cleanups); cleanupErr != nil {\n\t\t\terr = multierror.Append(err, cleanupErr)\n\t\t}\n\t\treturn input, nil, err\n\t}\n\treturn newPath, cleanups, nil\n}\n\n// builderBuildHandler handles `nerdctl image build`\nfunc builderBuildHandler(c *commandDefinition, args []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t// nerdctl image build [flags] PATH\n\t// The first argument is the directory to build; the rest are ignored.\n\tif len(args) < 1 {\n\t\t// This will return an error\n\t\treturn &parsedArgs{args: args}, nil\n\t}\n\tnewPath, cleanups, err := fileOrUrlOrStdin(args[0], argHandlers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &parsedArgs{args: append([]string{newPath}, args[1:]...), cleanup: cleanups}, nil\n}\n\n// hostPathResult is the return value of a hostPathDeterminerFunc that is used\n// in containerCopyHandler for determining which argument is the host path that\n// must be munged.\ntype hostPathResult int\n\nconst (\n\thostPathUnknown = hostPathResult(iota)\n\thostPathCurrent = hostPathResult(iota)\n\thostPathOther   = hostPathResult(iota)\n\thostPathNeither = hostPathResult(iota)\n)\n\n// containerCopyHandler handles `nerdctl container cp`\nfunc containerCopyHandler(c *commandDefinition, args []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\tvar resultArgs []string\n\tvar cleanups []cleanupFunc\n\tvar paths []string\n\n\t// Positional arguments `nerdctl container cp` are all paths, whether inside\n\t// the container or outside.\n\n\tfor _, arg := range args {\n\t\tif arg == \"-\" || !strings.HasPrefix(arg, \"-\") {\n\t\t\t// If the arg is \"-\" (stdin/stdout) or doesn't start with -, it's a path.\n\t\t\tpaths = append(paths, arg)\n\t\t} else {\n\t\t\tresultArgs = append(resultArgs, arg)\n\t\t}\n\t}\n\n\tif len(paths) != 2 {\n\t\t// We should have exactly one source and one destination... just fail\n\t\terr := fmt.Errorf(\"accepts 2 args, received %d\", len(paths))\n\t\tif cleanupErr := runCleanups(cleanups); cleanupErr != nil {\n\t\t\terr = multierror.Append(err, cleanupErr)\n\t\t}\n\t\treturn nil, err\n\t}\n\n\thostPathDeterminerFuncs := []func(i int, p string) hostPathResult{\n\t\tfunc(i int, p string) hostPathResult {\n\t\t\tif p == \"-\" {\n\t\t\t\t// If one argument is \"-\", the other must be a container path, so\n\t\t\t\t// neither needs to be modified.\n\t\t\t\treturn hostPathNeither\n\t\t\t}\n\t\t\treturn hostPathUnknown\n\t\t},\n\t\tfunc(i int, p string) hostPathResult {\n\t\t\tcolon := strings.Index(p, \":\")\n\t\t\tif colon < 1 {\n\t\t\t\t// If there's no colon in the path specification at all, or if the\n\t\t\t\t// string starts with a colon (which is invalid), then this must not be\n\t\t\t\t// a container path (and therefore the other one is).\n\t\t\t\treturn hostPathCurrent\n\t\t\t}\n\t\t\treturn hostPathUnknown\n\t\t},\n\t\tfunc(i int, p string) hostPathResult {\n\t\t\tcolon := strings.Index(p, \":\")\n\t\t\tif colon > 1 {\n\t\t\t\t// There's multiple characters before the first colon; this is a container\n\t\t\t\t// path specification (foo:/path/in/container), so the other must be a\n\t\t\t\t// host path specification.\n\t\t\t\treturn hostPathOther\n\t\t\t}\n\t\t\treturn hostPathUnknown\n\t\t},\n\t\tfunc(i int, p string) hostPathResult {\n\t\t\tif strings.Index(p, \":\") != 1 {\n\t\t\t\t// Shouldn't get here -- one of the two previous functions should have\n\t\t\t\t// found something already.\n\t\t\t\tpanic(fmt.Sprintf(\"Expected path %q to start with a character followed by a colon!\", p))\n\t\t\t}\n\t\t\tif i != 0 {\n\t\t\t\tpanic(\"Should not reach this on second path\")\n\t\t\t}\n\t\t\t// Fall back: the first element should be treated as the container path.\n\t\t\treturn hostPathOther\n\t\t},\n\t}\n\nfunctionLoop:\n\tfor _, f := range hostPathDeterminerFuncs {\n\t\tfor i, p := range paths {\n\t\t\tresult := f(i, p)\n\t\t\thostPathIndex := i\n\t\t\tswitch result {\n\t\t\tcase hostPathNeither:\n\t\t\t\t//nolint:gocritic // We break the loop once we are done appending\n\t\t\t\tresultArgs = append(resultArgs, paths...)\n\t\t\t\tbreak functionLoop\n\t\t\tcase hostPathUnknown:\n\t\t\t\tcontinue\n\t\t\tcase hostPathOther:\n\t\t\t\thostPathIndex = 1 - i\n\t\t\t}\n\n\t\t\t// If we reach here, we found the host path to munge.\n\t\t\t// Modify the path in-place.\n\t\t\tnewPath, newCleanups, err := argHandlers.filePathArgHandler(paths[hostPathIndex])\n\t\t\tcleanups = append(cleanups, newCleanups...)\n\t\t\tif err != nil {\n\t\t\t\tif cleanupErr := runCleanups(cleanups); cleanupErr != nil {\n\t\t\t\t\terr = multierror.Append(err, cleanupErr)\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tpaths[hostPathIndex] = newPath\n\t\t\t//nolint:gocritic // We break the loop once we are done appending\n\t\t\tresultArgs = append(resultArgs, paths...)\n\t\t\tbreak functionLoop\n\t\t}\n\t}\n\n\treturn &parsedArgs{args: resultArgs, cleanup: cleanups}, nil\n}\n\n// imageImportHandler handles `nerdctl image import`\nfunc imageImportHandler(c *commandDefinition, args []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t// nerdctl image import [OPTIONS] file|URL|- [REPOSITORY[:TAG]] [flags]\n\tif len(args) < 1 {\n\t\t// This will return an error\n\t\treturn &parsedArgs{args: args}, nil\n\t}\n\tnewPath, cleanups, err := fileOrUrlOrStdin(args[0], argHandlers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &parsedArgs{args: append([]string{newPath}, args[1:]...), cleanup: cleanups}, nil\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/command_handlers_test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBuilderBuildHandler(t *testing.T) {\n\tt.Run(\"munges the image directory\", func(t *testing.T) {\n\t\thandlers := argHandlersType{\n\t\t\tfilePathArgHandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\treturn \"<<path>>\", nil, nil\n\t\t\t},\n\t\t}\n\t\tparsed, err := builderBuildHandler(nil, []string{\"path\"}, handlers)\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, []string{\"<<path>>\"}, parsed.args)\n\t\tassert.Nil(t, parsed.cleanup)\n\t})\n\tt.Run(\"handles errors from munging\", func(t *testing.T) {\n\t\thandlerError := fmt.Errorf(\"some handler error\")\n\t\tcleanupError := fmt.Errorf(\"some cleanup error\")\n\t\thandlers := argHandlersType{\n\t\t\tfilePathArgHandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\treturn \"\", []cleanupFunc{func() error { return cleanupError }}, handlerError\n\t\t\t},\n\t\t}\n\t\t_, err := builderBuildHandler(nil, []string{\"path\"}, handlers)\n\t\tassert.Error(t, err)\n\t\tassert.ErrorContains(t, err, handlerError.Error())\n\t\tassert.ErrorContains(t, err, cleanupError.Error())\n\t})\n}\n\nfunc TestContainerCopyHandler(t *testing.T) {\n\tt.Parallel()\n\ttype testCaseType struct {\n\t\tinput    []string\n\t\texpected []string\n\t\terr      string\n\t}\n\n\ttestCases := []testCaseType{\n\t\t{\n\t\t\tinput:    []string{\"-\", \"c:file\"},\n\t\t\texpected: []string{\"-\", \"c:file\"},\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"c:file\", \"-\"},\n\t\t\texpected: []string{\"c:file\", \"-\"},\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"input\", \"c:file\"},\n\t\t\texpected: []string{\"<input>\", \"c:file\"},\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"c:file\", \"input\"},\n\t\t\texpected: []string{\"c:file\", \"<input>\"},\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"container:path\", \"c:file\"},\n\t\t\texpected: []string{\"container:path\", \"<c:file>\"},\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"c:file\", \"container:path\"},\n\t\t\texpected: []string{\"<c:file>\", \"container:path\"},\n\t\t},\n\t\t{\n\t\t\t// Check fallback: if ambiguous, assume copying out of a container.\n\t\t\tinput:    []string{\"c:file\", \"d:file\"},\n\t\t\texpected: []string{\"c:file\", \"<d:file>\"},\n\t\t},\n\t\t{\n\t\t\tinput: []string{\"missing argument\"},\n\t\t\terr:   \"accepts 2 args, received 1\",\n\t\t},\n\t\t{\n\t\t\tinput: []string{\"missing positional argument\", \"-flag\"},\n\t\t\terr:   \"accepts 2 args, received 1\",\n\t\t},\n\t\t{\n\t\t\tinput:    []string{\"-flag1\", \"c:file\", \"-flag2\", \"input\", \"-flag3\"},\n\t\t\texpected: []string{\"-flag1\", \"-flag2\", \"-flag3\", \"c:file\", \"<input>\"},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tfunc(testCase testCaseType) {\n\t\t\tt.Run(strings.Join(testCase.input, \"/\"), func(t *testing.T) {\n\t\t\t\tt.Parallel()\n\t\t\t\tranHandler := false\n\t\t\t\tranCleanups := false\n\t\t\t\thandlers := argHandlersType{\n\t\t\t\t\tfilePathArgHandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\t\t\tranHandler = true\n\t\t\t\t\t\treturn fmt.Sprintf(\"<%s>\", s), []cleanupFunc{func() error {\n\t\t\t\t\t\t\tranCleanups = true\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}}, nil\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tresult, err := containerCopyHandler(nil, testCase.input, handlers)\n\t\t\t\tif testCase.err != \"\" {\n\t\t\t\t\tassert.EqualError(t, err, testCase.err)\n\t\t\t\t\tif ranHandler {\n\t\t\t\t\t\tassert.True(t, ranCleanups)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tassert.NoError(t, err, \"Unexpected error running copy handler\")\n\t\t\t\t\tassert.Equal(t, testCase.expected, result.args)\n\t\t\t\t\tif ranHandler {\n\t\t\t\t\t\tassert.NotEmpty(t, result.cleanup)\n\t\t\t\t\t\tassert.False(t, ranCleanups)\n\t\t\t\t\t\tassert.NoError(t, runCleanups(result.cleanup))\n\t\t\t\t\t\tassert.True(t, ranCleanups)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tassert.Empty(t, result.cleanup)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t})\n\t\t}(testCase)\n\t}\n\n\tt.Run(\"cleanup errors\", func(t *testing.T) {\n\t\thandlerError := fmt.Errorf(\"some handler error\")\n\t\tcleanupError := fmt.Errorf(\"some cleanup error\")\n\t\thandlers := argHandlersType{\n\t\t\tfilePathArgHandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\treturn \"\", []cleanupFunc{func() error { return cleanupError }}, handlerError\n\t\t\t},\n\t\t}\n\t\t_, err := containerCopyHandler(nil, []string{\"host\", \"container:/path\"}, handlers)\n\t\tassert.ErrorContains(t, err, handlerError.Error())\n\t\tassert.ErrorContains(t, err, cleanupError.Error())\n\t})\n}\n\nfunc TestImageImportHandler(t *testing.T) {\n\tcleanupError := fmt.Errorf(\"cleanup error\")\n\ttestCases := []struct {\n\t\tdescription   string\n\t\tinput         []string\n\t\texpected      []string\n\t\thandler       func(s string) (string, []cleanupFunc, error)\n\t\tassertCleanup func(*testing.T, []cleanupFunc)\n\t}{\n\t\t{\n\t\t\tdescription: \"ignore missing arguments\",\n\t\t\tinput:       []string{},\n\t\t\texpected:    []string{},\n\t\t},\n\t\t{\n\t\t\tdescription: \"accepts stdin\",\n\t\t\tinput:       []string{\"-\"},\n\t\t\texpected:    []string{\"-\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"accepts URLs\",\n\t\t\tinput:       []string{\"https://registry.example.com/hello\"},\n\t\t\texpected:    []string{\"https://registry.example.com/hello\"},\n\t\t},\n\t\t{\n\t\t\tdescription: \"accepts paths\",\n\t\t\tinput:       []string{\"hello\"},\n\t\t\texpected:    []string{\"<<hello>>\"},\n\t\t\thandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\treturn \"<<hello>>\", []cleanupFunc{func() error { return cleanupError }}, nil\n\t\t\t},\n\t\t\tassertCleanup: func(t *testing.T, cf []cleanupFunc) {\n\t\t\t\tif assert.Len(t, cf, 1) {\n\t\t\t\t\tassert.ErrorIs(t, cf[0](), cleanupError)\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Run(testCase.description, func(t *testing.T) {\n\t\t\thandlers := argHandlersType{\n\t\t\t\tfilePathArgHandler: func(s string) (string, []cleanupFunc, error) {\n\t\t\t\t\tpanic(\"should not be called\")\n\t\t\t\t},\n\t\t\t}\n\t\t\tif testCase.handler != nil {\n\t\t\t\thandlers.filePathArgHandler = testCase.handler\n\t\t\t}\n\t\t\tparsed, err := imageImportHandler(nil, testCase.input, handlers)\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.EqualValues(t, testCase.expected, parsed.args)\n\t\t\tif testCase.assertCleanup != nil {\n\t\t\t\ttestCase.assertCleanup(t, parsed.cleanup)\n\t\t\t} else {\n\t\t\t\tassert.Zero(t, parsed.cleanup)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/debugging.go",
    "content": "//go:build debug\n\n/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"sort\"\n)\n\n// describeCommands is a debugging function that prints out all commands.\n// This is normally never called, but we keep this implemented as it is useful\n// for debugging.\nfunc describeCommands() {\n\thandlerNames := make(map[string]string)\n\thandlerNames[fmt.Sprintf(\"%v\", nil)] = \"~\"\n\t// The next few lines should ignore govet's \"printf\" lint because we are\n\t// intentionally printing a function instead of calling it.\n\thandlerNames[fmt.Sprintf(\"%v\", ignoredArgHandler)] = \"ignored\"        //nolint:govet,printf\n\thandlerNames[fmt.Sprintf(\"%v\", volumeArgHandler)] = \"volume\"          //nolint:govet,printf\n\thandlerNames[fmt.Sprintf(\"%v\", filePathArgHandler)] = \"file path\"     //nolint:govet,printf\n\thandlerNames[fmt.Sprintf(\"%v\", outputPathArgHandler)] = \"output path\" //nolint:govet,printf\n\n\tlog.Println(\"========== COMMAND STRUCTURE ==========\")\n\tvar paths []string\n\tfor path := range commands {\n\t\tpaths = append(paths, path)\n\t}\n\tsort.Strings(paths)\n\tfor _, path := range paths {\n\t\tcommand := commands[path]\n\t\tlog.Printf(\"%-20s %v\", path, command.handler) //nolint:govet,printf\n\t\tvar optionNames []string\n\t\tfor optionName := range command.options {\n\t\t\toptionNames = append(optionNames, optionName)\n\t\t}\n\t\tsort.Strings(optionNames)\n\t\tfor _, optionName := range optionNames {\n\t\t\thandler := command.options[optionName]\n\t\t\thandlerName, ok := handlerNames[fmt.Sprintf(\"%v\", handler)]\n\t\t\tif !ok {\n\t\t\t\thandlerName = \"<invalid handler>\"\n\t\t\t}\n\t\t\tlog.Printf(\"%20s %s\", optionName, handlerName)\n\t\t}\n\t}\n\tlog.Println(\"========== END COMMAND STRUCTURE ==========\")\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/debugging_stub.go",
    "content": "//go:build !debug\n\n/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\n// describeCommands is a debugging function that prints out all commands.\n// This implementation is a stub that does nothing.\nfunc describeCommands() {\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/generate/README.md",
    "content": "# nerdctl-sub/generate\n\nThis directory contains a tool that generates the argument parser for\nnerdctl-stub (by parsing the output of `nerdctl -help`).\n\n## Usage\n\n```powershell\nyarn generate:nerdctl-stub\n```\n"
  },
  {
    "path": "src/go/nerdctl-stub/generate/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/nerdctl-stub/generate\n\ngo 1.25.0\n\nrequire github.com/sirupsen/logrus v1.9.4\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/stretchr/testify v1.11.1 // indirect\n\tgolang.org/x/sys v0.42.0 // indirect\n)\n"
  },
  {
    "path": "src/go/nerdctl-stub/generate/go.sum",
    "content": "github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "src/go/nerdctl-stub/generate/main_linux.go",
    "content": "// package main produces stubs for the nerdctl subcommands (and their\n// options); this is expected to be overridden for options that involve paths.\n// All options generated this will have their values ignored.\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/template\"\n\t\"unicode\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// nerdctl contains the path to the nerdctl binary to run.\nvar nerdctl = \"/usr/local/bin/nerdctl\"\n\n// outputPath is the file we should generate.\nvar outputPath = \"../nerdctl_commands_generated.go\"\n\ntype helpData struct {\n\t// Commands lists the subcommands available\n\tCommands []string\n\t// options available for this command; the key is the long option\n\t// (`--version`) or the short option (`-v`), and the value is whether the\n\t// option takes an argument.\n\tOptions map[string]bool\n\t// If set, this command can have subcommands; this alters argument parsing.\n\tcanHaveSubcommands bool\n\t// If set, this command can pass flags to foreign commands, as in `nerdctl run`.\n\t// This alters argument parsing by letting us ignore unknown flags.\n\tHasForeignFlags bool\n\t// mergedOptions includes local options plus inherited options.\n\tmergedOptions map[string]struct{}\n}\n\n// prologueTemplate describes the file header for the generated file.\nconst prologueTemplate = `\n// Code generated by {{ .package }} - DO NOT EDIT.\n\n// package main implements a stub for nerdctl\npackage main\n\n// commands supported by nerdctl; the key here is a space-separated subcommand\n// path to reach the given subcommand (where the root command is empty).\nvar commands = map[string]commandDefinition {\n`\n\n// epilogueTemplate describes the file trailer for the generated file.\nconst epilogueTemplate = `\n}\n`\n\nfunc main() {\n\tverbose := flag.Bool(\"verbose\", false, \"extra logging\")\n\tflag.Parse()\n\tif *verbose {\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t}\n\n\toutput, err := os.Create(outputPath)\n\tif err != nil {\n\t\tlogrus.WithError(err).WithField(\"path\", outputPath).Fatal(\"error creating output\")\n\t}\n\tdefer output.Close()\n\t//nolint:dogsled // we only require the file name; we can also ignore `ok`, as\n\t// on failure we just have no useful file name.\n\t_, filename, _, _ := runtime.Caller(0)\n\tdata := map[string]interface{}{\n\t\t\"package\": filename,\n\t}\n\tif buildInfo, ok := debug.ReadBuildInfo(); ok {\n\t\tdata[\"package\"] = buildInfo.Main.Path\n\t}\n\terr = template.Must(template.New(\"\").Parse(prologueTemplate)).Execute(output, data)\n\tif err != nil {\n\t\tlogrus.WithError(err).Fatal(\"could not execute prologue\")\n\t}\n\terr = buildSubcommand(context.Background(), []string{}, helpData{}, output)\n\tif err != nil {\n\t\tlogrus.WithError(err).Fatal(\"could not build subcommands\")\n\t}\n\terr = template.Must(template.New(\"\").Parse(epilogueTemplate)).Execute(output, data)\n\tif err != nil {\n\t\tlogrus.WithError(err).Fatal(\"could not execute epilogue\")\n\t}\n}\n\n// buildSubcommand generates the option parser data for a given subcommand.\n// args provides the list of arguments to get to the subcommand; the last\n// element in the slice is the name of the subcommand.\n// writer is the file into which the result should be written; it is expected that `go fmt`\n// will be run on it eventually.\nfunc buildSubcommand(ctx context.Context, args []string, parentData helpData, writer io.Writer) error {\n\thelp, err := getHelp(ctx, args)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting help for %v: %w\", args, err)\n\t}\n\tsubcommands := parseHelp(help, parentData)\n\n\tfields := logrus.Fields{\"args\": args}\n\tif subcommands.HasForeignFlags {\n\t\tfields[\"type\"] = \"arguments\"\n\t} else if !subcommands.canHaveSubcommands {\n\t\tfields[\"type\"] = \"positional\"\n\t}\n\tlogrus.WithFields(fields).Trace(\"building subcommand\")\n\n\tif !subcommands.canHaveSubcommands && len(subcommands.Commands) > 0 {\n\t\treturn fmt.Errorf(\"invalid command %v: has positional arguments, but also subcommands %+v\", args, subcommands.Commands)\n\t}\n\tif subcommands.canHaveSubcommands && subcommands.HasForeignFlags {\n\t\treturn fmt.Errorf(\"invalid command %v: has subcommands and foreign flags\", args)\n\t}\n\n\terr = emitCommand(args, subcommands, writer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, subcommand := range subcommands.Commands {\n\t\tnewArgs := make([]string, 0, len(args))\n\t\tnewArgs = append(newArgs, args...)\n\t\tnewArgs = append(newArgs, subcommand)\n\t\terr := buildSubcommand(ctx, newArgs, subcommands, writer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// getHelp runs `nerdctl <args...> -help` and returns the result.\nfunc getHelp(ctx context.Context, args []string) (string, error) {\n\tnewArgs := make([]string, 0, len(args)+1)\n\tnewArgs = append(newArgs, args...)\n\tnewArgs = append(newArgs, \"--help\")\n\tcmd := exec.CommandContext(ctx, nerdctl, newArgs...)\n\tcmd.Stderr = os.Stderr\n\tresult, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(result), nil\n}\n\nconst (\n\tSTATE_OTHER = iota\n\tSTATE_COMMANDS\n\tSTATE_OPTIONS\n)\n\n// parseHelp consumes the output of `nerdctl help` (possibly for a subcommand)\n// and returns the available subcommands and options.\nfunc parseHelp(help string, parentData helpData) helpData {\n\tresult := helpData{Options: make(map[string]bool), mergedOptions: make(map[string]struct{})}\n\tfor k := range parentData.mergedOptions {\n\t\tresult.mergedOptions[k] = struct{}{}\n\t}\n\tstate := STATE_OTHER\n\tfor _, line := range strings.Split(help, \"\\n\") {\n\t\tline = strings.TrimRightFunc(line, unicode.IsSpace)\n\t\tif line == \"\" {\n\t\t\t// Skip empty lines (don't switch state either)\n\t\t\tcontinue\n\t\t}\n\t\tif !strings.HasPrefix(line, \" \") {\n\t\t\t// Line does not start with a space; it's a section header.\n\t\t\tif strings.HasSuffix(strings.ToUpper(line), \"COMMANDS:\") {\n\t\t\t\tstate = STATE_COMMANDS\n\t\t\t} else if strings.HasSuffix(strings.ToUpper(line), \"FLAGS:\") {\n\t\t\t\tstate = STATE_OPTIONS\n\t\t\t} else if strings.HasPrefix(strings.ToUpper(line), \"USAGE:\") {\n\t\t\t\t// Usage is on the same line, so we have to process this now.\n\t\t\t\t// Command is `nerdctl subcommand [flags]`; anything after `[flags]` is\n\t\t\t\t// assumed to be positional arguments.\n\t\t\t\t_, newArgs, _ := strings.Cut(strings.ToUpper(line), \"[FLAGS]\")\n\t\t\t\tnewArgs = strings.TrimSpace(newArgs)\n\t\t\t\t// Unlike everything else, `nerdctl compose` has a usage string of\n\t\t\t\t// `nerdctl compose [flags] COMMAND` so we need to ignore that.\n\t\t\t\tif newArgs == \"\" || newArgs == \"COMMAND\" {\n\t\t\t\t\tresult.canHaveSubcommands = true\n\t\t\t\t} else if strings.Contains(newArgs, \"COMMAND\") && strings.Contains(newArgs, \"...\") {\n\t\t\t\t\tresult.HasForeignFlags = true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tstate = STATE_OTHER\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tline = strings.TrimLeftFunc(line, unicode.IsSpace)\n\t\tif state == STATE_COMMANDS {\n\t\t\tparts := strings.SplitN(line, \"  \", 2)\n\t\t\tif len(parts) < 2 {\n\t\t\t\t// This line does not contain a command.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\twords := strings.Split(strings.TrimSpace(parts[0]), \", \")\n\t\t\tresult.Commands = append(result.Commands, words...)\n\t\t} else if state == STATE_OPTIONS {\n\t\t\tparts := strings.SplitN(line, \"  \", 2)\n\t\t\tif len(parts) < 2 {\n\t\t\t\t// This line does not contain an option.\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// The flags help has the format: `-f, --foo string   Description`\n\t\t\t// In order to figure out if the option takes arguments, we need to\n\t\t\t// parse the whole line first.\n\t\t\tvar words []string\n\t\t\thasOptions := false\n\t\t\tfor _, word := range strings.Split(strings.TrimSpace(parts[0]), \", \") {\n\t\t\t\tspaceIndex := strings.Index(word, \" \")\n\t\t\t\tif spaceIndex > -1 {\n\t\t\t\t\thasOptions = true\n\t\t\t\t\tword = word[:spaceIndex]\n\t\t\t\t}\n\t\t\t\twords = append(words, word)\n\t\t\t}\n\t\t\t// We may find an inherited flag; skip if the long option exists in\n\t\t\t// the parent\n\t\t\tif len(words) < 1 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, ok := parentData.mergedOptions[words[len(words)-1]]; !ok {\n\t\t\t\tfor _, word := range words {\n\t\t\t\t\tresult.Options[word] = hasOptions\n\t\t\t\t\tresult.mergedOptions[word] = struct{}{}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tsort.Strings(result.Commands)\n\treturn result\n}\n\n// commandTemplate is the text/template template for a single subcommand.\nconst commandTemplate = `\n\t{{ printf \"%q\" .Args }}: {\n\t\tcommandPath: {{ printf \"%q\" .Args }},\n\t\tsubcommands: map[string]struct{} {\n\t\t\t{{- range .Data.Commands }}\n\t\t\t\t{{ printf \"%q\" . }}: {},\n\t\t\t{{- end }}\n\t\t},\n\t\toptions: map[string]argHandler {\n\t\t\t{{ range $k, $v := .Data.Options }}\n\t\t\t\t{{- printf \"%q\" $k -}}: {{ if $v -}} ignoredArgHandler {{- else -}} nil {{- end -}},\n\t\t\t{{ end }}\n\t\t},\n\t\t{{- if .Data.HasForeignFlags }}\n\t\t\thasForeignFlags: true,\n\t\t{{- end }}\n\t},\n`\n\n// commandTemplateInput describes the data that will be fed to commandTemplate.\ntype commandTemplateInput struct {\n\tArgs string\n\tData helpData\n}\n\n// emitCommand outputs the golang code to the given writer.  args indicates the\n// arguments to reach this subcommand, and data is the parsed help output.\nfunc emitCommand(args []string, data helpData, writer io.Writer) error {\n\ttemplateData := commandTemplateInput{\n\t\tArgs: strings.Join(args, \" \"),\n\t\tData: data,\n\t}\n\n\ttmpl := template.Must(template.New(\"\").Parse(commandTemplate))\n\terr := tmpl.Execute(writer, templateData)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/generate/main_stub.go",
    "content": "//go:build !linux\n\npackage main\n\nimport (\n\t\"log\"\n)\n\nfunc main() {\n\tlog.Fatal(\"nerdctl-stub generate needs to be done on Linux\")\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/nerdctl-stub\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/sys v0.42.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "src/go/nerdctl-stub/go.sum",
    "content": "github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "src/go/nerdctl-stub/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"os\"\n)\n\ntype spawnOptions struct {\n\t// distro is the name of the WSL distribution for rancher-desktop.\n\tdistro string\n\t// nerdctl is the full path to a Linux-native nerdctl executable.\n\tnerdctl string\n\t// containerdSocket contains the path to the containerd socket.\n\tcontainerdSocket string\n\t// args are the parsed arguments for the WSL executable.\n\targs *parsedArgs\n}\n\nfunc main() {\n\terr := func() (err error) {\n\t\topts := spawnOptions{\n\t\t\tdistro:  os.Getenv(\"RD_WSL_DISTRO\"),\n\t\t\tnerdctl: os.Getenv(\"RD_NERDCTL\"),\n\t\t}\n\t\tif opts.distro == \"\" {\n\t\t\topts.distro = \"rancher-desktop\"\n\t\t}\n\t\tif opts.nerdctl == \"\" {\n\t\t\topts.nerdctl = \"/usr/local/bin/nerdctl\"\n\t\t}\n\t\topts.containerdSocket = \"/run/k3s/containerd/containerd.sock\"\n\n\t\targs, err := parseArgs()\n\t\tif err == nil {\n\t\t\topts.args = args\n\t\t} else {\n\t\t\t// If we fail to parse, display an error but still run nerdctl\n\t\t\tlog.Printf(\"Error parsing arguments: %s\", err)\n\t\t\topts.args = &parsedArgs{args: os.Args[1:]}\n\t\t}\n\n\t\tdefer func() {\n\t\t\terr = cleanupParseArgs()\n\t\t\t// The top-level function handles the error\n\t\t}()\n\n\t\terr = spawn(context.Background(), opts)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/main_linux.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/csv\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nconst (\n\t// mountPointField is the zero-indexed field number inf /proc/self/mountinfo\n\t// that contains the mount point.\n\tmountPointField = 4\n)\n\nfunc spawn(ctx context.Context, opts spawnOptions) error {\n\targs := []string{\"--distribution\", opts.distro, \"--exec\", opts.nerdctl, \"--address\", opts.containerdSocket}\n\targs = append(args, opts.args.args...)\n\tcmd := exec.CommandContext(ctx, \"wsl.exe\", args...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr := cmd.Run()\n\tfor _, cleanup := range opts.args.cleanup {\n\t\tif cleanupErr := cleanup(); cleanupErr != nil {\n\t\t\tlog.Printf(\"Error cleaning up: %s\", cleanupErr)\n\t\t}\n\t}\n\tif err != nil {\n\t\texitErr, ok := err.(*exec.ExitError)\n\t\tif ok {\n\t\t\tos.Exit(exitErr.ExitCode())\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nvar workdir string\n\n// Get the WSL mount point; typically, this is /mnt/wsl.\nfunc getWSLMountPoint() (string, error) {\n\tbuf, err := os.ReadFile(\"/proc/self/mountinfo\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error reading mounts: %w\", err)\n\t}\n\tfor _, line := range strings.Split(string(buf), \"\\n\") {\n\t\tif !strings.Contains(line, \" - tmpfs \") {\n\t\t\t// Skip the line if the filesystem type isn't \"tmpfs\"\n\t\t\tcontinue\n\t\t}\n\t\tfields := strings.Split(line, \" \")\n\t\tif len(fields) > mountPointField {\n\t\t\treturn fields[mountPointField], nil\n\t\t}\n\t}\n\treturn \"\", fmt.Errorf(\"could not find WSL mount root\")\n}\n\n// function prepareParseArgs should be called before argument parsing to set up\n// the system for arg parsing.\nfunc prepareParseArgs() error {\n\tif os.Geteuid() != 0 {\n\t\treturn fmt.Errorf(\"got unexpected euid %v\", os.Geteuid())\n\t}\n\tmountPoint, err := getWSLMountPoint()\n\tif err != nil {\n\t\treturn err\n\t}\n\trundir := path.Join(mountPoint, \"rancher-desktop/run/\")\n\terr = os.MkdirAll(rundir, 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\td, err := os.MkdirTemp(rundir, \"nerdctl-tmp.*\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tworkdir = d\n\treturn nil\n}\n\n// function cleanupParseArgs should be called after the command finishes\n// (regardless of whether it succeeded) to clean up any resources.\nfunc cleanupParseArgs() error {\n\tif workdir == \"\" {\n\t\treturn nil\n\t}\n\tentries, err := os.ReadDir(workdir)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tfor _, entry := range entries {\n\t\tentryPath := filepath.Join(workdir, entry.Name())\n\t\terr = unix.Unmount(entryPath, 0)\n\t\tif err != nil && !errors.Is(err, unix.EINVAL) {\n\t\t\tlog.Printf(\"Error unmounting %s: %s\", entryPath, err)\n\t\t}\n\t\terr = os.Remove(entryPath)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error removing mount directory %s: %s\", entryPath, err)\n\t\t}\n\t}\n\terr = os.Remove(workdir)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// doBindMount does the meat of the bind mounting.  Given a path, it makes a\n// mount inside workdir and returns the mounted path.\nfunc doBindMount(sourcePath string) (string, error) {\n\tinfo, err := os.Stat(sourcePath)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not stat %s: %w\", sourcePath, err)\n\t}\n\tvar result string\n\tif info.IsDir() {\n\t\tresult, err = os.MkdirTemp(workdir, \"input.*\")\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t} else {\n\t\tresultFile, err := os.CreateTemp(workdir, \"input.*\")\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tresultFile.Close()\n\t\tresult = resultFile.Name()\n\t}\n\terr = unix.Mount(sourcePath, result, \"none\", unix.MS_BIND|unix.MS_REC, \"\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn result, nil\n}\n\n// volumeArgHandler handles the argument for `nerdctl run --volume=...`\nfunc volumeArgHandler(arg string) (string, []cleanupFunc, error) {\n\t// args is of format [host:]container[:ro|:rw]\n\treadWrite := \"\"\n\tif strings.HasSuffix(arg, \":rw\") || strings.HasSuffix(arg, \":ro\") {\n\t\treadWrite = arg[len(arg)-3:]\n\t\targ = arg[:len(arg)-3]\n\t}\n\tcolonIndex := strings.Index(arg, \":\")\n\thostPath := \"\"\n\tcontainerPath := \"\"\n\tif colonIndex < 0 {\n\t\t// No colon, host and container path is the same.\n\t\thostPath = arg\n\t\tcontainerPath = arg\n\t} else {\n\t\thostPath = arg[:colonIndex]\n\t\tcontainerPath = arg[colonIndex+1:]\n\t}\n\n\tmountDir, err := doBindMount(hostPath)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn mountDir + \":\" + containerPath + readWrite, nil, nil\n}\n\n// mountArgHandler handles the argument for `nerdctl run --mount=...`\nfunc mountArgHandler(arg string) (string, []cleanupFunc, error) {\n\treturn mountArgProcessor(arg, doBindMount)\n}\n\n// filePathArgHandler handles arguments that take a file path for input\nfunc filePathArgHandler(arg string) (string, []cleanupFunc, error) {\n\tresult, err := doBindMount(arg)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn result, nil, nil\n}\n\n// outputPathArgHandler handles arguments that take a file path to indicate\n// where some file should be output.\nfunc outputPathArgHandler(arg string) (string, []cleanupFunc, error) {\n\tfile, err := os.CreateTemp(workdir, \"output.*\")\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\terr = file.Close()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\t// Some arguments error out if the file exists already.\n\terr = os.Remove(file.Name())\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tcallback := func() error {\n\t\tdefer os.Remove(file.Name())\n\t\tinput, err := os.Open(file.Name())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer input.Close()\n\t\toutput, err := os.Create(arg)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer output.Close()\n\t\t_, err = io.Copy(output, input)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// Since the executable is setuid, we need to make sure the normal\n\t\t// user owns the output file.\n\t\terr = os.Chown(arg, os.Getuid(), os.Getgid())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t}\n\treturn file.Name(), []cleanupFunc{callback}, nil\n}\n\n// builderCacheArgHandler handles arguments for\n// `nerdctl builder build --cache-from=` and `nerdctl builder build --cache-to=`\nfunc builderCacheArgHandler(arg string) (string, []cleanupFunc, error) {\n\treturn builderCacheProcessor(arg, filePathArgHandler, outputPathArgHandler)\n}\n\n// buildContextArgHandler handles arguments for\n// `nerdctl builder build --build-context=`.\nfunc buildContextArgHandler(arg string) (string, []cleanupFunc, error) {\n\t// The arg must be parsed as CSV (!?), and then split on `=` for key-value\n\t// pairs; for each value, it is either a URN with a prefix of one of\n\t// `urnPrefixes`, or it's a filesystem path.\n\n\tvar cleanups []cleanupFunc\n\turnPrefixes := []string{\"https://\", \"http://\", \"docker-image://\", \"target:\", \"oci-layout://\"}\n\tparts, err := csv.NewReader(strings.NewReader(arg)).Read()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tvar resultParts []string\n\tfor _, part := range parts {\n\t\tkv := strings.SplitN(part, \"=\", 2)\n\t\tif len(kv) != 2 {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to parse context value %q (expected key=value)\", part)\n\t\t}\n\t\tk, v := kv[0], kv[1]\n\t\tmatchesPrefix := func(prefix string) bool {\n\t\t\treturn strings.HasPrefix(v, prefix)\n\t\t}\n\t\tif !slices.ContainsFunc(urnPrefixes, matchesPrefix) {\n\t\t\tmount, newCleanups, err := filePathArgHandler(v)\n\t\t\tif err != nil {\n\t\t\t\t_ = runCleanups(cleanups)\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t\tv = mount\n\t\t\tcleanups = append(cleanups, newCleanups...)\n\t\t}\n\t\tresultParts = append(resultParts, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\tvar result bytes.Buffer\n\twriter := csv.NewWriter(&result)\n\tif err := writer.Write(resultParts); err != nil {\n\t\t_ = runCleanups(cleanups)\n\t\treturn \"\", nil, err\n\t}\n\twriter.Flush()\n\tif err := writer.Error(); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn strings.TrimSpace(result.String()), cleanups, nil\n}\n\n// argHandlers is the table of argument handlers.\nvar argHandlers = argHandlersType{\n\tvolumeArgHandler:       volumeArgHandler,\n\tfilePathArgHandler:     filePathArgHandler,\n\toutputPathArgHandler:   outputPathArgHandler,\n\tmountArgHandler:        mountArgHandler,\n\tbuilderCacheArgHandler: builderCacheArgHandler,\n\tbuildContextArgHandler: buildContextArgHandler,\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/main_shared.go",
    "content": "// This file contains shared routines for managing arguments.\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/hashicorp/go-multierror\"\n)\n\nfunc runCleanups(cleanups []cleanupFunc) error {\n\tvar errors *multierror.Error\n\n\tfor _, cleanup := range cleanups {\n\t\tif err := cleanup(); err != nil {\n\t\t\terrors = multierror.Append(errors, err)\n\t\t}\n\t}\n\n\treturn errors.ErrorOrNil()\n}\n\n// mountArgProcessor implements the details for handling the argument for\n// `nerdctl run --mount=...`\nfunc mountArgProcessor(arg string, mounter func(string) (string, error)) (string, []cleanupFunc, error) {\n\tvar chunks [][]string\n\tisBind := false\n\tfor _, chunk := range strings.Split(arg, \",\") {\n\t\tparts := strings.SplitN(chunk, \"=\", 2)\n\t\tif len(parts) != 2 {\n\t\t\t// Got something with no value, e.g. --mount=...,readonly,...\n\t\t\tchunks = append(chunks, []string{chunk})\n\t\t\tcontinue\n\t\t}\n\t\tif parts[0] == \"type\" && parts[1] == \"bind\" {\n\t\t\tisBind = true\n\t\t}\n\t\tchunks = append(chunks, parts)\n\t}\n\tif !isBind {\n\t\t// Not a bind mount; don't attempt to fix anything\n\t\treturn arg, nil, nil\n\t}\n\tfor _, chunk := range chunks {\n\t\tif len(chunk) != 2 {\n\t\t\tcontinue\n\t\t}\n\t\tif chunk[0] != \"source\" && chunk[0] != \"src\" {\n\t\t\tcontinue\n\t\t}\n\t\tmountDir, err := mounter(chunk[1])\n\t\tif err != nil {\n\t\t\treturn \"\", nil, err\n\t\t}\n\t\tchunk[1] = mountDir\n\t}\n\tresult := \"\"\n\tfor _, chunk := range chunks {\n\t\tresult = fmt.Sprintf(\"%s,%s\", result, strings.Join(chunk, \"=\"))\n\t}\n\treturn result[1:], nil, nil // Skip the initial \",\" we added\n}\n\n// builderCacheProcessor implements the details for handling the argument for\n// `nerdctl builder build --cache-from=...` and\n// `nerdctl builder build --cache-to=...`\nfunc builderCacheProcessor(arg string, inputMounter, outputMounter func(string) (string, []cleanupFunc, error)) (string, []cleanupFunc, error) {\n\tvar cleanups []cleanupFunc\n\n\t// The arg is comma-separated args, with `type=` and `src=`, `dest=`\n\t// If no type is given, nerdctl assume `type=registry`, which we can ignore.\n\t// ref: https://github.com/containerd/nerdctl/blob/v1.2.0/pkg/cmd/builder/build.go#L333-L345\n\t// Otherwise, for `src=` it's an input, and `dest=` is an output.\n\tvar parts []string\n\tfor _, part := range strings.Split(arg, \",\") {\n\t\tif strings.HasPrefix(part, \"src=\") {\n\t\t\tsrcPath := part[len(\"src=\"):]\n\t\t\tfixedPath, newCleanups, err := inputMounter(srcPath)\n\t\t\tif err != nil {\n\t\t\t\terrors := multierror.Append(err, runCleanups(newCleanups))\n\t\t\t\tif errors.Len() > 1 {\n\t\t\t\t\treturn \"\", nil, errors\n\t\t\t\t}\n\t\t\t\treturn \"\", nil, errors.Unwrap()\n\t\t\t}\n\t\t\tparts = append(parts, \"src=\"+fixedPath)\n\t\t\tcleanups = append(cleanups, newCleanups...)\n\t\t} else if strings.HasPrefix(part, \"dest=\") {\n\t\t\tdestPath := part[len(\"dest=\"):]\n\t\t\tfixedPath, newCleanups, err := outputMounter(destPath)\n\t\t\tif err != nil {\n\t\t\t\terrors := multierror.Append(err, runCleanups(newCleanups))\n\t\t\t\tif errors.Len() > 1 {\n\t\t\t\t\treturn \"\", nil, errors\n\t\t\t\t}\n\t\t\t\treturn \"\", nil, errors.Unwrap()\n\t\t\t}\n\t\t\tparts = append(parts, \"dest=\"+fixedPath)\n\t\t\tcleanups = append(cleanups, newCleanups...)\n\t\t} else {\n\t\t\tparts = append(parts, part)\n\t\t}\n\t}\n\n\tresultArg := strings.Join(parts, \",\")\n\treturn resultArg, cleanups, nil\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/main_shared_test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBuilderCacheProcessor(t *testing.T) {\n\tt.Run(\"ignores unknown arguments\", func(t *testing.T) {\n\t\tinput := \"hello/world,bar=baz\"\n\t\tresult, cleanups, err := builderCacheProcessor(input,\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tt.Error(\"should not have called inputMounter with\", s)\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"test failed\")\n\t\t\t},\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tt.Error(\"should not have called outputMounter with\", s)\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"test failed\")\n\t\t\t})\n\t\tassert.Equal(t, input, result, \"input should not have changed\")\n\t\tassert.Empty(t, cleanups, \"no cleanup functions should have been added\")\n\t\tassert.NoError(t, err, \"error unexpected\")\n\t})\n\tt.Run(\"processes input mounts\", func(t *testing.T) {\n\t\tinput := \"extra=stuff,src=moar stuff,trailer=other stuff\"\n\t\tcleanupDone := false\n\t\tresult, cleanups, err := builderCacheProcessor(input,\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tassert.Equal(t, \"moar stuff\", s)\n\t\t\t\treturn \"modified stuff\", []cleanupFunc{func() error {\n\t\t\t\t\tcleanupDone = true\n\t\t\t\t\treturn nil\n\t\t\t\t}}, nil\n\t\t\t},\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tt.Error(\"should not have called outputMounter with\", s)\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"test failed\")\n\t\t\t})\n\t\tassert.Equal(t, \"extra=stuff,src=modified stuff,trailer=other stuff\", result)\n\t\tassert.NotEmpty(t, cleanups, \"expected cleanup functions\")\n\t\tassert.NoError(t, err, \"error running builderCacheProcessor\")\n\t\tassert.False(t, cleanupDone, \"cleanup function already ran\")\n\t\tassert.NoError(t, runCleanups(cleanups))\n\t\tassert.True(t, cleanupDone, \"cleanup function did not run\")\n\t})\n\tt.Run(\"processes output mounts\", func(t *testing.T) {\n\t\tinput := \"extra=stuff,dest=moar stuff,trailer=other stuff\"\n\t\tcleanupDone := false\n\t\tresult, cleanups, err := builderCacheProcessor(input,\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tt.Error(\"should not have called inputMounter with\", s)\n\t\t\t\treturn \"\", nil, fmt.Errorf(\"test failed\")\n\t\t\t},\n\t\t\tfunc(s string) (string, []cleanupFunc, error) {\n\t\t\t\tassert.Equal(t, \"moar stuff\", s)\n\t\t\t\treturn \"modified stuff\", []cleanupFunc{func() error {\n\t\t\t\t\tcleanupDone = true\n\t\t\t\t\treturn nil\n\t\t\t\t}}, nil\n\t\t\t})\n\t\tassert.Equal(t, \"extra=stuff,dest=modified stuff,trailer=other stuff\", result)\n\t\tassert.NotEmpty(t, cleanups, \"expected cleanup functions\")\n\t\tassert.NoError(t, err, \"error running builderCacheProcessor\")\n\t\tassert.False(t, cleanupDone, \"cleanup function already ran\")\n\t\tassert.NoError(t, runCleanups(cleanups))\n\t\tassert.True(t, cleanupDone, \"cleanup function did not run\")\n\t})\n}\n\nfunc TestMountArgProcessor(t *testing.T) {\n\targ, cleanup, err := mountArgProcessor(\"--unknown-arg\", nil)\n\tassert.Equal(t, \"--unknown-arg\", arg)\n\tassert.Empty(t, cleanup)\n\tassert.NoError(t, err)\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/main_unsupported.go",
    "content": "//go:build !(linux || windows)\n\npackage main\n\nimport \"context\"\n\n// This file is a stub for unsupported platforms to make IDEs happy.\n\n// unhandledArgHandler is a handler for unsupported arguments.\nfunc unhandledArgHandler(arg string) (string, []cleanupFunc, error) {\n\tpanic(\"Platform is unsupported\")\n}\n\n// argHandlers is the table of argument handlers.\nvar argHandlers = argHandlersType{\n\tvolumeArgHandler:       unhandledArgHandler,\n\tfilePathArgHandler:     unhandledArgHandler,\n\toutputPathArgHandler:   unhandledArgHandler,\n\tmountArgHandler:        unhandledArgHandler,\n\tbuilderCacheArgHandler: unhandledArgHandler,\n}\n\nfunc spawn(ctx context.Context, opts spawnOptions) error {\n\tpanic(\"Platform is unsupported\")\n}\n\n// function prepareParseArgs should be called before argument parsing to set up\n// the system for arg parsing.\nfunc prepareParseArgs() error {\n\tpanic(\"Platform is unsupported\")\n}\n\n// function cleanupParseArgs should be called after the command finishes\n// (regardless of whether it succeeded) to clean up any resources.\nfunc cleanupParseArgs() error {\n\tpanic(\"Platform is unsupported\")\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/main_windows.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strings\"\n)\n\nfunc spawn(ctx context.Context, opts spawnOptions) error {\n\targs := []string{\"--distribution\", opts.distro, \"--exec\", \"/usr/local/bin/wsl-exec\", opts.nerdctl, \"--address\", opts.containerdSocket}\n\targs = append(args, opts.args.args...)\n\tcmd := exec.CommandContext(ctx, \"wsl.exe\", args...)\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr := cmd.Run()\n\tfor _, handler := range opts.args.cleanup {\n\t\tcleanupErr := handler()\n\t\tif cleanupErr != nil {\n\t\t\tlog.Printf(\"Error cleaning up: %s\", cleanupErr)\n\t\t}\n\t}\n\tif err != nil {\n\t\texitErr, ok := err.(*exec.ExitError)\n\t\tif ok {\n\t\t\tos.Exit(exitErr.ExitCode())\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// function prepareParseArgs should be called before argument parsing to set up\n// the system for arg parsing.\nfunc prepareParseArgs() error {\n\t// Nothing is required on Windows.\n\treturn nil\n}\n\n// function cleanupParseArgs should be called after the command finishes\n// (regardless of whether it succeeded) to clean up any resources.\nfunc cleanupParseArgs() error {\n\t// Nothing is required on Windows.\n\treturn nil\n}\n\n// pathToWSL converts a Windows path to one that can be used in WSL.\nfunc pathToWSL(arg string) (string, error) {\n\t// absPath is something like C:\\Foo\\Bar\\Baz\n\tabsPath, err := filepath.Abs(filepath.FromSlash(arg))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tslashPath := filepath.ToSlash(absPath)\n\tvol := filepath.VolumeName(absPath)\n\tif vol != \"\" && vol[len(vol)-1] == ':' {\n\t\tvolName := strings.ToLower(vol[:len(vol)-1])\n\t\treturn \"/mnt/\" + volName + slashPath[len(vol):], nil\n\t}\n\t// volume name is not what we expected\n\treturn slashPath, nil\n}\n\n// volumeArgHandler handles the argument for `nerdctl run --volume=...`\nfunc volumeArgHandler(arg string) (string, []cleanupFunc, error) {\n\t// Valid arguments are:\n\t// <host path>:<container path>\n\t// <host path>:<container path>:rw\n\t// <host path>:<container path>:ro\n\t// Because we only have Linux containers, and this is for Windows, we don't\n\t// need to worry about just `<path>` (where the host and container have the\n\t// same path).\n\tcleanArg := arg\n\treadWrite := \"\"\n\tif strings.HasSuffix(arg, \":ro\") || strings.HasSuffix(arg, \":rw\") {\n\t\treadWrite = arg[len(arg)-3:]\n\t\tcleanArg = arg[:len(arg)-3]\n\t}\n\t// For now, assume the container path doesn't contain colons.\n\tcolonIndex := strings.LastIndex(cleanArg, \":\")\n\tif colonIndex < 0 {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid volume mount: %s does not contain : separator\", arg)\n\t}\n\thostPath := cleanArg[:colonIndex]\n\tcontainerPath := cleanArg[colonIndex+1:]\n\twslHostPath, err := pathToWSL(hostPath)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"could not get volume host path for %s: %w\", arg, err)\n\t}\n\treturn wslHostPath + \":\" + containerPath + readWrite, nil, nil\n}\n\n// mountArgHandler handles the argument for `nerdctl run --mount=...`\nfunc mountArgHandler(arg string) (string, []cleanupFunc, error) {\n\treturn mountArgProcessor(arg, pathToWSL)\n}\n\n// filePathArgHandler handles arguments that take a file path for input\nfunc filePathArgHandler(arg string) (string, []cleanupFunc, error) {\n\tresult, err := pathToWSL(arg)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn result, nil, nil\n}\n\n// outputPathArgHandler handles arguments that take a file path to indicate\n// where some file should be output.\nfunc outputPathArgHandler(arg string) (string, []cleanupFunc, error) {\n\tresult, err := pathToWSL(arg)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn result, nil, nil\n}\n\n// builderCacheArgHandler handles arguments for\n// `nerdctl builder build --cache-from=` and `nerdctl builder build --cache-to=`\nfunc builderCacheArgHandler(arg string) (string, []cleanupFunc, error) {\n\treturn builderCacheProcessor(arg, filePathArgHandler, outputPathArgHandler)\n}\n\n// buildContextArgHandler handles arguments for\n// `nerdctl builder build --build-context=`.\nfunc buildContextArgHandler(arg string) (string, []cleanupFunc, error) {\n\t// The arg must be parsed as CSV (!?), and then split on `=` for key-value\n\t// pairs; for each value, it is either a URN with a prefix of one of\n\t// `urnPrefixes`, or it's a filesystem path.\n\n\turnPrefixes := []string{\"https://\", \"http://\", \"docker-image://\", \"target:\", \"oci-layout://\"}\n\tparts, err := csv.NewReader(strings.NewReader(arg)).Read()\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\tvar resultParts []string\n\tfor _, part := range parts {\n\t\tkv := strings.SplitN(part, \"=\", 2)\n\t\tif len(kv) != 2 {\n\t\t\treturn \"\", nil, fmt.Errorf(\"failed to parse context value %q (expected key=value)\", part)\n\t\t}\n\t\tk, v := kv[0], kv[1]\n\t\tmatchesPrefix := func(prefix string) bool {\n\t\t\treturn strings.HasPrefix(v, prefix)\n\t\t}\n\t\tif !slices.ContainsFunc(urnPrefixes, matchesPrefix) {\n\t\t\tv, err = pathToWSL(v)\n\t\t\tif err != nil {\n\t\t\t\treturn \"\", nil, err\n\t\t\t}\n\t\t}\n\t\tresultParts = append(resultParts, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\tvar result bytes.Buffer\n\twriter := csv.NewWriter(&result)\n\tif err := writer.Write(resultParts); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\twriter.Flush()\n\tif err := writer.Error(); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn strings.TrimSpace(result.String()), nil, nil\n}\n\n// argHandlers is the table of argument handlers.\nvar argHandlers = argHandlersType{\n\tvolumeArgHandler:       volumeArgHandler,\n\tfilePathArgHandler:     filePathArgHandler,\n\toutputPathArgHandler:   outputPathArgHandler,\n\tmountArgHandler:        mountArgHandler,\n\tbuilderCacheArgHandler: builderCacheArgHandler,\n\tbuildContextArgHandler: buildContextArgHandler,\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/nerdctl_commands_generated.go",
    "content": "// Code generated by github.com/rancher-sandbox/rancher-desktop/src/go/nerdctl-stub/generate - DO NOT EDIT.\n\n// package main implements a stub for nerdctl\npackage main\n\n// commands supported by nerdctl; the key here is a space-separated subcommand\n// path to reach the given subcommand (where the root command is empty).\nvar commands = map[string]commandDefinition{\n\n\t\"\": {\n\t\tcommandPath: \"\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"apparmor\":    {},\n\t\t\t\"attach\":      {},\n\t\t\t\"build\":       {},\n\t\t\t\"builder\":     {},\n\t\t\t\"checkpoint\":  {},\n\t\t\t\"commit\":      {},\n\t\t\t\"completion\":  {},\n\t\t\t\"compose\":     {},\n\t\t\t\"container\":   {},\n\t\t\t\"cp\":          {},\n\t\t\t\"create\":      {},\n\t\t\t\"diff\":        {},\n\t\t\t\"events\":      {},\n\t\t\t\"exec\":        {},\n\t\t\t\"export\":      {},\n\t\t\t\"healthcheck\": {},\n\t\t\t\"help\":        {},\n\t\t\t\"history\":     {},\n\t\t\t\"image\":       {},\n\t\t\t\"images\":      {},\n\t\t\t\"import\":      {},\n\t\t\t\"info\":        {},\n\t\t\t\"inspect\":     {},\n\t\t\t\"ipfs\":        {},\n\t\t\t\"kill\":        {},\n\t\t\t\"load\":        {},\n\t\t\t\"login\":       {},\n\t\t\t\"logout\":      {},\n\t\t\t\"logs\":        {},\n\t\t\t\"manifest\":    {},\n\t\t\t\"namespace\":   {},\n\t\t\t\"network\":     {},\n\t\t\t\"pause\":       {},\n\t\t\t\"port\":        {},\n\t\t\t\"ps\":          {},\n\t\t\t\"pull\":        {},\n\t\t\t\"push\":        {},\n\t\t\t\"rename\":      {},\n\t\t\t\"restart\":     {},\n\t\t\t\"rm\":          {},\n\t\t\t\"rmi\":         {},\n\t\t\t\"run\":         {},\n\t\t\t\"save\":        {},\n\t\t\t\"start\":       {},\n\t\t\t\"stats\":       {},\n\t\t\t\"stop\":        {},\n\t\t\t\"system\":      {},\n\t\t\t\"tag\":         {},\n\t\t\t\"top\":         {},\n\t\t\t\"unpause\":     {},\n\t\t\t\"update\":      {},\n\t\t\t\"version\":     {},\n\t\t\t\"volume\":      {},\n\t\t\t\"wait\":        {},\n\t\t},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--H\":                 ignoredArgHandler,\n\t\t\t\"--a\":                 ignoredArgHandler,\n\t\t\t\"--address\":           ignoredArgHandler,\n\t\t\t\"--bridge-ip\":         ignoredArgHandler,\n\t\t\t\"--cdi-spec-dirs\":     ignoredArgHandler,\n\t\t\t\"--cgroup-manager\":    ignoredArgHandler,\n\t\t\t\"--cni-netconfpath\":   ignoredArgHandler,\n\t\t\t\"--cni-path\":          ignoredArgHandler,\n\t\t\t\"--data-root\":         ignoredArgHandler,\n\t\t\t\"--debug\":             nil,\n\t\t\t\"--debug-full\":        nil,\n\t\t\t\"--experimental\":      nil,\n\t\t\t\"--help\":              nil,\n\t\t\t\"--host\":              ignoredArgHandler,\n\t\t\t\"--host-gateway-ip\":   ignoredArgHandler,\n\t\t\t\"--hosts-dir\":         ignoredArgHandler,\n\t\t\t\"--insecure-registry\": nil,\n\t\t\t\"--kube-hide-dupe\":    nil,\n\t\t\t\"--n\":                 ignoredArgHandler,\n\t\t\t\"--namespace\":         ignoredArgHandler,\n\t\t\t\"--snapshotter\":       ignoredArgHandler,\n\t\t\t\"--storage-driver\":    ignoredArgHandler,\n\t\t\t\"--userns-remap\":      ignoredArgHandler,\n\t\t\t\"--version\":           nil,\n\t\t\t\"-H\":                  ignoredArgHandler,\n\t\t\t\"-a\":                  ignoredArgHandler,\n\t\t\t\"-h\":                  nil,\n\t\t\t\"-n\":                  ignoredArgHandler,\n\t\t\t\"-v\":                  nil,\n\t\t},\n\t},\n\n\t\"apparmor\": {\n\t\tcommandPath: \"apparmor\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"inspect\": {},\n\t\t\t\"load\":    {},\n\t\t\t\"ls\":      {},\n\t\t\t\"unload\":  {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"apparmor inspect\": {\n\t\tcommandPath: \"apparmor inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"apparmor load\": {\n\t\tcommandPath: \"apparmor load\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"apparmor ls\": {\n\t\tcommandPath: \"apparmor ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--quiet\":  nil,\n\t\t\t\"-q\":       nil,\n\t\t},\n\t},\n\n\t\"apparmor unload\": {\n\t\tcommandPath: \"apparmor unload\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"attach\": {\n\t\tcommandPath: \"attach\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--detach-keys\": ignoredArgHandler,\n\t\t\t\"--no-stdin\":    nil,\n\t\t},\n\t},\n\n\t\"build\": {\n\t\tcommandPath: \"build\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":      ignoredArgHandler,\n\t\t\t\"--allow\":         ignoredArgHandler,\n\t\t\t\"--attest\":        ignoredArgHandler,\n\t\t\t\"--build-arg\":     ignoredArgHandler,\n\t\t\t\"--build-context\": ignoredArgHandler,\n\t\t\t\"--buildkit-host\": ignoredArgHandler,\n\t\t\t\"--cache-from\":    ignoredArgHandler,\n\t\t\t\"--cache-to\":      ignoredArgHandler,\n\t\t\t\"--file\":          ignoredArgHandler,\n\t\t\t\"--iidfile\":       ignoredArgHandler,\n\t\t\t\"--label\":         ignoredArgHandler,\n\t\t\t\"--network\":       ignoredArgHandler,\n\t\t\t\"--no-cache\":      nil,\n\t\t\t\"--output\":        ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--progress\":      ignoredArgHandler,\n\t\t\t\"--provenance\":    ignoredArgHandler,\n\t\t\t\"--pull\":          nil,\n\t\t\t\"--quiet\":         nil,\n\t\t\t\"--rm\":            nil,\n\t\t\t\"--sbom\":          ignoredArgHandler,\n\t\t\t\"--secret\":        ignoredArgHandler,\n\t\t\t\"--ssh\":           ignoredArgHandler,\n\t\t\t\"--tag\":           ignoredArgHandler,\n\t\t\t\"--target\":        ignoredArgHandler,\n\t\t\t\"-f\":              ignoredArgHandler,\n\t\t\t\"-o\":              ignoredArgHandler,\n\t\t\t\"-q\":              nil,\n\t\t\t\"-t\":              ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"builder\": {\n\t\tcommandPath: \"builder\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"build\": {},\n\t\t\t\"debug\": {},\n\t\t\t\"prune\": {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"builder build\": {\n\t\tcommandPath: \"builder build\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":      ignoredArgHandler,\n\t\t\t\"--allow\":         ignoredArgHandler,\n\t\t\t\"--attest\":        ignoredArgHandler,\n\t\t\t\"--build-arg\":     ignoredArgHandler,\n\t\t\t\"--build-context\": ignoredArgHandler,\n\t\t\t\"--buildkit-host\": ignoredArgHandler,\n\t\t\t\"--cache-from\":    ignoredArgHandler,\n\t\t\t\"--cache-to\":      ignoredArgHandler,\n\t\t\t\"--file\":          ignoredArgHandler,\n\t\t\t\"--iidfile\":       ignoredArgHandler,\n\t\t\t\"--label\":         ignoredArgHandler,\n\t\t\t\"--network\":       ignoredArgHandler,\n\t\t\t\"--no-cache\":      nil,\n\t\t\t\"--output\":        ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--progress\":      ignoredArgHandler,\n\t\t\t\"--provenance\":    ignoredArgHandler,\n\t\t\t\"--pull\":          nil,\n\t\t\t\"--quiet\":         nil,\n\t\t\t\"--rm\":            nil,\n\t\t\t\"--sbom\":          ignoredArgHandler,\n\t\t\t\"--secret\":        ignoredArgHandler,\n\t\t\t\"--ssh\":           ignoredArgHandler,\n\t\t\t\"--tag\":           ignoredArgHandler,\n\t\t\t\"--target\":        ignoredArgHandler,\n\t\t\t\"-f\":              ignoredArgHandler,\n\t\t\t\"-o\":              ignoredArgHandler,\n\t\t\t\"-q\":              nil,\n\t\t\t\"-t\":              ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"builder debug\": {\n\t\tcommandPath: \"builder debug\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--build-arg\":              ignoredArgHandler,\n\t\t\t\"--buildg-startup-timeout\": ignoredArgHandler,\n\t\t\t\"--file\":                   ignoredArgHandler,\n\t\t\t\"--image\":                  ignoredArgHandler,\n\t\t\t\"--secret\":                 ignoredArgHandler,\n\t\t\t\"--ssh\":                    ignoredArgHandler,\n\t\t\t\"--target\":                 ignoredArgHandler,\n\t\t\t\"-f\":                       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"builder prune\": {\n\t\tcommandPath: \"builder prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":           nil,\n\t\t\t\"--buildkit-host\": ignoredArgHandler,\n\t\t\t\"--force\":         nil,\n\t\t\t\"-a\":              nil,\n\t\t\t\"-f\":              nil,\n\t\t},\n\t},\n\n\t\"checkpoint\": {\n\t\tcommandPath: \"checkpoint\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"create\": {},\n\t\t\t\"ls\":     {},\n\t\t\t\"rm\":     {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"checkpoint create\": {\n\t\tcommandPath: \"checkpoint create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--checkpoint-dir\": ignoredArgHandler,\n\t\t\t\"--leave-running\":  nil,\n\t\t},\n\t},\n\n\t\"checkpoint ls\": {\n\t\tcommandPath: \"checkpoint ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--checkpoint-dir\": ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"checkpoint rm\": {\n\t\tcommandPath: \"checkpoint rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--checkpoint-dir\": ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"commit\": {\n\t\tcommandPath: \"commit\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--author\":                        ignoredArgHandler,\n\t\t\t\"--change\":                        ignoredArgHandler,\n\t\t\t\"--compression\":                   ignoredArgHandler,\n\t\t\t\"--estargz\":                       nil,\n\t\t\t\"--estargz-chunk-size\":            ignoredArgHandler,\n\t\t\t\"--estargz-compression-level\":     ignoredArgHandler,\n\t\t\t\"--estargz-min-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--format\":                        ignoredArgHandler,\n\t\t\t\"--message\":                       ignoredArgHandler,\n\t\t\t\"--pause\":                         nil,\n\t\t\t\"--zstdchunked\":                   nil,\n\t\t\t\"--zstdchunked-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--zstdchunked-compression-level\": ignoredArgHandler,\n\t\t\t\"-a\":                              ignoredArgHandler,\n\t\t\t\"-c\":                              ignoredArgHandler,\n\t\t\t\"-m\":                              ignoredArgHandler,\n\t\t\t\"-p\":                              nil,\n\t\t},\n\t},\n\n\t\"completion\": {\n\t\tcommandPath: \"completion\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"bash\":       {},\n\t\t\t\"fish\":       {},\n\t\t\t\"powershell\": {},\n\t\t\t\"zsh\":        {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"completion bash\": {\n\t\tcommandPath: \"completion bash\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--no-descriptions\": nil,\n\t\t},\n\t},\n\n\t\"completion fish\": {\n\t\tcommandPath: \"completion fish\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--no-descriptions\": nil,\n\t\t},\n\t},\n\n\t\"completion powershell\": {\n\t\tcommandPath: \"completion powershell\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--no-descriptions\": nil,\n\t\t},\n\t},\n\n\t\"completion zsh\": {\n\t\tcommandPath: \"completion zsh\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--no-descriptions\": nil,\n\t\t},\n\t},\n\n\t\"compose\": {\n\t\tcommandPath: \"compose\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"build\":   {},\n\t\t\t\"config\":  {},\n\t\t\t\"cp\":      {},\n\t\t\t\"create\":  {},\n\t\t\t\"down\":    {},\n\t\t\t\"exec\":    {},\n\t\t\t\"images\":  {},\n\t\t\t\"kill\":    {},\n\t\t\t\"logs\":    {},\n\t\t\t\"pause\":   {},\n\t\t\t\"port\":    {},\n\t\t\t\"ps\":      {},\n\t\t\t\"pull\":    {},\n\t\t\t\"push\":    {},\n\t\t\t\"restart\": {},\n\t\t\t\"rm\":      {},\n\t\t\t\"run\":     {},\n\t\t\t\"start\":   {},\n\t\t\t\"stop\":    {},\n\t\t\t\"top\":     {},\n\t\t\t\"unpause\": {},\n\t\t\t\"up\":      {},\n\t\t\t\"version\": {},\n\t\t},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--env-file\":          ignoredArgHandler,\n\t\t\t\"--f\":                 ignoredArgHandler,\n\t\t\t\"--file\":              ignoredArgHandler,\n\t\t\t\"--ipfs-address\":      ignoredArgHandler,\n\t\t\t\"--profile\":           ignoredArgHandler,\n\t\t\t\"--project-directory\": ignoredArgHandler,\n\t\t\t\"--project-name\":      ignoredArgHandler,\n\t\t\t\"-f\":                  ignoredArgHandler,\n\t\t\t\"-p\":                  ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose build\": {\n\t\tcommandPath: \"compose build\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--build-arg\": ignoredArgHandler,\n\t\t\t\"--no-cache\":  nil,\n\t\t\t\"--progress\":  ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose config\": {\n\t\tcommandPath: \"compose config\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--hash\":     ignoredArgHandler,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"--services\": nil,\n\t\t\t\"--volumes\":  nil,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"compose cp\": {\n\t\tcommandPath: \"compose cp\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--dry-run\":     nil,\n\t\t\t\"--follow-link\": nil,\n\t\t\t\"--index\":       ignoredArgHandler,\n\t\t\t\"-L\":            nil,\n\t\t},\n\t},\n\n\t\"compose create\": {\n\t\tcommandPath: \"compose create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--build\":          nil,\n\t\t\t\"--force-recreate\": nil,\n\t\t\t\"--no-build\":       nil,\n\t\t\t\"--no-recreate\":    nil,\n\t\t\t\"--pull\":           ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose down\": {\n\t\tcommandPath: \"compose down\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--remove-orphans\": nil,\n\t\t\t\"--volumes\":        ignoredArgHandler,\n\t\t\t\"-v\":               ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose exec\": {\n\t\tcommandPath: \"compose exec\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--detach\":     nil,\n\t\t\t\"--env\":        ignoredArgHandler,\n\t\t\t\"--index\":      ignoredArgHandler,\n\t\t\t\"--no-TTY\":     nil,\n\t\t\t\"--privileged\": nil,\n\t\t\t\"--user\":       ignoredArgHandler,\n\t\t\t\"--workdir\":    ignoredArgHandler,\n\t\t\t\"-T\":           nil,\n\t\t\t\"-d\":           nil,\n\t\t\t\"-e\":           ignoredArgHandler,\n\t\t\t\"-u\":           ignoredArgHandler,\n\t\t\t\"-w\":           ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"compose images\": {\n\t\tcommandPath: \"compose images\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--quiet\":  nil,\n\t\t\t\"-q\":       nil,\n\t\t},\n\t},\n\n\t\"compose kill\": {\n\t\tcommandPath: \"compose kill\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose logs\": {\n\t\tcommandPath: \"compose logs\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--follow\":        nil,\n\t\t\t\"--no-color\":      nil,\n\t\t\t\"--no-log-prefix\": nil,\n\t\t\t\"--tail\":          ignoredArgHandler,\n\t\t\t\"--timestamps\":    nil,\n\t\t\t\"-f\":              nil,\n\t\t\t\"-t\":              nil,\n\t\t},\n\t},\n\n\t\"compose pause\": {\n\t\tcommandPath: \"compose pause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"compose port\": {\n\t\tcommandPath: \"compose port\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--index\":    ignoredArgHandler,\n\t\t\t\"--protocol\": ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose ps\": {\n\t\tcommandPath: \"compose ps\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":      nil,\n\t\t\t\"--filter\":   ignoredArgHandler,\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"--services\": nil,\n\t\t\t\"--status\":   ignoredArgHandler,\n\t\t\t\"-a\":         nil,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"compose pull\": {\n\t\tcommandPath: \"compose pull\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--quiet\": nil,\n\t\t\t\"-q\":      nil,\n\t\t},\n\t},\n\n\t\"compose push\": {\n\t\tcommandPath: \"compose push\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"compose restart\": {\n\t\tcommandPath: \"compose restart\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--timeout\": ignoredArgHandler,\n\t\t\t\"-t\":        ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose rm\": {\n\t\tcommandPath: \"compose rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\":   nil,\n\t\t\t\"--stop\":    nil,\n\t\t\t\"--volumes\": nil,\n\t\t\t\"-f\":        nil,\n\t\t\t\"-s\":        nil,\n\t\t\t\"-v\":        nil,\n\t\t},\n\t},\n\n\t\"compose run\": {\n\t\tcommandPath: \"compose run\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--build\":          nil,\n\t\t\t\"--detach\":         nil,\n\t\t\t\"--entrypoint\":     ignoredArgHandler,\n\t\t\t\"--env\":            ignoredArgHandler,\n\t\t\t\"--interactive\":    nil,\n\t\t\t\"--label\":          ignoredArgHandler,\n\t\t\t\"--name\":           ignoredArgHandler,\n\t\t\t\"--no-build\":       nil,\n\t\t\t\"--no-color\":       nil,\n\t\t\t\"--no-deps\":        nil,\n\t\t\t\"--no-log-prefix\":  nil,\n\t\t\t\"--publish\":        ignoredArgHandler,\n\t\t\t\"--quiet-pull\":     nil,\n\t\t\t\"--remove-orphans\": nil,\n\t\t\t\"--rm\":             nil,\n\t\t\t\"--service-ports\":  nil,\n\t\t\t\"--user\":           ignoredArgHandler,\n\t\t\t\"--volume\":         ignoredArgHandler,\n\t\t\t\"--workdir\":        ignoredArgHandler,\n\t\t\t\"-d\":               nil,\n\t\t\t\"-e\":               ignoredArgHandler,\n\t\t\t\"-i\":               nil,\n\t\t\t\"-l\":               ignoredArgHandler,\n\t\t\t\"-u\":               ignoredArgHandler,\n\t\t\t\"-v\":               ignoredArgHandler,\n\t\t\t\"-w\":               ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"compose start\": {\n\t\tcommandPath: \"compose start\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"compose stop\": {\n\t\tcommandPath: \"compose stop\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--timeout\": ignoredArgHandler,\n\t\t\t\"-t\":        ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"compose top\": {\n\t\tcommandPath: \"compose top\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"compose unpause\": {\n\t\tcommandPath: \"compose unpause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"compose up\": {\n\t\tcommandPath: \"compose up\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--abort-on-container-exit\": nil,\n\t\t\t\"--build\":                   nil,\n\t\t\t\"--detach\":                  nil,\n\t\t\t\"--force-recreate\":          nil,\n\t\t\t\"--ipfs\":                    nil,\n\t\t\t\"--no-build\":                nil,\n\t\t\t\"--no-color\":                nil,\n\t\t\t\"--no-log-prefix\":           nil,\n\t\t\t\"--no-recreate\":             nil,\n\t\t\t\"--pull\":                    ignoredArgHandler,\n\t\t\t\"--quiet-pull\":              nil,\n\t\t\t\"--remove-orphans\":          nil,\n\t\t\t\"--scale\":                   ignoredArgHandler,\n\t\t\t\"-d\":                        nil,\n\t\t},\n\t},\n\n\t\"compose version\": {\n\t\tcommandPath: \"compose version\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--short\":  nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container\": {\n\t\tcommandPath: \"container\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"attach\":      {},\n\t\t\t\"commit\":      {},\n\t\t\t\"cp\":          {},\n\t\t\t\"create\":      {},\n\t\t\t\"diff\":        {},\n\t\t\t\"exec\":        {},\n\t\t\t\"export\":      {},\n\t\t\t\"healthcheck\": {},\n\t\t\t\"inspect\":     {},\n\t\t\t\"kill\":        {},\n\t\t\t\"logs\":        {},\n\t\t\t\"ls\":          {},\n\t\t\t\"pause\":       {},\n\t\t\t\"port\":        {},\n\t\t\t\"prune\":       {},\n\t\t\t\"rename\":      {},\n\t\t\t\"restart\":     {},\n\t\t\t\"rm\":          {},\n\t\t\t\"run\":         {},\n\t\t\t\"start\":       {},\n\t\t\t\"stats\":       {},\n\t\t\t\"stop\":        {},\n\t\t\t\"unpause\":     {},\n\t\t\t\"update\":      {},\n\t\t\t\"wait\":        {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"container attach\": {\n\t\tcommandPath: \"container attach\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--detach-keys\": ignoredArgHandler,\n\t\t\t\"--no-stdin\":    nil,\n\t\t},\n\t},\n\n\t\"container commit\": {\n\t\tcommandPath: \"container commit\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--author\":                        ignoredArgHandler,\n\t\t\t\"--change\":                        ignoredArgHandler,\n\t\t\t\"--compression\":                   ignoredArgHandler,\n\t\t\t\"--estargz\":                       nil,\n\t\t\t\"--estargz-chunk-size\":            ignoredArgHandler,\n\t\t\t\"--estargz-compression-level\":     ignoredArgHandler,\n\t\t\t\"--estargz-min-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--format\":                        ignoredArgHandler,\n\t\t\t\"--message\":                       ignoredArgHandler,\n\t\t\t\"--pause\":                         nil,\n\t\t\t\"--zstdchunked\":                   nil,\n\t\t\t\"--zstdchunked-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--zstdchunked-compression-level\": ignoredArgHandler,\n\t\t\t\"-a\":                              ignoredArgHandler,\n\t\t\t\"-c\":                              ignoredArgHandler,\n\t\t\t\"-m\":                              ignoredArgHandler,\n\t\t\t\"-p\":                              nil,\n\t\t},\n\t},\n\n\t\"container cp\": {\n\t\tcommandPath: \"container cp\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--follow-link\": nil,\n\t\t\t\"-L\":            nil,\n\t\t},\n\t},\n\n\t\"container create\": {\n\t\tcommandPath: \"container create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":                              ignoredArgHandler,\n\t\t\t\"--annotation\":                            ignoredArgHandler,\n\t\t\t\"--blkio-weight\":                          ignoredArgHandler,\n\t\t\t\"--blkio-weight-device\":                   ignoredArgHandler,\n\t\t\t\"--cap-add\":                               ignoredArgHandler,\n\t\t\t\"--cap-drop\":                              ignoredArgHandler,\n\t\t\t\"--cgroup-conf\":                           ignoredArgHandler,\n\t\t\t\"--cgroup-parent\":                         ignoredArgHandler,\n\t\t\t\"--cgroupns\":                              ignoredArgHandler,\n\t\t\t\"--cidfile\":                               ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--cpu-period\":                            ignoredArgHandler,\n\t\t\t\"--cpu-quota\":                             ignoredArgHandler,\n\t\t\t\"--cpu-rt-period\":                         ignoredArgHandler,\n\t\t\t\"--cpu-rt-runtime\":                        ignoredArgHandler,\n\t\t\t\"--cpu-shares\":                            ignoredArgHandler,\n\t\t\t\"--cpus\":                                  ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":                           ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":                           ignoredArgHandler,\n\t\t\t\"--detach-keys\":                           ignoredArgHandler,\n\t\t\t\"--device\":                                ignoredArgHandler,\n\t\t\t\"--device-read-bps\":                       ignoredArgHandler,\n\t\t\t\"--device-read-iops\":                      ignoredArgHandler,\n\t\t\t\"--device-write-bps\":                      ignoredArgHandler,\n\t\t\t\"--device-write-iops\":                     ignoredArgHandler,\n\t\t\t\"--dns\":                                   ignoredArgHandler,\n\t\t\t\"--dns-opt\":                               ignoredArgHandler,\n\t\t\t\"--dns-option\":                            ignoredArgHandler,\n\t\t\t\"--dns-search\":                            ignoredArgHandler,\n\t\t\t\"--domainname\":                            ignoredArgHandler,\n\t\t\t\"--entrypoint\":                            ignoredArgHandler,\n\t\t\t\"--env\":                                   ignoredArgHandler,\n\t\t\t\"--env-file\":                              ignoredArgHandler,\n\t\t\t\"--gpus\":                                  ignoredArgHandler,\n\t\t\t\"--group-add\":                             ignoredArgHandler,\n\t\t\t\"--health-cmd\":                            ignoredArgHandler,\n\t\t\t\"--health-interval\":                       ignoredArgHandler,\n\t\t\t\"--health-retries\":                        ignoredArgHandler,\n\t\t\t\"--health-start-period\":                   ignoredArgHandler,\n\t\t\t\"--health-timeout\":                        ignoredArgHandler,\n\t\t\t\"--hostname\":                              ignoredArgHandler,\n\t\t\t\"--init\":                                  nil,\n\t\t\t\"--init-binary\":                           ignoredArgHandler,\n\t\t\t\"--interactive\":                           nil,\n\t\t\t\"--ip\":                                    ignoredArgHandler,\n\t\t\t\"--ip6\":                                   ignoredArgHandler,\n\t\t\t\"--ipc\":                                   ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--isolation\":                             ignoredArgHandler,\n\t\t\t\"--kernel-memory\":                         ignoredArgHandler,\n\t\t\t\"--label\":                                 ignoredArgHandler,\n\t\t\t\"--label-file\":                            ignoredArgHandler,\n\t\t\t\"--log-driver\":                            ignoredArgHandler,\n\t\t\t\"--log-opt\":                               ignoredArgHandler,\n\t\t\t\"--mac-address\":                           ignoredArgHandler,\n\t\t\t\"--memory\":                                ignoredArgHandler,\n\t\t\t\"--memory-reservation\":                    ignoredArgHandler,\n\t\t\t\"--memory-swap\":                           ignoredArgHandler,\n\t\t\t\"--memory-swappiness\":                     ignoredArgHandler,\n\t\t\t\"--mount\":                                 ignoredArgHandler,\n\t\t\t\"--name\":                                  ignoredArgHandler,\n\t\t\t\"--net\":                                   ignoredArgHandler,\n\t\t\t\"--network\":                               ignoredArgHandler,\n\t\t\t\"--no-healthcheck\":                        nil,\n\t\t\t\"--oom-kill-disable\":                      nil,\n\t\t\t\"--oom-score-adj\":                         ignoredArgHandler,\n\t\t\t\"--pid\":                                   ignoredArgHandler,\n\t\t\t\"--pidfile\":                               ignoredArgHandler,\n\t\t\t\"--pids-limit\":                            ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--privileged\":                            nil,\n\t\t\t\"--publish\":                               ignoredArgHandler,\n\t\t\t\"--pull\":                                  ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--rdt-class\":                             ignoredArgHandler,\n\t\t\t\"--read-only\":                             nil,\n\t\t\t\"--restart\":                               ignoredArgHandler,\n\t\t\t\"--rm\":                                    nil,\n\t\t\t\"--rootfs\":                                nil,\n\t\t\t\"--runtime\":                               ignoredArgHandler,\n\t\t\t\"--security-opt\":                          ignoredArgHandler,\n\t\t\t\"--shm-size\":                              ignoredArgHandler,\n\t\t\t\"--sig-proxy\":                             nil,\n\t\t\t\"--stop-signal\":                           ignoredArgHandler,\n\t\t\t\"--stop-timeout\":                          ignoredArgHandler,\n\t\t\t\"--sysctl\":                                ignoredArgHandler,\n\t\t\t\"--systemd\":                               ignoredArgHandler,\n\t\t\t\"--tmpfs\":                                 ignoredArgHandler,\n\t\t\t\"--tty\":                                   nil,\n\t\t\t\"--ulimit\":                                ignoredArgHandler,\n\t\t\t\"--umask\":                                 ignoredArgHandler,\n\t\t\t\"--user\":                                  ignoredArgHandler,\n\t\t\t\"--userns\":                                ignoredArgHandler,\n\t\t\t\"--uts\":                                   ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"--volume\":                                ignoredArgHandler,\n\t\t\t\"--volumes-from\":                          ignoredArgHandler,\n\t\t\t\"--workdir\":                               ignoredArgHandler,\n\t\t\t\"-e\":                                      ignoredArgHandler,\n\t\t\t\"-h\":                                      ignoredArgHandler,\n\t\t\t\"-i\":                                      nil,\n\t\t\t\"-l\":                                      ignoredArgHandler,\n\t\t\t\"-m\":                                      ignoredArgHandler,\n\t\t\t\"-p\":                                      ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t\t\"-t\":                                      nil,\n\t\t\t\"-u\":                                      ignoredArgHandler,\n\t\t\t\"-v\":                                      ignoredArgHandler,\n\t\t\t\"-w\":                                      ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"container diff\": {\n\t\tcommandPath: \"container diff\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container exec\": {\n\t\tcommandPath: \"container exec\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--detach\":      nil,\n\t\t\t\"--env\":         ignoredArgHandler,\n\t\t\t\"--env-file\":    ignoredArgHandler,\n\t\t\t\"--interactive\": nil,\n\t\t\t\"--privileged\":  nil,\n\t\t\t\"--tty\":         nil,\n\t\t\t\"--user\":        ignoredArgHandler,\n\t\t\t\"--workdir\":     ignoredArgHandler,\n\t\t\t\"-d\":            nil,\n\t\t\t\"-e\":            ignoredArgHandler,\n\t\t\t\"-i\":            nil,\n\t\t\t\"-t\":            nil,\n\t\t\t\"-u\":            ignoredArgHandler,\n\t\t\t\"-w\":            ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"container export\": {\n\t\tcommandPath: \"container export\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--output\": ignoredArgHandler,\n\t\t\t\"-o\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container healthcheck\": {\n\t\tcommandPath: \"container healthcheck\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container inspect\": {\n\t\tcommandPath: \"container inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--mode\":   ignoredArgHandler,\n\t\t\t\"--size\":   nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-s\":       nil,\n\t\t},\n\t},\n\n\t\"container kill\": {\n\t\tcommandPath: \"container kill\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container logs\": {\n\t\tcommandPath: \"container logs\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--details\":    nil,\n\t\t\t\"--follow\":     nil,\n\t\t\t\"--since\":      ignoredArgHandler,\n\t\t\t\"--tail\":       ignoredArgHandler,\n\t\t\t\"--timestamps\": nil,\n\t\t\t\"--until\":      ignoredArgHandler,\n\t\t\t\"-f\":           nil,\n\t\t\t\"-n\":           ignoredArgHandler,\n\t\t\t\"-t\":           nil,\n\t\t},\n\t},\n\n\t\"container ls\": {\n\t\tcommandPath: \"container ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":      nil,\n\t\t\t\"--filter\":   ignoredArgHandler,\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--last\":     ignoredArgHandler,\n\t\t\t\"--latest\":   nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"--size\":     nil,\n\t\t\t\"-a\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-l\":         nil,\n\t\t\t\"-n\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t\t\"-s\":         nil,\n\t\t},\n\t},\n\n\t\"container pause\": {\n\t\tcommandPath: \"container pause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container port\": {\n\t\tcommandPath: \"container port\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container prune\": {\n\t\tcommandPath: \"container prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\": nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"container rename\": {\n\t\tcommandPath: \"container rename\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container restart\": {\n\t\tcommandPath: \"container restart\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"--time\":   ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t\t\"-t\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container rm\": {\n\t\tcommandPath: \"container rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\":   nil,\n\t\t\t\"--volumes\": nil,\n\t\t\t\"-f\":        nil,\n\t\t\t\"-v\":        nil,\n\t\t},\n\t},\n\n\t\"container run\": {\n\t\tcommandPath: \"container run\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":                              ignoredArgHandler,\n\t\t\t\"--annotation\":                            ignoredArgHandler,\n\t\t\t\"--attach\":                                ignoredArgHandler,\n\t\t\t\"--blkio-weight\":                          ignoredArgHandler,\n\t\t\t\"--blkio-weight-device\":                   ignoredArgHandler,\n\t\t\t\"--cap-add\":                               ignoredArgHandler,\n\t\t\t\"--cap-drop\":                              ignoredArgHandler,\n\t\t\t\"--cgroup-conf\":                           ignoredArgHandler,\n\t\t\t\"--cgroup-parent\":                         ignoredArgHandler,\n\t\t\t\"--cgroupns\":                              ignoredArgHandler,\n\t\t\t\"--cidfile\":                               ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--cpu-period\":                            ignoredArgHandler,\n\t\t\t\"--cpu-quota\":                             ignoredArgHandler,\n\t\t\t\"--cpu-rt-period\":                         ignoredArgHandler,\n\t\t\t\"--cpu-rt-runtime\":                        ignoredArgHandler,\n\t\t\t\"--cpu-shares\":                            ignoredArgHandler,\n\t\t\t\"--cpus\":                                  ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":                           ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":                           ignoredArgHandler,\n\t\t\t\"--detach\":                                nil,\n\t\t\t\"--detach-keys\":                           ignoredArgHandler,\n\t\t\t\"--device\":                                ignoredArgHandler,\n\t\t\t\"--device-read-bps\":                       ignoredArgHandler,\n\t\t\t\"--device-read-iops\":                      ignoredArgHandler,\n\t\t\t\"--device-write-bps\":                      ignoredArgHandler,\n\t\t\t\"--device-write-iops\":                     ignoredArgHandler,\n\t\t\t\"--dns\":                                   ignoredArgHandler,\n\t\t\t\"--dns-opt\":                               ignoredArgHandler,\n\t\t\t\"--dns-option\":                            ignoredArgHandler,\n\t\t\t\"--dns-search\":                            ignoredArgHandler,\n\t\t\t\"--domainname\":                            ignoredArgHandler,\n\t\t\t\"--entrypoint\":                            ignoredArgHandler,\n\t\t\t\"--env\":                                   ignoredArgHandler,\n\t\t\t\"--env-file\":                              ignoredArgHandler,\n\t\t\t\"--gpus\":                                  ignoredArgHandler,\n\t\t\t\"--group-add\":                             ignoredArgHandler,\n\t\t\t\"--health-cmd\":                            ignoredArgHandler,\n\t\t\t\"--health-interval\":                       ignoredArgHandler,\n\t\t\t\"--health-retries\":                        ignoredArgHandler,\n\t\t\t\"--health-start-period\":                   ignoredArgHandler,\n\t\t\t\"--health-timeout\":                        ignoredArgHandler,\n\t\t\t\"--hostname\":                              ignoredArgHandler,\n\t\t\t\"--init\":                                  nil,\n\t\t\t\"--init-binary\":                           ignoredArgHandler,\n\t\t\t\"--interactive\":                           nil,\n\t\t\t\"--ip\":                                    ignoredArgHandler,\n\t\t\t\"--ip6\":                                   ignoredArgHandler,\n\t\t\t\"--ipc\":                                   ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--isolation\":                             ignoredArgHandler,\n\t\t\t\"--kernel-memory\":                         ignoredArgHandler,\n\t\t\t\"--label\":                                 ignoredArgHandler,\n\t\t\t\"--label-file\":                            ignoredArgHandler,\n\t\t\t\"--log-driver\":                            ignoredArgHandler,\n\t\t\t\"--log-opt\":                               ignoredArgHandler,\n\t\t\t\"--mac-address\":                           ignoredArgHandler,\n\t\t\t\"--memory\":                                ignoredArgHandler,\n\t\t\t\"--memory-reservation\":                    ignoredArgHandler,\n\t\t\t\"--memory-swap\":                           ignoredArgHandler,\n\t\t\t\"--memory-swappiness\":                     ignoredArgHandler,\n\t\t\t\"--mount\":                                 ignoredArgHandler,\n\t\t\t\"--name\":                                  ignoredArgHandler,\n\t\t\t\"--net\":                                   ignoredArgHandler,\n\t\t\t\"--network\":                               ignoredArgHandler,\n\t\t\t\"--no-healthcheck\":                        nil,\n\t\t\t\"--oom-kill-disable\":                      nil,\n\t\t\t\"--oom-score-adj\":                         ignoredArgHandler,\n\t\t\t\"--pid\":                                   ignoredArgHandler,\n\t\t\t\"--pidfile\":                               ignoredArgHandler,\n\t\t\t\"--pids-limit\":                            ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--privileged\":                            nil,\n\t\t\t\"--publish\":                               ignoredArgHandler,\n\t\t\t\"--pull\":                                  ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--rdt-class\":                             ignoredArgHandler,\n\t\t\t\"--read-only\":                             nil,\n\t\t\t\"--restart\":                               ignoredArgHandler,\n\t\t\t\"--rm\":                                    nil,\n\t\t\t\"--rootfs\":                                nil,\n\t\t\t\"--runtime\":                               ignoredArgHandler,\n\t\t\t\"--security-opt\":                          ignoredArgHandler,\n\t\t\t\"--shm-size\":                              ignoredArgHandler,\n\t\t\t\"--sig-proxy\":                             nil,\n\t\t\t\"--stop-signal\":                           ignoredArgHandler,\n\t\t\t\"--stop-timeout\":                          ignoredArgHandler,\n\t\t\t\"--sysctl\":                                ignoredArgHandler,\n\t\t\t\"--systemd\":                               ignoredArgHandler,\n\t\t\t\"--tmpfs\":                                 ignoredArgHandler,\n\t\t\t\"--tty\":                                   nil,\n\t\t\t\"--ulimit\":                                ignoredArgHandler,\n\t\t\t\"--umask\":                                 ignoredArgHandler,\n\t\t\t\"--user\":                                  ignoredArgHandler,\n\t\t\t\"--userns\":                                ignoredArgHandler,\n\t\t\t\"--uts\":                                   ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"--volume\":                                ignoredArgHandler,\n\t\t\t\"--volumes-from\":                          ignoredArgHandler,\n\t\t\t\"--workdir\":                               ignoredArgHandler,\n\t\t\t\"-a\":                                      ignoredArgHandler,\n\t\t\t\"-d\":                                      nil,\n\t\t\t\"-e\":                                      ignoredArgHandler,\n\t\t\t\"-h\":                                      ignoredArgHandler,\n\t\t\t\"-i\":                                      nil,\n\t\t\t\"-l\":                                      ignoredArgHandler,\n\t\t\t\"-m\":                                      ignoredArgHandler,\n\t\t\t\"-p\":                                      ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t\t\"-t\":                                      nil,\n\t\t\t\"-u\":                                      ignoredArgHandler,\n\t\t\t\"-v\":                                      ignoredArgHandler,\n\t\t\t\"-w\":                                      ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"container start\": {\n\t\tcommandPath: \"container start\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--attach\":         nil,\n\t\t\t\"--checkpoint\":     ignoredArgHandler,\n\t\t\t\"--checkpoint-dir\": ignoredArgHandler,\n\t\t\t\"--detach-keys\":    ignoredArgHandler,\n\t\t\t\"--interactive\":    nil,\n\t\t\t\"-a\":               nil,\n\t\t\t\"-i\":               nil,\n\t\t},\n\t},\n\n\t\"container stats\": {\n\t\tcommandPath: \"container stats\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":       nil,\n\t\t\t\"--format\":    ignoredArgHandler,\n\t\t\t\"--no-stream\": nil,\n\t\t\t\"--no-trunc\":  nil,\n\t\t\t\"-a\":          nil,\n\t\t},\n\t},\n\n\t\"container stop\": {\n\t\tcommandPath: \"container stop\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"--time\":   ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t\t\"-t\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container unpause\": {\n\t\tcommandPath: \"container unpause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"container update\": {\n\t\tcommandPath: \"container update\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--blkio-weight\":       ignoredArgHandler,\n\t\t\t\"--cpu-period\":         ignoredArgHandler,\n\t\t\t\"--cpu-quota\":          ignoredArgHandler,\n\t\t\t\"--cpu-shares\":         ignoredArgHandler,\n\t\t\t\"--cpus\":               ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":        ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":        ignoredArgHandler,\n\t\t\t\"--kernel-memory\":      ignoredArgHandler,\n\t\t\t\"--memory\":             ignoredArgHandler,\n\t\t\t\"--memory-reservation\": ignoredArgHandler,\n\t\t\t\"--memory-swap\":        ignoredArgHandler,\n\t\t\t\"--pids-limit\":         ignoredArgHandler,\n\t\t\t\"--restart\":            ignoredArgHandler,\n\t\t\t\"-m\":                   ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"container wait\": {\n\t\tcommandPath: \"container wait\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"cp\": {\n\t\tcommandPath: \"cp\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--follow-link\": nil,\n\t\t\t\"-L\":            nil,\n\t\t},\n\t},\n\n\t\"create\": {\n\t\tcommandPath: \"create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":                              ignoredArgHandler,\n\t\t\t\"--annotation\":                            ignoredArgHandler,\n\t\t\t\"--blkio-weight\":                          ignoredArgHandler,\n\t\t\t\"--blkio-weight-device\":                   ignoredArgHandler,\n\t\t\t\"--cap-add\":                               ignoredArgHandler,\n\t\t\t\"--cap-drop\":                              ignoredArgHandler,\n\t\t\t\"--cgroup-conf\":                           ignoredArgHandler,\n\t\t\t\"--cgroup-parent\":                         ignoredArgHandler,\n\t\t\t\"--cgroupns\":                              ignoredArgHandler,\n\t\t\t\"--cidfile\":                               ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--cpu-period\":                            ignoredArgHandler,\n\t\t\t\"--cpu-quota\":                             ignoredArgHandler,\n\t\t\t\"--cpu-rt-period\":                         ignoredArgHandler,\n\t\t\t\"--cpu-rt-runtime\":                        ignoredArgHandler,\n\t\t\t\"--cpu-shares\":                            ignoredArgHandler,\n\t\t\t\"--cpus\":                                  ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":                           ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":                           ignoredArgHandler,\n\t\t\t\"--detach-keys\":                           ignoredArgHandler,\n\t\t\t\"--device\":                                ignoredArgHandler,\n\t\t\t\"--device-read-bps\":                       ignoredArgHandler,\n\t\t\t\"--device-read-iops\":                      ignoredArgHandler,\n\t\t\t\"--device-write-bps\":                      ignoredArgHandler,\n\t\t\t\"--device-write-iops\":                     ignoredArgHandler,\n\t\t\t\"--dns\":                                   ignoredArgHandler,\n\t\t\t\"--dns-opt\":                               ignoredArgHandler,\n\t\t\t\"--dns-option\":                            ignoredArgHandler,\n\t\t\t\"--dns-search\":                            ignoredArgHandler,\n\t\t\t\"--domainname\":                            ignoredArgHandler,\n\t\t\t\"--entrypoint\":                            ignoredArgHandler,\n\t\t\t\"--env\":                                   ignoredArgHandler,\n\t\t\t\"--env-file\":                              ignoredArgHandler,\n\t\t\t\"--gpus\":                                  ignoredArgHandler,\n\t\t\t\"--group-add\":                             ignoredArgHandler,\n\t\t\t\"--health-cmd\":                            ignoredArgHandler,\n\t\t\t\"--health-interval\":                       ignoredArgHandler,\n\t\t\t\"--health-retries\":                        ignoredArgHandler,\n\t\t\t\"--health-start-period\":                   ignoredArgHandler,\n\t\t\t\"--health-timeout\":                        ignoredArgHandler,\n\t\t\t\"--hostname\":                              ignoredArgHandler,\n\t\t\t\"--init\":                                  nil,\n\t\t\t\"--init-binary\":                           ignoredArgHandler,\n\t\t\t\"--interactive\":                           nil,\n\t\t\t\"--ip\":                                    ignoredArgHandler,\n\t\t\t\"--ip6\":                                   ignoredArgHandler,\n\t\t\t\"--ipc\":                                   ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--isolation\":                             ignoredArgHandler,\n\t\t\t\"--kernel-memory\":                         ignoredArgHandler,\n\t\t\t\"--label\":                                 ignoredArgHandler,\n\t\t\t\"--label-file\":                            ignoredArgHandler,\n\t\t\t\"--log-driver\":                            ignoredArgHandler,\n\t\t\t\"--log-opt\":                               ignoredArgHandler,\n\t\t\t\"--mac-address\":                           ignoredArgHandler,\n\t\t\t\"--memory\":                                ignoredArgHandler,\n\t\t\t\"--memory-reservation\":                    ignoredArgHandler,\n\t\t\t\"--memory-swap\":                           ignoredArgHandler,\n\t\t\t\"--memory-swappiness\":                     ignoredArgHandler,\n\t\t\t\"--mount\":                                 ignoredArgHandler,\n\t\t\t\"--name\":                                  ignoredArgHandler,\n\t\t\t\"--net\":                                   ignoredArgHandler,\n\t\t\t\"--network\":                               ignoredArgHandler,\n\t\t\t\"--no-healthcheck\":                        nil,\n\t\t\t\"--oom-kill-disable\":                      nil,\n\t\t\t\"--oom-score-adj\":                         ignoredArgHandler,\n\t\t\t\"--pid\":                                   ignoredArgHandler,\n\t\t\t\"--pidfile\":                               ignoredArgHandler,\n\t\t\t\"--pids-limit\":                            ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--privileged\":                            nil,\n\t\t\t\"--publish\":                               ignoredArgHandler,\n\t\t\t\"--pull\":                                  ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--rdt-class\":                             ignoredArgHandler,\n\t\t\t\"--read-only\":                             nil,\n\t\t\t\"--restart\":                               ignoredArgHandler,\n\t\t\t\"--rm\":                                    nil,\n\t\t\t\"--rootfs\":                                nil,\n\t\t\t\"--runtime\":                               ignoredArgHandler,\n\t\t\t\"--security-opt\":                          ignoredArgHandler,\n\t\t\t\"--shm-size\":                              ignoredArgHandler,\n\t\t\t\"--sig-proxy\":                             nil,\n\t\t\t\"--stop-signal\":                           ignoredArgHandler,\n\t\t\t\"--stop-timeout\":                          ignoredArgHandler,\n\t\t\t\"--sysctl\":                                ignoredArgHandler,\n\t\t\t\"--systemd\":                               ignoredArgHandler,\n\t\t\t\"--tmpfs\":                                 ignoredArgHandler,\n\t\t\t\"--tty\":                                   nil,\n\t\t\t\"--ulimit\":                                ignoredArgHandler,\n\t\t\t\"--umask\":                                 ignoredArgHandler,\n\t\t\t\"--user\":                                  ignoredArgHandler,\n\t\t\t\"--userns\":                                ignoredArgHandler,\n\t\t\t\"--uts\":                                   ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"--volume\":                                ignoredArgHandler,\n\t\t\t\"--volumes-from\":                          ignoredArgHandler,\n\t\t\t\"--workdir\":                               ignoredArgHandler,\n\t\t\t\"-e\":                                      ignoredArgHandler,\n\t\t\t\"-h\":                                      ignoredArgHandler,\n\t\t\t\"-i\":                                      nil,\n\t\t\t\"-l\":                                      ignoredArgHandler,\n\t\t\t\"-m\":                                      ignoredArgHandler,\n\t\t\t\"-p\":                                      ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t\t\"-t\":                                      nil,\n\t\t\t\"-u\":                                      ignoredArgHandler,\n\t\t\t\"-v\":                                      ignoredArgHandler,\n\t\t\t\"-w\":                                      ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"diff\": {\n\t\tcommandPath: \"diff\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"events\": {\n\t\tcommandPath: \"events\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--filter\": ignoredArgHandler,\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"exec\": {\n\t\tcommandPath: \"exec\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--detach\":      nil,\n\t\t\t\"--env\":         ignoredArgHandler,\n\t\t\t\"--env-file\":    ignoredArgHandler,\n\t\t\t\"--interactive\": nil,\n\t\t\t\"--privileged\":  nil,\n\t\t\t\"--tty\":         nil,\n\t\t\t\"--user\":        ignoredArgHandler,\n\t\t\t\"--workdir\":     ignoredArgHandler,\n\t\t\t\"-d\":            nil,\n\t\t\t\"-e\":            ignoredArgHandler,\n\t\t\t\"-i\":            nil,\n\t\t\t\"-t\":            nil,\n\t\t\t\"-u\":            ignoredArgHandler,\n\t\t\t\"-w\":            ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"export\": {\n\t\tcommandPath: \"export\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--output\": ignoredArgHandler,\n\t\t\t\"-o\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"healthcheck\": {\n\t\tcommandPath: \"healthcheck\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"help\": {\n\t\tcommandPath: \"help\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"history\": {\n\t\tcommandPath: \"history\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--human\":    nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"-H\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"image\": {\n\t\tcommandPath: \"image\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"build\":   {},\n\t\t\t\"convert\": {},\n\t\t\t\"decrypt\": {},\n\t\t\t\"encrypt\": {},\n\t\t\t\"history\": {},\n\t\t\t\"import\":  {},\n\t\t\t\"inspect\": {},\n\t\t\t\"load\":    {},\n\t\t\t\"ls\":      {},\n\t\t\t\"prune\":   {},\n\t\t\t\"pull\":    {},\n\t\t\t\"push\":    {},\n\t\t\t\"rm\":      {},\n\t\t\t\"save\":    {},\n\t\t\t\"tag\":     {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"image build\": {\n\t\tcommandPath: \"image build\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":      ignoredArgHandler,\n\t\t\t\"--allow\":         ignoredArgHandler,\n\t\t\t\"--attest\":        ignoredArgHandler,\n\t\t\t\"--build-arg\":     ignoredArgHandler,\n\t\t\t\"--build-context\": ignoredArgHandler,\n\t\t\t\"--buildkit-host\": ignoredArgHandler,\n\t\t\t\"--cache-from\":    ignoredArgHandler,\n\t\t\t\"--cache-to\":      ignoredArgHandler,\n\t\t\t\"--file\":          ignoredArgHandler,\n\t\t\t\"--iidfile\":       ignoredArgHandler,\n\t\t\t\"--label\":         ignoredArgHandler,\n\t\t\t\"--network\":       ignoredArgHandler,\n\t\t\t\"--no-cache\":      nil,\n\t\t\t\"--output\":        ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--progress\":      ignoredArgHandler,\n\t\t\t\"--provenance\":    ignoredArgHandler,\n\t\t\t\"--pull\":          nil,\n\t\t\t\"--quiet\":         nil,\n\t\t\t\"--rm\":            nil,\n\t\t\t\"--sbom\":          ignoredArgHandler,\n\t\t\t\"--secret\":        ignoredArgHandler,\n\t\t\t\"--ssh\":           ignoredArgHandler,\n\t\t\t\"--tag\":           ignoredArgHandler,\n\t\t\t\"--target\":        ignoredArgHandler,\n\t\t\t\"-f\":              ignoredArgHandler,\n\t\t\t\"-o\":              ignoredArgHandler,\n\t\t\t\"-q\":              nil,\n\t\t\t\"-t\":              ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image convert\": {\n\t\tcommandPath: \"image convert\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\":                 nil,\n\t\t\t\"--estargz\":                       nil,\n\t\t\t\"--estargz-chunk-size\":            ignoredArgHandler,\n\t\t\t\"--estargz-compression-level\":     ignoredArgHandler,\n\t\t\t\"--estargz-external-toc\":          nil,\n\t\t\t\"--estargz-gzip-helper\":           ignoredArgHandler,\n\t\t\t\"--estargz-keep-diff-id\":          nil,\n\t\t\t\"--estargz-min-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--estargz-record-in\":             ignoredArgHandler,\n\t\t\t\"--format\":                        ignoredArgHandler,\n\t\t\t\"--nydus\":                         nil,\n\t\t\t\"--nydus-builder-path\":            ignoredArgHandler,\n\t\t\t\"--nydus-compressor\":              ignoredArgHandler,\n\t\t\t\"--nydus-prefetch-patterns\":       ignoredArgHandler,\n\t\t\t\"--nydus-work-dir\":                ignoredArgHandler,\n\t\t\t\"--oci\":                           nil,\n\t\t\t\"--overlaybd\":                     nil,\n\t\t\t\"--overlaybd-dbstr\":               ignoredArgHandler,\n\t\t\t\"--overlaybd-fs-type\":             ignoredArgHandler,\n\t\t\t\"--platform\":                      ignoredArgHandler,\n\t\t\t\"--soci\":                          nil,\n\t\t\t\"--soci-min-layer-size\":           ignoredArgHandler,\n\t\t\t\"--soci-span-size\":                ignoredArgHandler,\n\t\t\t\"--uncompress\":                    nil,\n\t\t\t\"--zstd\":                          nil,\n\t\t\t\"--zstd-compression-level\":        ignoredArgHandler,\n\t\t\t\"--zstdchunked\":                   nil,\n\t\t\t\"--zstdchunked-chunk-size\":        ignoredArgHandler,\n\t\t\t\"--zstdchunked-compression-level\": ignoredArgHandler,\n\t\t\t\"--zstdchunked-record-in\":         ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image decrypt\": {\n\t\tcommandPath: \"image decrypt\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--dec-recipient\": ignoredArgHandler,\n\t\t\t\"--gpg-homedir\":   ignoredArgHandler,\n\t\t\t\"--gpg-version\":   ignoredArgHandler,\n\t\t\t\"--key\":           ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image encrypt\": {\n\t\tcommandPath: \"image encrypt\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--dec-recipient\": ignoredArgHandler,\n\t\t\t\"--gpg-homedir\":   ignoredArgHandler,\n\t\t\t\"--gpg-version\":   ignoredArgHandler,\n\t\t\t\"--key\":           ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--recipient\":     ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image history\": {\n\t\tcommandPath: \"image history\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--human\":    nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"-H\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"image import\": {\n\t\tcommandPath: \"image import\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--message\":  ignoredArgHandler,\n\t\t\t\"--platform\": ignoredArgHandler,\n\t\t\t\"-m\":         ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image inspect\": {\n\t\tcommandPath: \"image inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--mode\":     ignoredArgHandler,\n\t\t\t\"--platform\": ignoredArgHandler,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image load\": {\n\t\tcommandPath: \"image load\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--input\":         ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--quiet\":         nil,\n\t\t\t\"-i\":              ignoredArgHandler,\n\t\t\t\"-q\":              nil,\n\t\t},\n\t},\n\n\t\"image ls\": {\n\t\tcommandPath: \"image ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":      nil,\n\t\t\t\"--digests\":  nil,\n\t\t\t\"--filter\":   ignoredArgHandler,\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--names\":    nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"-a\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"image prune\": {\n\t\tcommandPath: \"image prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":    nil,\n\t\t\t\"--filter\": ignoredArgHandler,\n\t\t\t\"--force\":  nil,\n\t\t\t\"-a\":       nil,\n\t\t\t\"-f\":       nil,\n\t\t},\n\t},\n\n\t\"image pull\": {\n\t\tcommandPath: \"image pull\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\":                         nil,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--soci-index-digest\":                     ignoredArgHandler,\n\t\t\t\"--unpack\":                                ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t},\n\t},\n\n\t\"image push\": {\n\t\tcommandPath: \"image push\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\":                    nil,\n\t\t\t\"--allow-nondistributable-artifacts\": nil,\n\t\t\t\"--cosign-key\":                       ignoredArgHandler,\n\t\t\t\"--estargz\":                          nil,\n\t\t\t\"--ipfs-address\":                     ignoredArgHandler,\n\t\t\t\"--ipfs-ensure-image\":                nil,\n\t\t\t\"--notation-key-name\":                ignoredArgHandler,\n\t\t\t\"--platform\":                         ignoredArgHandler,\n\t\t\t\"--quiet\":                            nil,\n\t\t\t\"--sign\":                             ignoredArgHandler,\n\t\t\t\"--soci-min-layer-size\":              ignoredArgHandler,\n\t\t\t\"--soci-span-size\":                   ignoredArgHandler,\n\t\t\t\"-q\":                                 nil,\n\t\t},\n\t},\n\n\t\"image rm\": {\n\t\tcommandPath: \"image rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--async\": nil,\n\t\t\t\"--force\": nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"image save\": {\n\t\tcommandPath: \"image save\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--output\":        ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"-o\":              ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"image tag\": {\n\t\tcommandPath: \"image tag\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"images\": {\n\t\tcommandPath: \"images\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":      nil,\n\t\t\t\"--digests\":  nil,\n\t\t\t\"--filter\":   ignoredArgHandler,\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--names\":    nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"-a\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t},\n\t},\n\n\t\"import\": {\n\t\tcommandPath: \"import\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--message\":  ignoredArgHandler,\n\t\t\t\"--platform\": ignoredArgHandler,\n\t\t\t\"-m\":         ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"info\": {\n\t\tcommandPath: \"info\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--mode\":   ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"inspect\": {\n\t\tcommandPath: \"inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--mode\":   ignoredArgHandler,\n\t\t\t\"--size\":   nil,\n\t\t\t\"--type\":   ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-s\":       nil,\n\t\t},\n\t},\n\n\t\"ipfs\": {\n\t\tcommandPath: \"ipfs\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"registry\": {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"ipfs registry\": {\n\t\tcommandPath: \"ipfs registry\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"serve\": {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"ipfs registry serve\": {\n\t\tcommandPath: \"ipfs registry serve\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--ipfs-address\":    ignoredArgHandler,\n\t\t\t\"--listen-registry\": ignoredArgHandler,\n\t\t\t\"--read-retry-num\":  ignoredArgHandler,\n\t\t\t\"--read-timeout\":    ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"kill\": {\n\t\tcommandPath: \"kill\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"load\": {\n\t\tcommandPath: \"load\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--input\":         ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"--quiet\":         nil,\n\t\t\t\"-i\":              ignoredArgHandler,\n\t\t\t\"-q\":              nil,\n\t\t},\n\t},\n\n\t\"login\": {\n\t\tcommandPath: \"login\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--password\":       ignoredArgHandler,\n\t\t\t\"--password-stdin\": nil,\n\t\t\t\"--username\":       ignoredArgHandler,\n\t\t\t\"-p\":               ignoredArgHandler,\n\t\t\t\"-u\":               ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"logout\": {\n\t\tcommandPath: \"logout\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"logs\": {\n\t\tcommandPath: \"logs\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--details\":    nil,\n\t\t\t\"--follow\":     nil,\n\t\t\t\"--since\":      ignoredArgHandler,\n\t\t\t\"--tail\":       ignoredArgHandler,\n\t\t\t\"--timestamps\": nil,\n\t\t\t\"--until\":      ignoredArgHandler,\n\t\t\t\"-f\":           nil,\n\t\t\t\"-n\":           ignoredArgHandler,\n\t\t\t\"-t\":           nil,\n\t\t},\n\t},\n\n\t\"manifest\": {\n\t\tcommandPath: \"manifest\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"annotate\": {},\n\t\t\t\"create\":   {},\n\t\t\t\"inspect\":  {},\n\t\t\t\"push\":     {},\n\t\t\t\"rm\":       {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"manifest annotate\": {\n\t\tcommandPath: \"manifest annotate\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--arch\":        ignoredArgHandler,\n\t\t\t\"--os\":          ignoredArgHandler,\n\t\t\t\"--os-features\": ignoredArgHandler,\n\t\t\t\"--os-version\":  ignoredArgHandler,\n\t\t\t\"--variant\":     ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"manifest create\": {\n\t\tcommandPath: \"manifest create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--amend\":    nil,\n\t\t\t\"--insecure\": nil,\n\t\t},\n\t},\n\n\t\"manifest inspect\": {\n\t\tcommandPath: \"manifest inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--insecure\": nil,\n\t\t\t\"--verbose\":  nil,\n\t\t},\n\t},\n\n\t\"manifest push\": {\n\t\tcommandPath: \"manifest push\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--insecure\": nil,\n\t\t\t\"--purge\":    nil,\n\t\t},\n\t},\n\n\t\"manifest rm\": {\n\t\tcommandPath: \"manifest rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"namespace\": {\n\t\tcommandPath: \"namespace\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"create\":  {},\n\t\t\t\"inspect\": {},\n\t\t\t\"ls\":      {},\n\t\t\t\"remove\":  {},\n\t\t\t\"update\":  {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"namespace create\": {\n\t\tcommandPath: \"namespace create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--label\": ignoredArgHandler,\n\t\t\t\"-l\":      ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"namespace inspect\": {\n\t\tcommandPath: \"namespace inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"namespace ls\": {\n\t\tcommandPath: \"namespace ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--quiet\":  nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-q\":       nil,\n\t\t},\n\t},\n\n\t\"namespace remove\": {\n\t\tcommandPath: \"namespace remove\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--cgroup\": nil,\n\t\t\t\"-c\":       nil,\n\t\t},\n\t},\n\n\t\"namespace update\": {\n\t\tcommandPath: \"namespace update\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--label\": ignoredArgHandler,\n\t\t\t\"-l\":      ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"network\": {\n\t\tcommandPath: \"network\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"create\":  {},\n\t\t\t\"inspect\": {},\n\t\t\t\"ls\":      {},\n\t\t\t\"prune\":   {},\n\t\t\t\"rm\":      {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"network create\": {\n\t\tcommandPath: \"network create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--driver\":      ignoredArgHandler,\n\t\t\t\"--gateway\":     ignoredArgHandler,\n\t\t\t\"--internal\":    nil,\n\t\t\t\"--ip-range\":    ignoredArgHandler,\n\t\t\t\"--ipam-driver\": ignoredArgHandler,\n\t\t\t\"--ipam-opt\":    ignoredArgHandler,\n\t\t\t\"--ipv6\":        nil,\n\t\t\t\"--label\":       ignoredArgHandler,\n\t\t\t\"--opt\":         ignoredArgHandler,\n\t\t\t\"--subnet\":      ignoredArgHandler,\n\t\t\t\"-d\":            ignoredArgHandler,\n\t\t\t\"-o\":            ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"network inspect\": {\n\t\tcommandPath: \"network inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--mode\":   ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"network ls\": {\n\t\tcommandPath: \"network ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--filter\": ignoredArgHandler,\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--quiet\":  nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-q\":       nil,\n\t\t},\n\t},\n\n\t\"network prune\": {\n\t\tcommandPath: \"network prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\": nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"network rm\": {\n\t\tcommandPath: \"network rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"pause\": {\n\t\tcommandPath: \"pause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"port\": {\n\t\tcommandPath: \"port\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"ps\": {\n\t\tcommandPath: \"ps\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":      nil,\n\t\t\t\"--filter\":   ignoredArgHandler,\n\t\t\t\"--format\":   ignoredArgHandler,\n\t\t\t\"--last\":     ignoredArgHandler,\n\t\t\t\"--latest\":   nil,\n\t\t\t\"--no-trunc\": nil,\n\t\t\t\"--quiet\":    nil,\n\t\t\t\"--size\":     nil,\n\t\t\t\"-a\":         nil,\n\t\t\t\"-f\":         ignoredArgHandler,\n\t\t\t\"-l\":         nil,\n\t\t\t\"-n\":         ignoredArgHandler,\n\t\t\t\"-q\":         nil,\n\t\t\t\"-s\":         nil,\n\t\t},\n\t},\n\n\t\"pull\": {\n\t\tcommandPath: \"pull\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\":                         nil,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--soci-index-digest\":                     ignoredArgHandler,\n\t\t\t\"--unpack\":                                ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t},\n\t},\n\n\t\"push\": {\n\t\tcommandPath: \"push\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\":                    nil,\n\t\t\t\"--allow-nondistributable-artifacts\": nil,\n\t\t\t\"--cosign-key\":                       ignoredArgHandler,\n\t\t\t\"--estargz\":                          nil,\n\t\t\t\"--ipfs-address\":                     ignoredArgHandler,\n\t\t\t\"--ipfs-ensure-image\":                nil,\n\t\t\t\"--notation-key-name\":                ignoredArgHandler,\n\t\t\t\"--platform\":                         ignoredArgHandler,\n\t\t\t\"--quiet\":                            nil,\n\t\t\t\"--sign\":                             ignoredArgHandler,\n\t\t\t\"--soci-min-layer-size\":              ignoredArgHandler,\n\t\t\t\"--soci-span-size\":                   ignoredArgHandler,\n\t\t\t\"-q\":                                 nil,\n\t\t},\n\t},\n\n\t\"rename\": {\n\t\tcommandPath: \"rename\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"restart\": {\n\t\tcommandPath: \"restart\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"--time\":   ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t\t\"-t\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"rm\": {\n\t\tcommandPath: \"rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\":   nil,\n\t\t\t\"--volumes\": nil,\n\t\t\t\"-f\":        nil,\n\t\t\t\"-v\":        nil,\n\t\t},\n\t},\n\n\t\"rmi\": {\n\t\tcommandPath: \"rmi\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--async\": nil,\n\t\t\t\"--force\": nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"run\": {\n\t\tcommandPath: \"run\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--add-host\":                              ignoredArgHandler,\n\t\t\t\"--annotation\":                            ignoredArgHandler,\n\t\t\t\"--attach\":                                ignoredArgHandler,\n\t\t\t\"--blkio-weight\":                          ignoredArgHandler,\n\t\t\t\"--blkio-weight-device\":                   ignoredArgHandler,\n\t\t\t\"--cap-add\":                               ignoredArgHandler,\n\t\t\t\"--cap-drop\":                              ignoredArgHandler,\n\t\t\t\"--cgroup-conf\":                           ignoredArgHandler,\n\t\t\t\"--cgroup-parent\":                         ignoredArgHandler,\n\t\t\t\"--cgroupns\":                              ignoredArgHandler,\n\t\t\t\"--cidfile\":                               ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity\":           ignoredArgHandler,\n\t\t\t\"--cosign-certificate-identity-regexp\":    ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer\":        ignoredArgHandler,\n\t\t\t\"--cosign-certificate-oidc-issuer-regexp\": ignoredArgHandler,\n\t\t\t\"--cosign-key\":                            ignoredArgHandler,\n\t\t\t\"--cpu-period\":                            ignoredArgHandler,\n\t\t\t\"--cpu-quota\":                             ignoredArgHandler,\n\t\t\t\"--cpu-rt-period\":                         ignoredArgHandler,\n\t\t\t\"--cpu-rt-runtime\":                        ignoredArgHandler,\n\t\t\t\"--cpu-shares\":                            ignoredArgHandler,\n\t\t\t\"--cpus\":                                  ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":                           ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":                           ignoredArgHandler,\n\t\t\t\"--detach\":                                nil,\n\t\t\t\"--detach-keys\":                           ignoredArgHandler,\n\t\t\t\"--device\":                                ignoredArgHandler,\n\t\t\t\"--device-read-bps\":                       ignoredArgHandler,\n\t\t\t\"--device-read-iops\":                      ignoredArgHandler,\n\t\t\t\"--device-write-bps\":                      ignoredArgHandler,\n\t\t\t\"--device-write-iops\":                     ignoredArgHandler,\n\t\t\t\"--dns\":                                   ignoredArgHandler,\n\t\t\t\"--dns-opt\":                               ignoredArgHandler,\n\t\t\t\"--dns-option\":                            ignoredArgHandler,\n\t\t\t\"--dns-search\":                            ignoredArgHandler,\n\t\t\t\"--domainname\":                            ignoredArgHandler,\n\t\t\t\"--entrypoint\":                            ignoredArgHandler,\n\t\t\t\"--env\":                                   ignoredArgHandler,\n\t\t\t\"--env-file\":                              ignoredArgHandler,\n\t\t\t\"--gpus\":                                  ignoredArgHandler,\n\t\t\t\"--group-add\":                             ignoredArgHandler,\n\t\t\t\"--health-cmd\":                            ignoredArgHandler,\n\t\t\t\"--health-interval\":                       ignoredArgHandler,\n\t\t\t\"--health-retries\":                        ignoredArgHandler,\n\t\t\t\"--health-start-period\":                   ignoredArgHandler,\n\t\t\t\"--health-timeout\":                        ignoredArgHandler,\n\t\t\t\"--hostname\":                              ignoredArgHandler,\n\t\t\t\"--init\":                                  nil,\n\t\t\t\"--init-binary\":                           ignoredArgHandler,\n\t\t\t\"--interactive\":                           nil,\n\t\t\t\"--ip\":                                    ignoredArgHandler,\n\t\t\t\"--ip6\":                                   ignoredArgHandler,\n\t\t\t\"--ipc\":                                   ignoredArgHandler,\n\t\t\t\"--ipfs-address\":                          ignoredArgHandler,\n\t\t\t\"--isolation\":                             ignoredArgHandler,\n\t\t\t\"--kernel-memory\":                         ignoredArgHandler,\n\t\t\t\"--label\":                                 ignoredArgHandler,\n\t\t\t\"--label-file\":                            ignoredArgHandler,\n\t\t\t\"--log-driver\":                            ignoredArgHandler,\n\t\t\t\"--log-opt\":                               ignoredArgHandler,\n\t\t\t\"--mac-address\":                           ignoredArgHandler,\n\t\t\t\"--memory\":                                ignoredArgHandler,\n\t\t\t\"--memory-reservation\":                    ignoredArgHandler,\n\t\t\t\"--memory-swap\":                           ignoredArgHandler,\n\t\t\t\"--memory-swappiness\":                     ignoredArgHandler,\n\t\t\t\"--mount\":                                 ignoredArgHandler,\n\t\t\t\"--name\":                                  ignoredArgHandler,\n\t\t\t\"--net\":                                   ignoredArgHandler,\n\t\t\t\"--network\":                               ignoredArgHandler,\n\t\t\t\"--no-healthcheck\":                        nil,\n\t\t\t\"--oom-kill-disable\":                      nil,\n\t\t\t\"--oom-score-adj\":                         ignoredArgHandler,\n\t\t\t\"--pid\":                                   ignoredArgHandler,\n\t\t\t\"--pidfile\":                               ignoredArgHandler,\n\t\t\t\"--pids-limit\":                            ignoredArgHandler,\n\t\t\t\"--platform\":                              ignoredArgHandler,\n\t\t\t\"--privileged\":                            nil,\n\t\t\t\"--publish\":                               ignoredArgHandler,\n\t\t\t\"--pull\":                                  ignoredArgHandler,\n\t\t\t\"--quiet\":                                 nil,\n\t\t\t\"--rdt-class\":                             ignoredArgHandler,\n\t\t\t\"--read-only\":                             nil,\n\t\t\t\"--restart\":                               ignoredArgHandler,\n\t\t\t\"--rm\":                                    nil,\n\t\t\t\"--rootfs\":                                nil,\n\t\t\t\"--runtime\":                               ignoredArgHandler,\n\t\t\t\"--security-opt\":                          ignoredArgHandler,\n\t\t\t\"--shm-size\":                              ignoredArgHandler,\n\t\t\t\"--sig-proxy\":                             nil,\n\t\t\t\"--stop-signal\":                           ignoredArgHandler,\n\t\t\t\"--stop-timeout\":                          ignoredArgHandler,\n\t\t\t\"--sysctl\":                                ignoredArgHandler,\n\t\t\t\"--systemd\":                               ignoredArgHandler,\n\t\t\t\"--tmpfs\":                                 ignoredArgHandler,\n\t\t\t\"--tty\":                                   nil,\n\t\t\t\"--ulimit\":                                ignoredArgHandler,\n\t\t\t\"--umask\":                                 ignoredArgHandler,\n\t\t\t\"--user\":                                  ignoredArgHandler,\n\t\t\t\"--userns\":                                ignoredArgHandler,\n\t\t\t\"--uts\":                                   ignoredArgHandler,\n\t\t\t\"--verify\":                                ignoredArgHandler,\n\t\t\t\"--volume\":                                ignoredArgHandler,\n\t\t\t\"--volumes-from\":                          ignoredArgHandler,\n\t\t\t\"--workdir\":                               ignoredArgHandler,\n\t\t\t\"-a\":                                      ignoredArgHandler,\n\t\t\t\"-d\":                                      nil,\n\t\t\t\"-e\":                                      ignoredArgHandler,\n\t\t\t\"-h\":                                      ignoredArgHandler,\n\t\t\t\"-i\":                                      nil,\n\t\t\t\"-l\":                                      ignoredArgHandler,\n\t\t\t\"-m\":                                      ignoredArgHandler,\n\t\t\t\"-p\":                                      ignoredArgHandler,\n\t\t\t\"-q\":                                      nil,\n\t\t\t\"-t\":                                      nil,\n\t\t\t\"-u\":                                      ignoredArgHandler,\n\t\t\t\"-v\":                                      ignoredArgHandler,\n\t\t\t\"-w\":                                      ignoredArgHandler,\n\t\t},\n\t\thasForeignFlags: true,\n\t},\n\n\t\"save\": {\n\t\tcommandPath: \"save\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all-platforms\": nil,\n\t\t\t\"--output\":        ignoredArgHandler,\n\t\t\t\"--platform\":      ignoredArgHandler,\n\t\t\t\"-o\":              ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"start\": {\n\t\tcommandPath: \"start\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--attach\":         nil,\n\t\t\t\"--checkpoint\":     ignoredArgHandler,\n\t\t\t\"--checkpoint-dir\": ignoredArgHandler,\n\t\t\t\"--detach-keys\":    ignoredArgHandler,\n\t\t\t\"--interactive\":    nil,\n\t\t\t\"-a\":               nil,\n\t\t\t\"-i\":               nil,\n\t\t},\n\t},\n\n\t\"stats\": {\n\t\tcommandPath: \"stats\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":       nil,\n\t\t\t\"--format\":    ignoredArgHandler,\n\t\t\t\"--no-stream\": nil,\n\t\t\t\"--no-trunc\":  nil,\n\t\t\t\"-a\":          nil,\n\t\t},\n\t},\n\n\t\"stop\": {\n\t\tcommandPath: \"stop\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--signal\": ignoredArgHandler,\n\t\t\t\"--time\":   ignoredArgHandler,\n\t\t\t\"-s\":       ignoredArgHandler,\n\t\t\t\"-t\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"system\": {\n\t\tcommandPath: \"system\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"events\": {},\n\t\t\t\"info\":   {},\n\t\t\t\"prune\":  {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"system events\": {\n\t\tcommandPath: \"system events\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--filter\": ignoredArgHandler,\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"system info\": {\n\t\tcommandPath: \"system info\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--mode\":   ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"system prune\": {\n\t\tcommandPath: \"system prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":     nil,\n\t\t\t\"--force\":   nil,\n\t\t\t\"--volumes\": nil,\n\t\t\t\"-a\":        nil,\n\t\t\t\"-f\":        nil,\n\t\t},\n\t},\n\n\t\"tag\": {\n\t\tcommandPath: \"tag\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"top\": {\n\t\tcommandPath: \"top\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"unpause\": {\n\t\tcommandPath: \"unpause\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n\n\t\"update\": {\n\t\tcommandPath: \"update\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--blkio-weight\":       ignoredArgHandler,\n\t\t\t\"--cpu-period\":         ignoredArgHandler,\n\t\t\t\"--cpu-quota\":          ignoredArgHandler,\n\t\t\t\"--cpu-shares\":         ignoredArgHandler,\n\t\t\t\"--cpus\":               ignoredArgHandler,\n\t\t\t\"--cpuset-cpus\":        ignoredArgHandler,\n\t\t\t\"--cpuset-mems\":        ignoredArgHandler,\n\t\t\t\"--kernel-memory\":      ignoredArgHandler,\n\t\t\t\"--memory\":             ignoredArgHandler,\n\t\t\t\"--memory-reservation\": ignoredArgHandler,\n\t\t\t\"--memory-swap\":        ignoredArgHandler,\n\t\t\t\"--pids-limit\":         ignoredArgHandler,\n\t\t\t\"--restart\":            ignoredArgHandler,\n\t\t\t\"-m\":                   ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"version\": {\n\t\tcommandPath: \"version\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"volume\": {\n\t\tcommandPath: \"volume\",\n\t\tsubcommands: map[string]struct{}{\n\t\t\t\"create\":  {},\n\t\t\t\"inspect\": {},\n\t\t\t\"ls\":      {},\n\t\t\t\"prune\":   {},\n\t\t\t\"rm\":      {},\n\t\t},\n\t\toptions: map[string]argHandler{},\n\t},\n\n\t\"volume create\": {\n\t\tcommandPath: \"volume create\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--label\": ignoredArgHandler,\n\t\t},\n\t},\n\n\t\"volume inspect\": {\n\t\tcommandPath: \"volume inspect\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--size\":   nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-s\":       nil,\n\t\t},\n\t},\n\n\t\"volume ls\": {\n\t\tcommandPath: \"volume ls\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--filter\": ignoredArgHandler,\n\t\t\t\"--format\": ignoredArgHandler,\n\t\t\t\"--quiet\":  nil,\n\t\t\t\"--size\":   nil,\n\t\t\t\"-f\":       ignoredArgHandler,\n\t\t\t\"-q\":       nil,\n\t\t\t\"-s\":       nil,\n\t\t},\n\t},\n\n\t\"volume prune\": {\n\t\tcommandPath: \"volume prune\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--all\":   nil,\n\t\t\t\"--force\": nil,\n\t\t\t\"-a\":      nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"volume rm\": {\n\t\tcommandPath: \"volume rm\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions: map[string]argHandler{\n\t\t\t\"--force\": nil,\n\t\t\t\"-f\":      nil,\n\t\t},\n\t},\n\n\t\"wait\": {\n\t\tcommandPath: \"wait\",\n\t\tsubcommands: map[string]struct{}{},\n\t\toptions:     map[string]argHandler{},\n\t},\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/parse_args.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n)\n\ntype cleanupFunc func() error\n\n// parsedArgs describes the result of calling parseArgs.\ntype parsedArgs struct {\n\t// Arguments for nerdctl with paths replaced.\n\targs []string\n\t// cleanup functions to call\n\tcleanup []cleanupFunc\n}\n\n// argHandler is the type of a function that handles some argument.\ntype argHandler func(string) (string, []cleanupFunc, error)\n\n// argHandlersType defines the functions passed in command handlers.\ntype argHandlersType struct {\n\tvolumeArgHandler       argHandler\n\tfilePathArgHandler     argHandler\n\toutputPathArgHandler   argHandler\n\tmountArgHandler        argHandler\n\tbuilderCacheArgHandler argHandler\n\tbuildContextArgHandler argHandler\n}\n\n// commandHandlerType is the type of commandDefinition.handler, which is used\n// to handle positional arguments (and special subcommands).\n// The passed-in arguments excludes any flags given after positional arguments.\ntype commandHandlerType func(*commandDefinition, []string, argHandlersType) (*parsedArgs, error)\n\ntype commandDefinition struct {\n\t// commands points to the global command map; if this is null, then the global\n\t// variable named \"commands\" is used instead.\n\tcommands *map[string]commandDefinition\n\t// commandPath is the arguments needed to get to this command.\n\tcommandPath string\n\t// subcommands that can be spawned from this command.\n\tsubcommands map[string]struct{}\n\t// options for this (sub) command.  If the handler is null, the option does\n\t// not take arguments.\n\toptions map[string]argHandler\n\t// if set, this command can include foreign flags that should not be parsed.\n\t// This should be set for things like `nerdctl run` where flags can be passed\n\t// to the command to be run.\n\thasForeignFlags bool\n\t// handler for any positional arguments and subcommands.  This should not\n\t// include the name of the subcommand itself.  If this is not given, all\n\t// subcommands are searched for, and positional arguments are ignored.\n\thandler commandHandlerType\n}\n\n// parseOption takes an argument (that is known to start with `-` or `--`) plus\n// the next argument (which may be needed if a value is required), and returns\n// whether the value argument was consumed, plus any cleanup functions.\nfunc (c *commandDefinition) parseOption(arg, next string) ([]string, bool, []cleanupFunc, error) {\n\tif !strings.HasPrefix(arg, \"-\") {\n\t\tpanic(fmt.Sprintf(\"commandDefinition.parseOption called with invalid arg %q\", arg))\n\t}\n\n\t// Figure out what the option name is\n\toption := arg\n\tvalue := next\n\tconsumed := true\n\tsep := strings.Index(option, \"=\")\n\tif sep >= 0 {\n\t\tvalue = option[sep+1:]\n\t\toption = option[:sep]\n\t\tconsumed = false\n\t}\n\thandler, ok := c.options[option]\n\tif !ok {\n\t\t// There may be multiple single-character options bunched together, e.g. `-itp 80`.\n\t\tif len(option) > 1 && option[0] == '-' && option[1] != '-' {\n\t\t\t// Make sure all options (except the last) exist and take no arguments.\n\t\t\tfor _, ch := range option[1 : len(option)-1] {\n\t\t\t\thandler, ok = c.options[fmt.Sprintf(\"-%c\", ch)]\n\t\t\t\tif !ok || handler != nil {\n\t\t\t\t\tok = false\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\t// If all earlier options are fine, use the arg handler for the last option.\n\t\t\tif ok {\n\t\t\t\thandler, ok = c.options[fmt.Sprintf(\"-%s\", option[len(option)-1:])]\n\t\t\t}\n\t\t}\n\t\tif !ok {\n\t\t\t// The user may say `-foo` instead of `--foo`\n\t\t\toption = \"-\" + option\n\t\t\thandler, ok = c.options[option]\n\t\t}\n\t}\n\tif ok {\n\t\tif handler == nil {\n\t\t\t// This does not consume a value, and therefore doesn't need munging\n\t\t\treturn []string{arg}, false, nil, nil\n\t\t}\n\t\tconverted, cleanups, err := handler(value)\n\t\tif err != nil {\n\t\t\t// Note that we still need to pass along any cleanups even on failure\n\t\t\treturn nil, consumed, cleanups, err\n\t\t}\n\t\treturn []string{option, converted}, consumed, cleanups, nil\n\t}\n\n\t// Check if we can resolve this with the parent command.\n\tvar extraCleanups []cleanupFunc\n\tparentName := \"\"\n\tif lastSpace := strings.LastIndex(c.commandPath, \" \"); lastSpace > -1 {\n\t\tparentName = c.commandPath[:lastSpace]\n\t}\n\tif parentName != c.commandPath {\n\t\tglobalCommands := c.commands\n\t\tif globalCommands == nil {\n\t\t\tglobalCommands = &commands\n\t\t}\n\t\tparent, ok := (*globalCommands)[parentName]\n\t\tif !ok {\n\t\t\tpanic(fmt.Sprintf(\"command %q could not find parent %q\", c.commandPath, parentName))\n\t\t}\n\t\tparentResult, parentConsumed, parentCleanups, parentErr := parent.parseOption(arg, next)\n\t\tif parentErr == nil {\n\t\t\treturn parentResult, parentConsumed, parentCleanups, nil\n\t\t}\n\t\textraCleanups = parentCleanups\n\t}\n\treturn nil, false, extraCleanups, fmt.Errorf(\"command %q does not support option %s\", c.commandPath, arg)\n}\n\n// parse arguments for this command; this includes options (--long, -x) as well\n// as subcommands and positional arguments.\nfunc (c commandDefinition) parse(args []string) (*parsedArgs, error) {\n\t// Parsing rules:\n\t// - At each command level, short options (-x) at that level can be parsed.\n\t// - At each command level, long options from the current or any previous level can be parsed.\n\t// - If a command contains positional arguments, it may not contain any subcommands.\n\t//   (We check this in `./generate` to make sure this stays true.)\n\t// - Positional arguments can be intermixed with (both long and short) options.\n\t// - If a command can have foreign flags (e.g. `nerdctl run`), we stop parsing\n\t//   options on first positional argument.  This means we parse the flag in\n\t//   `nerdctl run --env foo=bar image sh -c ...` but not the `--env` flag in\n\t//   `nerdctl run image --env foo=bar sh -c ...`.\n\t// - Having foreign flags is mutually exclusive with having subcommands; this\n\t//   is also checked in `./generate`.\n\t// - `--` stops parsing of options.\n\tvar result parsedArgs\n\tvar positionalArgs []string\n\tfor argIndex := 0; argIndex < len(args); argIndex++ {\n\t\targ := args[argIndex]\n\t\tif arg == \"--\" {\n\t\t\t// No more options, only positional arguments.\n\t\t\tpositionalArgs = append(positionalArgs, args[argIndex+1:]...)\n\t\t\tbreak\n\t\t} else if strings.HasPrefix(arg, \"-\") {\n\t\t\tnext := \"\"\n\t\t\tif argIndex+1 < len(args) {\n\t\t\t\tnext = args[argIndex+1]\n\t\t\t}\n\t\t\tnewArgs, consumed, cleanups, err := c.parseOption(arg, next)\n\t\t\tif err != nil {\n\t\t\t\t// We need to run any cleanups we have so far\n\t\t\t\tfor _, cleanup := range append(cleanups, result.cleanup...) {\n\t\t\t\t\tcleanupErr := cleanup()\n\t\t\t\t\tif cleanupErr != nil {\n\t\t\t\t\t\tlog.Printf(\"Error running cleanup: %s\", cleanupErr)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tresult.args = append(result.args, newArgs...)\n\t\t\tresult.cleanup = append(result.cleanup, cleanups...)\n\t\t\tif consumed {\n\t\t\t\targIndex++\n\t\t\t}\n\t\t} else if len(c.subcommands) > 0 {\n\t\t\t// This command has subcommands; assume any non-flags are subcommands.\n\t\t\t// Hand off argument parsing to the subcommand.\n\t\t\tsubcommandPath := c.commandPath\n\t\t\tif subcommandPath != \"\" {\n\t\t\t\tsubcommandPath += \" \"\n\t\t\t}\n\t\t\tsubcommandPath += arg\n\t\t\tglobalCommands := c.commands\n\t\t\tif globalCommands == nil {\n\t\t\t\tglobalCommands = &commands\n\t\t\t}\n\t\t\tif subcommand, ok := (*globalCommands)[subcommandPath]; ok {\n\t\t\t\tchildResult, err := subcommand.parse(args[argIndex+1:])\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tresult.args = append(result.args, arg)\n\t\t\t\tresult.args = append(result.args, childResult.args...)\n\t\t\t\tresult.cleanup = append(result.cleanup, childResult.cleanup...)\n\t\t\t} else {\n\t\t\t\t// Invalid subcommand; ignore positional arguments.\n\t\t\t\tresult.args = append(result.args, args[argIndex:]...)\n\t\t\t}\n\t\t\tbreak\n\t\t} else {\n\t\t\tif c.hasForeignFlags {\n\t\t\t\t// If we have foreign flags, assume the rest of the arguments starting\n\t\t\t\t// from the first positional argument is foreign.\n\t\t\t\tpositionalArgs = append(positionalArgs, args[argIndex:]...)\n\t\t\t\tbreak\n\t\t\t} else {\n\t\t\t\t// This command doesn't have subcommands, nor foreign arguments.\n\t\t\t\t// Everything is positional arguments; we still have to parse other\n\t\t\t\t// arguments for flags, though.\n\t\t\t\tpositionalArgs = append(positionalArgs, arg)\n\t\t\t}\n\t\t}\n\t}\n\t// At this point, `result` is filled with options, and `positionalArgs`\n\t// contains the unparsed positional arguments.\n\tif c.handler != nil {\n\t\tchildResult, err := c.handler(&c, positionalArgs, argHandlers)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tresult.args = append(result.args, childResult.args...)\n\t\tresult.cleanup = append(result.cleanup, childResult.cleanup...)\n\t} else {\n\t\tif len(positionalArgs) > 0 && !slices.Contains(result.args, \"--\") {\n\t\t\tresult.args = slices.Concat(result.args, []string{\"--\"}, positionalArgs)\n\t\t} else {\n\t\t\tresult.args = append(result.args, positionalArgs...)\n\t\t}\n\t}\n\treturn &result, nil\n}\n\n// parseArgs parses the process arguments (os.Args) and returns them with any\n// strings referring to paths replaced with replacements that will work with\n// nerdctl (i.e. inside the correct WSL container).\nfunc parseArgs() (*parsedArgs, error) {\n\terr := prepareParseArgs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresult, err := commands[\"\"].parse(os.Args[1:])\n\tif err != nil {\n\t\t_ = cleanupParseArgs()\n\t\treturn nil, err\n\t}\n\treturn result, nil\n}\n\n// ignoredArgHandler handles arguments that do not contain paths.\nfunc ignoredArgHandler(input string) (string, []cleanupFunc, error) {\n\treturn input, nil, nil\n}\n\n// registerArgHandler sets option handlers.  This should be called from init()\n// to set up any option handlers that need to handle paths.\nfunc registerArgHandler(command, option string, handler argHandler) {\n\t// Do some extra checking to guard against typos.\n\tif _, ok := commands[command]; !ok {\n\t\tpanic(fmt.Sprintf(\"unknown command %q\", command))\n\t}\n\tif _, ok := commands[command].options[option]; !ok {\n\t\tpanic(fmt.Sprintf(\"command %q does not have option %q\", command, option))\n\t}\n\tcommands[command].options[option] = handler\n}\n\n// registerCommandHandler sets handlers for positional arguments.  This should\n// be called from init().\nfunc registerCommandHandler(command string, handler commandHandlerType) {\n\t// Do some extra checking to guard against typos.\n\tif _, ok := commands[command]; !ok {\n\t\tpanic(fmt.Sprintf(\"unknown command %q\", command))\n\t}\n\tc := commands[command]\n\tc.handler = handler\n\tcommands[command] = c\n}\n\n// aliasCommand sets up an alias to a different command.  Both the alias and the\n// target command must already exist and have the same options / subcommands (as\n// it should already be an alias).  This is normally not needed for the help\n// commands, as they do not take any arguments.\nfunc aliasCommand(alias, target string) {\n\taliasCommand, ok := commands[alias]\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"unknown alias command %q\", alias))\n\t}\n\ttargetCommand, ok := commands[target]\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"unknown target command %q\", target))\n\t}\n\n\t// Try harder to check that the commands look similar\n\tif len(aliasCommand.subcommands) != len(targetCommand.subcommands) {\n\t\tpanic(fmt.Sprintf(\"cannot alias %q to %q: different subcommands\", alias, target))\n\t}\n\tfor subcommand := range aliasCommand.subcommands {\n\t\tif _, ok := targetCommand.subcommands[subcommand]; !ok {\n\t\t\tpanic(fmt.Sprintf(\"cannot alias %q to %q: missing subcommand %q\", alias, target, subcommand))\n\t\t}\n\t}\n\tvar aliasOnlyOptions []string\n\tvar targetOnlyOptions []string\n\tfor opt := range aliasCommand.options {\n\t\tif _, ok := targetCommand.options[opt]; !ok {\n\t\t\taliasOnlyOptions = append(aliasOnlyOptions, opt)\n\t\t}\n\t}\n\tif len(aliasOnlyOptions) > 0 {\n\t\tpanic(fmt.Sprintf(\"cannot alias %q to %q: alias-only options %s\", alias, target, aliasOnlyOptions))\n\t}\n\tfor opt := range targetCommand.options {\n\t\tif _, ok := aliasCommand.options[opt]; !ok {\n\t\t\ttargetOnlyOptions = append(targetOnlyOptions, opt)\n\t\t}\n\t}\n\tif len(targetOnlyOptions) > 0 {\n\t\tpanic(fmt.Sprintf(\"cannot alias %q to %q: target-only options %s\", alias, target, targetOnlyOptions))\n\t}\n\n\tcommands[alias] = commands[target]\n}\n\nfunc init() {\n\t// Set up the argument handlers\n\tregisterArgHandler(\"builder build\", \"--build-context\", argHandlers.buildContextArgHandler)\n\tregisterArgHandler(\"builder build\", \"--cache-from\", argHandlers.builderCacheArgHandler)\n\tregisterArgHandler(\"builder build\", \"--cache-to\", argHandlers.builderCacheArgHandler)\n\tregisterArgHandler(\"builder build\", \"--file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"builder build\", \"-f\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"builder build\", \"--iidfile\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"builder debug\", \"--file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"builder debug\", \"-f\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"checkpoint create\", \"--checkpoint-dir\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"checkpoint ls\", \"--checkpoint-dir\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"checkpoint rm\", \"--checkpoint-dir\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"compose\", \"--file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"compose\", \"-f\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"compose\", \"--project-directory\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"compose\", \"--env-file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"compose run\", \"--volume\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"compose run\", \"-v\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"container create\", \"--cidfile\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container create\", \"--cosign-key\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container create\", \"--env-file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container create\", \"--label-file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container create\", \"--mount\", argHandlers.mountArgHandler)\n\tregisterArgHandler(\"container create\", \"--pidfile\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container create\", \"--volume\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"container create\", \"-v\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"container export\", \"--output\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container export\", \"-o\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container run\", \"--cidfile\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container run\", \"--cosign-key\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container run\", \"--env-file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container run\", \"--label-file\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"container run\", \"--mount\", argHandlers.mountArgHandler)\n\tregisterArgHandler(\"container run\", \"--pidfile\", argHandlers.outputPathArgHandler)\n\tregisterArgHandler(\"container run\", \"--volume\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"container run\", \"-v\", argHandlers.volumeArgHandler)\n\tregisterArgHandler(\"container start\", \"--checkpoint-dir\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"image convert\", \"--estargz-record-in\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"image load\", \"--input\", argHandlers.filePathArgHandler)\n\tregisterArgHandler(\"image save\", \"--output\", argHandlers.outputPathArgHandler)\n\n\t// Set up command handlers\n\tregisterCommandHandler(\"builder build\", builderBuildHandler)\n\tregisterCommandHandler(\"container cp\", containerCopyHandler)\n\tregisterCommandHandler(\"image import\", imageImportHandler)\n\n\t// Set up aliases\n\taliasCommand(\"commit\", \"container commit\")\n\taliasCommand(\"cp\", \"container cp\")\n\taliasCommand(\"create\", \"container create\")\n\taliasCommand(\"exec\", \"container exec\")\n\taliasCommand(\"export\", \"container export\")\n\taliasCommand(\"kill\", \"container kill\")\n\taliasCommand(\"image build\", \"builder build\")\n\taliasCommand(\"import\", \"image import\")\n\taliasCommand(\"logs\", \"container logs\")\n\taliasCommand(\"pause\", \"container pause\")\n\taliasCommand(\"port\", \"container port\")\n\taliasCommand(\"rename\", \"container rename\")\n\taliasCommand(\"rm\", \"container rm\")\n\taliasCommand(\"run\", \"container run\")\n\taliasCommand(\"start\", \"container start\")\n\taliasCommand(\"stop\", \"container stop\")\n\taliasCommand(\"unpause\", \"container unpause\")\n\taliasCommand(\"wait\", \"container wait\")\n\taliasCommand(\"build\", \"builder build\")\n\taliasCommand(\"load\", \"image load\")\n\taliasCommand(\"pull\", \"image pull\")\n\taliasCommand(\"push\", \"image push\")\n\taliasCommand(\"save\", \"image save\")\n\taliasCommand(\"tag\", \"image tag\")\n\n\tdescribeCommands()\n}\n"
  },
  {
    "path": "src/go/nerdctl-stub/parse_args_test.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar errExpected = fmt.Errorf(\"expected error\")\n\nfunc generateCleanupFunc(output *bool, withError bool) cleanupFunc {\n\treturn func() error {\n\t\tif output != nil {\n\t\t\t*output = true\n\t\t}\n\t\tif withError {\n\t\t\treturn errExpected\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc generateOptionHandler(output *bool, argError, cleanupError bool) argHandler {\n\tcleanup := generateCleanupFunc(output, cleanupError)\n\treturn func(arg string) (string, []cleanupFunc, error) {\n\t\tif argError {\n\t\t\treturn \"\", []cleanupFunc{cleanup}, errExpected\n\t\t}\n\t\treturn arg, []cleanupFunc{cleanup}, nil\n\t}\n}\n\nfunc TestParseOptions(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"unsupported option\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{}\n\t\t_, _, _, err := c.parseOption(\"-hello\", \"world\")\n\t\tassert.EqualError(t, err, `command \"\" does not support option -hello`)\n\t})\n\tt.Run(\"option with no value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--hello\": nil}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"--hello\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--hello\"}, args)\n\t\t\tassert.False(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"option with value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--hello\": ignoredArgHandler}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"--hello\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--hello\", \"world\"}, args)\n\t\t\tassert.True(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"option with embedded value\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--hello\": ignoredArgHandler}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"--hello=moo\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--hello\", \"moo\"}, args)\n\t\t\tassert.False(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"option with short name\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--hello\": nil}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"-hello\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"-hello\"}, args)\n\t\t\tassert.False(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"option with bunched up single-letter options\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--ab\": ignoredArgHandler, \"-a\": nil, \"-b\": nil}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"-ab\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"-ab\"}, args)\n\t\t\tassert.False(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"option with bunched up single-letter options, with argument\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--ab\": nil, \"-a\": nil, \"-b\": ignoredArgHandler}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"-ab\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"-ab\", \"world\"}, args)\n\t\t\tassert.True(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"short option, not all characters are single-letter options\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--abc\": nil, \"-a\": nil, \"-c\": ignoredArgHandler}}\n\t\targs, consumed, cleanup, err := c.parseOption(\"-abc\", \"world\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"-abc\"}, args)\n\t\t\tassert.False(t, consumed)\n\t\t\tassert.Nil(t, cleanup)\n\t\t}\n\t})\n\tt.Run(\"passes along any cleanups on failure\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"--hello\": generateOptionHandler(nil, true, true),\n\t\t\t},\n\t\t}\n\t\t_, _, cleanups, err := c.parseOption(\"--hello\", \"world\")\n\t\tassert.Error(t, err)\n\t\tif assert.Len(t, cleanups, 1) {\n\t\t\tresult := cleanups[0]()\n\t\t\tassert.Same(t, errExpected, result)\n\t\t}\n\t})\n\tt.Run(\"looks for options in parent commands\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tlocalCommands := make(map[string]commandDefinition)\n\t\tlocalCommands[\"\"] = commandDefinition{\n\t\t\tcommands:    &localCommands,\n\t\t\tcommandPath: \"\",\n\t\t\toptions:     map[string]argHandler{\"--hello\": nil},\n\t\t}\n\t\tlocalCommands[\"subcommand\"] = commandDefinition{\n\t\t\tcommands:    &localCommands,\n\t\t\tcommandPath: \"subcommand\",\n\t\t\toptions:     map[string]argHandler{\"--world\": nil},\n\t\t}\n\t\tlocalCommands[\"subcommand more\"] = commandDefinition{\n\t\t\tcommands:    &localCommands,\n\t\t\tcommandPath: \"subcommand more\",\n\t\t\toptions:     map[string]argHandler{\"--foo\": nil},\n\t\t}\n\t\tcommand := localCommands[\"subcommand more\"]\n\t\targs, _, _, err := command.parseOption(\"--hello\", \"\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--hello\"}, args)\n\t\t}\n\n\t\targs, _, _, err = command.parseOption(\"--world\", \"\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--world\"}, args)\n\t\t}\n\n\t\targs, _, _, err = command.parseOption(\"--foo\", \"\")\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"--foo\"}, args)\n\t\t}\n\t})\n}\n\nfunc TestParse(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"options\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{options: map[string]argHandler{\"--option\": nil}}\n\t\tresult, err := c.parse([]string{\"--option\"})\n\t\tif assert.NoError(t, err) {\n\t\t\texpected := &parsedArgs{args: []string{\"--option\"}}\n\t\t\tassert.Equal(t, expected, result)\n\t\t}\n\t})\n\tt.Run(\"options with parse error\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcleanupRun := false\n\t\tc := commandDefinition{\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"-o\": generateOptionHandler(&cleanupRun, true, false),\n\t\t\t},\n\t\t}\n\t\t_, err := c.parse([]string{\"-o=xxx\"})\n\t\tassert.Error(t, err)\n\t\tassert.True(t, cleanupRun)\n\t})\n\tt.Run(\"positional argument handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trun := false\n\t\tc := commandDefinition{\n\t\t\thandler: func(c *commandDefinition, args []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t\t\t\trun = true\n\t\t\t\tassert.Equal(t, []string{\"positional\", \"arguments\"}, args)\n\t\t\t\treturn &parsedArgs{}, nil\n\t\t\t},\n\t\t}\n\t\t_, err := c.parse([]string{\"positional\", \"arguments\"})\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, run)\n\t})\n\tt.Run(\"positional args without handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tc := commandDefinition{}\n\t\tresult, err := c.parse([]string{\"hello\", \"world\"})\n\t\tassert.NoError(t, err)\n\t\tif assert.NotNil(t, result) {\n\t\t\tassert.Equal(t, []string{\"--\", \"hello\", \"world\"}, result.args)\n\t\t}\n\t})\n\tt.Run(\"subcommand handler\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\trun := false\n\t\tlocalCommands := make(map[string]commandDefinition)\n\t\tlocalCommands[\"\"] = commandDefinition{\n\t\t\tcommands: &localCommands,\n\t\t\tsubcommands: map[string]struct{}{\n\t\t\t\t\"subcommand\": {},\n\t\t\t},\n\t\t}\n\t\tlocalCommands[\"subcommand\"] = commandDefinition{\n\t\t\tcommands: &localCommands,\n\t\t\thandler: func(c *commandDefinition, args []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t\t\t\trun = true\n\t\t\t\tassert.Equal(t, []string{\"a\", \"b\"}, args)\n\t\t\t\treturn &parsedArgs{}, nil\n\t\t\t},\n\t\t}\n\t\t_, err := localCommands[\"\"].parse([]string{\"subcommand\", \"a\", \"b\"})\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, run)\n\t})\n\tt.Run(\"subcommand with mixed arguments\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar seenArgs []string\n\t\tlocalCommands := make(map[string]commandDefinition)\n\t\tlocalCommands[\"\"] = commandDefinition{\n\t\t\tcommands: &localCommands,\n\t\t\tsubcommands: map[string]struct{}{\n\t\t\t\t\"subcommand\": {},\n\t\t\t},\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"--foo\": ignoredArgHandler,\n\t\t\t},\n\t\t}\n\t\tlocalCommands[\"subcommand\"] = commandDefinition{\n\t\t\tcommandPath: \"subcommand\",\n\t\t\tcommands:    &localCommands,\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"--bar\": ignoredArgHandler,\n\t\t\t},\n\t\t\thandler: func(cd *commandDefinition, s []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t\t\t\tseenArgs = s\n\t\t\t\treturn &parsedArgs{}, nil\n\t\t\t},\n\t\t}\n\t\tresult, err := localCommands[\"\"].parse([]string{\"subcommand\", \"--foo\", \"FOO\", \"qq\", \"--bar\", \"BAR\", \"zz\"})\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"qq\", \"zz\"}, seenArgs)\n\t\t\t// Because we have a custom handler, they don't show up in result.args\n\t\t\tassert.Equal(t, []string{\"subcommand\", \"--foo\", \"FOO\", \"--bar\", \"BAR\"}, result.args)\n\t\t}\n\t})\n\tt.Run(\"subcommand with foreign flags\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tvar seenArgs []string\n\t\tlocalCommands := make(map[string]commandDefinition)\n\t\tlocalCommands[\"\"] = commandDefinition{\n\t\t\tcommands: &localCommands,\n\t\t\tsubcommands: map[string]struct{}{\n\t\t\t\t\"subcommand\": {},\n\t\t\t},\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"--foo\": ignoredArgHandler,\n\t\t\t},\n\t\t}\n\t\tlocalCommands[\"subcommand\"] = commandDefinition{\n\t\t\tcommandPath: \"subcommand\",\n\t\t\tcommands:    &localCommands,\n\t\t\toptions: map[string]argHandler{\n\t\t\t\t\"--bar\": ignoredArgHandler,\n\t\t\t},\n\t\t\thasForeignFlags: true,\n\t\t\thandler: func(cd *commandDefinition, s []string, argHandlers argHandlersType) (*parsedArgs, error) {\n\t\t\t\tseenArgs = s\n\t\t\t\treturn &parsedArgs{}, nil\n\t\t\t},\n\t\t}\n\t\tresult, err := localCommands[\"\"].parse([]string{\"subcommand\", \"--foo\", \"FOO\", \"qq\", \"--bar\", \"BAR\", \"zz\"})\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, []string{\"qq\", \"--bar\", \"BAR\", \"zz\"}, seenArgs)\n\t\t\t// Because we have a custom handler, they don't show up in result.args\n\t\t\tassert.Equal(t, []string{\"subcommand\", \"--foo\", \"FOO\"}, result.args)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/networking/.github/workflows/go.yaml",
    "content": "name: Go\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n      with:\n        persist-credentials: false\n\n    - name: Set up Go\n      uses: actions/setup-go@v3\n      with:\n        go-version-file: go.mod\n        cache: true\n\n    - name: Build\n      run: make build\n\n    - name: Run golangci-lint [linux]\n      uses: golangci/golangci-lint-action@v3.1.0\n      with:\n        args: --verbose --timeout 3m\n        # Disable pkg & build cache flags; the manual build step fills those in,\n        # so repopulating the cache just shows a pile of errors.\n        skip-pkg-cache: true\n        skip-build-cache: true\n      env:\n        GOOS: \"linux\"\n\n    - name: Run golangci-lint [windows]\n      uses: golangci/golangci-lint-action@v3.1.0\n      with:\n        args: --verbose --timeout 3m\n        # Disable pkg & build cache flags; the manual build step fills those in,\n        # so repopulating the cache just shows a pile of errors.\n        skip-pkg-cache: true\n        skip-build-cache: true\n      env:\n        GOOS: \"windows\"\n"
  },
  {
    "path": "src/go/networking/.github/workflows/release.yaml",
    "content": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*\"\n      - \"test-v*\"\n\npermissions:\n  contents: read\n\nenv:\n  TAR_FILE: rancher-desktop-networking-${{ github.ref_name }}.tar.gz\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n        with:\n          persist-credentials: false\n          fetch-depth: 0\n      - uses: actions/setup-go@v3\n        with:\n          go-version-file: go.mod\n      - name: create build artifacts\n        run: make build\n        env:\n          CGO_ENABLED: \"0\"\n      - name: create tarball\n        run: |\n          tar czf -C ./bin $TAR_FILE .\n          md5sum $TAR_FILE > $TAR_FILE.md5\n      - uses: actions/upload-artifact@v3\n        with:\n          name: rancher-desktop-networking.tar.gz\n          path: rancher-desktop-networking-${{ github.ref_name }}.tar.gz\n          if-no-files-found: error\n      - uses: actions/upload-artifact@v3\n        with:\n          name: rancher-desktop-networking.tar.gz.md5\n          path: rancher-desktop-networking-${{ github.ref_name }}.tar.gz.md5\n          if-no-files-found: error\n  release:\n    needs: build\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - name: Download tar artifact\n        uses: actions/download-artifact@v3\n        with:\n          name: rancher-desktop-networking.tar.gz\n      - name: Download tar MD5 artifact\n        uses: actions/download-artifact@v3\n        with:\n          name: rancher-desktop-networking.tar.gz.md5\n      - name: Create release\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        run: >-\n          gh release create\n          \"${{ github.ref_name }}\"\n          $TAR_FILE\n          $TAR_FILE.md5\n          --draft\n          --title \"${{ github.ref_name }}\"\n          --repo ${{ github.repository }}\n"
  },
  {
    "path": "src/go/networking/.gitignore",
    "content": "/bin/\ncapture.pcap\ntmp/\n*.exe\n"
  },
  {
    "path": "src/go/networking/LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License."
  },
  {
    "path": "src/go/networking/Makefile",
    "content": "LDFLAGS = -ldflags '-s -w'\n\n.PHONY: build\nbuild: host-switch vm-switch network-setup wsl-proxy\n\nbin/host-switch.exe:\n\tGOOS=windows go build $(LDFLAGS) -o $@ ./cmd/host\n\n.PHONY: host-switch\nhost-switch: bin/host-switch.exe\n\nbin/vm-switch:\n\tGOOS=linux go build $(LDFLAGS) -o $@ ./cmd/vm\n\n.PHONY: vm-switch\nvm-switch: bin/vm-switch\n\nbin/network-setup:\n\tGOOS=linux go build $(LDFLAGS) -o $@ ./cmd/network\n\n.PHONY: network-setup\nnetwork-setup: bin/network-setup\n\nbin/wsl-proxy:\n\tGOOS=linux go build $(LDFLAGS) -o $@ ./cmd/proxy\n\n.PHONY: wsl-proxy\nwsl-proxy: bin/wsl-proxy\n\n.PHONY: fmt\nfmt:\n\tgofmt -l -s -w .\n\n.PHONY: clean\nclean:\n\trm -rf ./bin\n\n.PHONY: vendor\nvendor:\n\tgo mod tidy\n\tgo mod vendor\n"
  },
  {
    "path": "src/go/networking/README.md",
    "content": "# rancher-desktop-networking\n`rancher-desktop-networking` is a simple layer 2 switch that allows connectivity between the VM and the host over `AF_VSOCK` written in Go using [gvisor's](https://github.com/google/gvisor) network stack and [gvisor-tap-vsock](https://github.com/google/gvisor). In addition to piping the ethernet frames, it also provides DNS, DHCP services and dynamic port forwarding.\n\nRancher Desktop Networking consists of three main components: host-switch, vm-switch, and network-setup. Below is a brief explanation of what each component does.\n\n## How it works\n```mermaid\n%% diamonds are main processes such as vm-switch and host-switch\n%% circles are os syscalls\n%% squares are general processes and utilities\nflowchart  LR;\n subgraph Host[\"HOST\"]\n subgraph hostSwitch[\"host-switch.exe\"]\n vsockHost{\"main loop\"}\n eth((\"reconstruct ETH frames\"))\n syscall((\"OS syscall\"))\n dhcp[\"DHCP\"]\n dns[\"DNS\"]\n api[\"API\"]\nportForwarding[\"Port Forwarding\"]\n vsockHost <----> eth\n eth <----> syscall\n vsockHost ----> dhcp\n vsockHost ----> dns\n vsockHost ----> api\n vsockHost ----> portForwarding\n end\n end\n subgraph VM[\"VM\"]\n subgraph netNs[\"Isolated Network Namespace\"]\n tapDevice(\"eth0\")\n subgraph vmSwitch[\"vm-switch\"]\n vsockVM{\"VM Switch\"}\n ethVM((\"listens for ETH frames\\n from TAP Device\"))\n ethVM <----> vsockVM\n end\n tapDevice <----> ethVM\n init[\"/sbin/init\"]\n end\n network-setup[\"/usr/local/bin/network-setup\"]\n end\n vsockVM  <---> |AF_VSOCK| vsockHost\n\n```\n\n## host-switch\n`host-switch` runs on the Windows host and acts as a receiver for all traffic originating from the network namespace within the WSL VM. It performs a handshake to find the right VM to talk to over `AF_VSOCK`. Once a correct VM is found, it then listens for the incoming traffic from that VM. In addition to this, it can provide a DNS resolver that runs in the user space network along with an API that allows for dynamic port forwarding.\n\n## network-setup\nIts main responsibility is to respond to the handshake request from the `host-switch.exe`, create a network namespace and start the `vm-switch` subprocess in the newly created network namespace. In addition, it also calls unshare with provided arguments through `--unshare-args`. Below is a sequence diagram demonstrating the process. The process also establishes a Virtual Ethernet pair consisting of two endpoints: `veth-rd-wsl` and `veth-rd-ns`. `veth-rd-wsl` resides within the WSL's default namespace and is configured to listen on the IP address `192.168.143.2`. Conversely, `veth-rd-ns` is located within a network namespace and is assigned the IP address `192.168.143.1`.\n\n## vm-switch\nOnce the `vm-switch` process starts in the new namespace, it creates a tap (`eth0`) and a `lo` device. The Kernel then forwards all the raw Ethernet frames to the tap device. The tap device forwards the frames over [vsock](https://wiki.qemu.org/Features/VirtioVsock) to the host. The process on the host (`host-switch.exe`) decapsulates the frames, and since it maintains both internal (`vm-switch` to `host-switch.exe`) and external (`host-switch.exe` to the internet) connections, it connects to the external endpoints via normal syscalls.\n\n```mermaid\nsequenceDiagram\n    participant wsl-init (pid n)\n    participant network-setup\n    participant vm-switch\n    participant wsl-init (pid 1)\n    participant host-switch.exe\n\n    Note over wsl-init (pid n),wsl-init (pid 1): WSL distro (Network Namespace)\n    Note over host-switch.exe: windows host\n\n    wsl-init (pid n)->>network-setup: spawn\n\n    host-switch.exe->>network-setup: handshake request\n    network-setup->>host-switch.exe: handshake response\n    host-switch.exe->>network-setup: vsock listener ready\n\n    network-setup->>network-setup: open vsock\n    network-setup->>network-setup: create namespace\n\n    network-setup->>vm-switch: spawn\n    Note over network-setup,vm-switch: spawn in network namespace, pass in vsock fd\n\n    network-setup->>wsl-init (pid 1): spawn\n    Note over network-setup,wsl-init (pid 1): spawns in netns, new mnt/pid ns\n\n    vm-switch->>vm-switch: create lo/eth0\n    vm-switch->>vm-switch: DHCP eth0\n    vm-switch->>vm-switch: listen for connections\n    wsl-init (pid 1)-->>wsl-init (pid 1): Spawn /sbin/init\n\n    vm-switch->>host-switch.exe: forward ethernet\n```\n## wsl-proxy\n\nIts primary function comes into play when WSL integration is activated alongside the network tunnel. It runs in the default network namespace and establishes a Unix socket listener for the guest agent process to connect to from inside the network namespace. The guest agent forwards the published ports from various APIs (docker, containerd, and K8s) over the Unix socket to the WSL-proxy. Once the WSL-proxy receives this information, it establishes a listener on the localhost bound to that port. When traffic is received on that listener, it then pipes the traffic to the bridge interface that connects the default namespace to the namespaced network, which forwards the traffic to the namespace and back.\n"
  },
  {
    "path": "src/go/networking/cmd/host/config_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"net\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/config\"\n)\n\nconst (\n\tcaptureFile    = \"capture.pcap\"\n\tlocalHost      = \"127.0.0.1\"\n\tdefaultMTU     = 1500\n\tgatewayMacAddr = \"5a:94:ef:e4:0c:dd\"\n)\n\ntype arrayFlags []string\n\nfunc (i *arrayFlags) String() string {\n\treturn \"Array Flags\"\n}\n\nfunc (i *arrayFlags) Set(value string) error {\n\t*i = append(*i, value)\n\treturn nil\n}\n\nfunc newConfig(subnet config.Subnet, staticPortForwarding map[string]string, debug bool) types.Configuration {\n\tc := types.Configuration{\n\t\tDebug:             debug,\n\t\tMTU:               defaultMTU,\n\t\tSubnet:            subnet.SubnetCIDR,\n\t\tGatewayIP:         subnet.GatewayIP,\n\t\tGatewayMacAddress: gatewayMacAddr,\n\t\tDHCPStaticLeases:  subnet.StaticDHCPLease,\n\t\tDNS: []types.Zone{\n\t\t\t{\n\t\t\t\tName: \"rancher-desktop.internal.\",\n\t\t\t\tRecords: []types.Record{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"gateway\",\n\t\t\t\t\t\tIP:   net.ParseIP(subnet.GatewayIP),\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"host\",\n\t\t\t\t\t\tIP:   net.ParseIP(subnet.StaticDNSHost),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"docker.internal.\",\n\t\t\t\tRecords: []types.Record{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"gateway\",\n\t\t\t\t\t\tIP:   net.ParseIP(subnet.GatewayIP),\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"host\",\n\t\t\t\t\t\tIP:   net.ParseIP(subnet.StaticDNSHost),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tDNSSearchDomains: config.SearchDomains(),\n\t\tForwards:         staticPortForwarding,\n\t\tNAT: map[string]string{\n\t\t\tsubnet.StaticDNSHost: localHost,\n\t\t},\n\t\tGatewayVirtualIPs: []string{subnet.StaticDNSHost},\n\t}\n\tif debug {\n\t\tc.CaptureFile = captureFile\n\t}\n\treturn c\n}\n"
  },
  {
    "path": "src/go/networking/cmd/host/switch_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\t\"github.com/containers/gvisor-tap-vsock/pkg/virtualnetwork\"\n\t\"github.com/dustin/go-humanize\"\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/vsock\"\n)\n\nvar (\n\tdebug             bool\n\tvirtualSubnet     string\n\tstaticPortForward arrayFlags\n)\n\nconst (\n\tvsockListenPort    = 6656\n\tvsockHandshakePort = 6669\n\ttimeoutSeconds     = 5 * 60\n)\n\nfunc main() {\n\tflag.BoolVar(&debug, \"debug\", false, \"enable additional debugging\")\n\tflag.StringVar(&virtualSubnet, \"subnet\", config.DefaultSubnet,\n\t\tfmt.Sprintf(\"Subnet range with CIDR suffix for virtual network, e,g: %s\", config.DefaultSubnet))\n\tflag.Var(&staticPortForward, \"port-forward\",\n\t\t\"List of ports that needs to be pre forwarded to the WSL VM in Host:Port=Guest:Port format e.g: 127.0.0.1:2222=192.168.127.2:22\")\n\tflag.Parse()\n\n\tif debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\n\tsubnet, err := config.ValidateSubnet(virtualSubnet)\n\tif err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n\n\tlogrus.Debugf(\"attempting to start with the following subnet: %+v\", subnet)\n\n\tportForwarding, err := config.ParsePortForwarding(staticPortForward)\n\tif err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n\n\tif err := runSwitch(*subnet, portForwarding); err != nil {\n\t\tlogrus.Error(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc runSwitch(subnet config.Subnet, portForwarding map[string]string) error {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tgroupErrs, ctx := errgroup.WithContext(ctx)\n\n\t// catch user issued signals\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)\n\n\tcfg := newConfig(subnet, portForwarding, debug)\n\n\tln, err := vsockHandshake(ctx, vsockHandshakePort, vsock.SignaturePhrase)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"handshake with peer process failed: %w\", err)\n\t}\n\n\tlogrus.Debugf(\"attempting to start a virtual network with the following config: %+v\", cfg)\n\tgroupErrs.Go(func() error {\n\t\treturn run(ctx, groupErrs, &cfg, ln)\n\t})\n\n\t// Wait for something to happen\n\tgroupErrs.Go(func() error {\n\t\tselect {\n\t\t// Catch signals so exits are graceful and defers can run\n\t\tcase s := <-sigChan:\n\t\t\tcancel()\n\t\t\treturn fmt.Errorf(\"signal caught: %v\", s)\n\t\tcase <-ctx.Done():\n\t\t\treturn nil\n\t\t}\n\t})\n\t// Wait for all of the go funcs to finish up\n\treturn groupErrs.Wait()\n}\n\nfunc run(ctx context.Context, g *errgroup.Group, cfg *types.Configuration, ln net.Listener) error {\n\tvn, err := virtualnetwork.New(cfg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Info(\"waiting for clients...\")\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Errorf(\"failed to accept: %v\", err)\n\t\t\t}\n\t\t\t// AcceptStdio calls the underlying virtual network switch Accept function\n\t\t\terr = vn.AcceptStdio(ctx, conn)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Errorf(\"failed to accept connection: %v\", err)\n\t\t\t} else {\n\t\t\t\tlogrus.Infof(\"accepted connection: ctx=%+v conn=%+v\", ctx, conn)\n\t\t\t}\n\t\t}\n\t}()\n\n\tapiServer := fmt.Sprintf(\"%s:80\", cfg.GatewayIP)\n\tvnLn, err := vn.Listen(\"tcp\", apiServer)\n\tif err != nil {\n\t\treturn err\n\t}\n\tmux := http.NewServeMux()\n\tmux.Handle(\"/services/forwarder/all\", vn.Mux())\n\tmux.Handle(\"/services/forwarder/expose\", vn.Mux())\n\tmux.Handle(\"/services/forwarder/unexpose\", vn.Mux())\n\thttpServe(ctx, g, vnLn, mux)\n\tlogrus.Infof(\"port forwarding API server is running on: %s\", apiServer)\n\n\tlogInterval := time.Second * 5\n\tif debug {\n\t\tg.Go(func() error {\n\t\tdebugLog:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(logInterval):\n\t\t\t\t\tlogrus.Debugf(\"%v sent to the VM, %v received from the VM\", humanize.Bytes(vn.BytesSent()), humanize.Bytes(vn.BytesReceived()))\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\tbreak debugLog\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\treturn nil\n}\n\nfunc httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) {\n\tg.Go(func() error {\n\t\t<-ctx.Done()\n\t\treturn ln.Close()\n\t})\n\tg.Go(func() error {\n\t\ts := &http.Server{\n\t\t\tHandler:      mux,\n\t\t\tReadTimeout:  10 * time.Second,\n\t\t\tWriteTimeout: 10 * time.Second,\n\t\t}\n\t\terr := s.Serve(ln)\n\t\tif err != nil && !errors.Is(err, http.ErrServerClosed) {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t})\n}\n\nfunc vsockHandshake(ctx context.Context, handshakePort uint32, signature string) (net.Listener, error) {\n\tbailout := time.After(time.Second * timeoutSeconds)\n\tvmGUID, err := vsock.GetVMGUID(ctx, signature, handshakePort, bailout)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"trying to find WSL GUID failed: %w\", err)\n\t}\n\tlogrus.Infof(\"successful handshake, waiting for a vsock connection from VMGUID: %v on Port: %v\", vmGUID.String(), vsockListenPort)\n\tln, err := vsock.Listen(vmGUID, vsockListenPort)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"creating vsock listener for host-switch failed: %w\", err)\n\t}\n\terr = signalVsockListenerReady(vmGUID, vsockHandshakePort)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"sending %s signal to peer process failed: %w\", vsock.ReadySignal, err)\n\t}\n\treturn ln, nil\n}\n\nfunc signalVsockListenerReady(vmGUID hvsock.GUID, peerPort uint32) error {\n\tconn, err := vsock.GetVsockConnection(vmGUID, peerPort)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer conn.Close()\n\t_, err = conn.Write([]byte(vsock.ReadySignal))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/networking/cmd/network/setup_linux.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Command setup-network initializes the network namespace created by the\n// systemd unit `network-namespace.service` and forwards traffic.\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"strconv\"\n\n\t\"github.com/coreos/go-systemd/v22/dbus\"\n\t\"github.com/linuxkit/virtsock/pkg/vsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/vishvananda/netlink\"\n\t\"github.com/vishvananda/netns\"\n\t\"golang.org/x/sys/unix\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/log\"\n\trdvsock \"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/vsock\"\n)\n\nvar options struct {\n\tdebug            bool\n\tvmSwitchPath     string\n\tunshareArg       string\n\tvmSwitchLogFile  string\n\tdhcpScript       string\n\tlogFile          string\n\tnamespaceService string\n\ttapIface         string\n\tsubnet           string\n\ttapDeviceMacAddr string\n}\n\nconst (\n\tnsenter                 = \"/usr/bin/nsenter\"\n\tunshare                 = \"/usr/bin/unshare\"\n\tvsockHandshakePort      = 6669\n\tvsockDialPort           = 6656\n\tdefaultTapDevice        = \"eth0\"\n\tWSLVeth                 = \"veth-rd-wsl\"\n\tWSLVethIP               = \"192.168.143.2\"\n\tnamespaceVeth           = \"veth-rd-ns\"\n\tnamespaceVethIP         = \"192.168.143.1\"\n\tdefaultNamespaceService = \"network-namespace.service\"\n\tdefaultNamespacePID     = 1\n\tcidrOnes                = 24\n\tcidrBits                = 32\n\tstdout                  = \"/dev/stdout\"\n)\n\nfunc run() error {\n\tinitializeFlags()\n\n\tif err := setupLogging(options.logFile); err != nil {\n\t\treturn err\n\t}\n\n\tif options.vmSwitchPath == \"\" {\n\t\treturn fmt.Errorf(\"path to the vm-switch process must be provided\")\n\t}\n\n\tctx, cancel := signal.NotifyContext(context.Background(), unix.SIGTERM, unix.SIGHUP, unix.SIGQUIT)\n\tdefer cancel()\n\n\toriginNS, err := netns.Get()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed getting a handle to the current namespace: %w\", err)\n\t}\n\n\t// Remove any existing veth devices (before we set up network namespaces)\n\tcleanupVethLink(originNS)\n\n\t// listenForHandshake blocks until a successful handshake is established.\n\tif err := listenForHandshake(ctx); err != nil {\n\t\treturn fmt.Errorf(\"failed to handshake with host-switch: %w\", err)\n\t}\n\n\tlogrus.Debugf(\"attempting to connect to the host on CID: %v and Port: %d\", vsock.CIDHost, vsockDialPort)\n\tvsockConn, err := vsock.Dial(vsock.CIDHost, vsockDialPort)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.Debugf(\"successful connection to host on CID: %v and Port: %d: connection: %+v\", vsock.CIDHost, vsockDialPort, vsockConn)\n\n\tconnFile, err := vsockConn.File()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Ensure we stay on the same OS thread so that we don't switch namespaces\n\t// accidentally.  This must happen before we change any namespaces.\n\truntime.LockOSThread()\n\tdefer runtime.UnlockOSThread()\n\n\tvar peerNS netns.NsHandle\n\tif os.Getenv(\"SYSTEMD_EXEC_PID\") != \"\" {\n\t\t// Running under systemd\n\t\tif options.unshareArg != \"\" {\n\t\t\tlogrus.Warnf(\"Using systemd, ignoring --unshare-arg=%q\", options.unshareArg)\n\t\t}\n\n\t\tnamespacePID, err := getNamespacePID(ctx)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpeerNS, err = netns.GetFromPid(namespacePID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get network namespace: %w\", err)\n\t\t}\n\t} else {\n\t\t// Running under OpenRC\n\t\tif options.unshareArg == \"\" {\n\t\t\treturn fmt.Errorf(\"unshare program arg must be provided\")\n\t\t}\n\n\t\tpeerNS, err = configureNamespace()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := unshareCmd(ctx, peerNS, options.unshareArg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = createVethPair(\n\t\toriginNS,\n\t\tpeerNS,\n\t\tWSLVeth,\n\t\tnamespaceVeth)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create veth pair: %w\", err)\n\t}\n\tdefer cleanupVethLink(originNS)\n\n\tif err := configureVethPair(WSLVeth, WSLVethIP); err != nil {\n\t\treturn fmt.Errorf(\"failed setting up veth %q for default namespace: %w\", WSLVeth, err)\n\t}\n\n\t// Enter the network namespace to set up its network interface, and to run\n\t// vm-switch.  We will only switch back to the default namespace on teardown\n\t// after this point.\n\tif err := netns.Set(peerNS); err != nil {\n\t\treturn fmt.Errorf(\"failed to set network namespace: %w\", err)\n\t}\n\tif err := configureVethPair(namespaceVeth, namespaceVethIP); err != nil {\n\t\treturn fmt.Errorf(\"failed to set up veth %q for Rancher Desktop namespace: %w\", namespaceVeth, err)\n\t}\n\n\tlogrus.Debug(\"Starting vm-switch...\")\n\n\tvmSwitchCmd := configureVMSwitch(\n\t\tctx,\n\t\toptions.vmSwitchLogFile,\n\t\toptions.vmSwitchPath,\n\t\toptions.tapIface,\n\t\toptions.subnet,\n\t\toptions.tapDeviceMacAddr,\n\t\toptions.dhcpScript,\n\t\tconnFile)\n\tif err := vmSwitchCmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"vm-switch failed to start: %w\", err)\n\t}\n\n\t// Use vmSwitchCmd.Start() + Run() so we can get better messages about whether\n\t// the start failed or if it started then exited.\n\n\tif err := vmSwitchCmd.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"vm-switch exited with error: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc main() {\n\tif err := run(); err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n}\n\nfunc initializeFlags() {\n\tflag.BoolVar(&options.debug, \"debug\", false, \"enable additional debugging\")\n\tflag.StringVar(&options.namespaceService, \"namespace-service\", defaultNamespaceService, \"systemd service which creates the network namespace\")\n\tflag.StringVar(&options.tapIface, \"tap-interface\", defaultTapDevice, \"tap interface name, eg. eth0, eth1\")\n\tflag.StringVar(&options.subnet, \"subnet\", config.DefaultSubnet,\n\t\tfmt.Sprintf(\"Subnet range with CIDR suffix that is associated to the tap interface, e,g: %s\", config.DefaultSubnet))\n\tflag.StringVar(&options.tapDeviceMacAddr, \"tap-mac-address\", config.TapDeviceMacAddr,\n\t\t\"MAC address that is associated to the tap interface\")\n\tflag.StringVar(&options.dhcpScript, \"dhcp-script\", \"\", \"script to run on DHCP events\")\n\tflag.StringVar(&options.vmSwitchPath, \"vm-switch-path\", \"\", \"the path to the vm-switch binary that will run in a new namespace\")\n\tflag.StringVar(&options.vmSwitchLogFile, \"vm-switch-logfile\", \"\", \"path to the logfile for vm-switch process\")\n\tflag.StringVar(&options.unshareArg, \"unshare-arg\", \"\", \"the command argument to pass to the unshare program\")\n\tflag.StringVar(&options.logFile, \"logfile\", \"/var/log/network-setup.log\", \"path to the logfile for network setup process\")\n\tflag.Parse()\n}\n\nfunc setupLogging(logFile string) error {\n\tif logFile == stdout {\n\t\t// Use the stdout handle instead of `/dev/stdout` because the latter does\n\t\t// not work correctly inside a systemd service.\n\t\tlogrus.StandardLogger().SetOutput(os.Stdout)\n\t} else {\n\t\tif err := log.SetOutputFile(logFile, logrus.StandardLogger()); err != nil {\n\t\t\treturn fmt.Errorf(\"setting logger's output file failed: %w\", err)\n\t\t}\n\t}\n\n\tif options.debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\n\treturn nil\n}\n\n// Set up the vm-switch process, but do not start it.  This is run from the same\n// network namespace as the current process.\nfunc configureVMSwitch(\n\tctx context.Context,\n\tvmSwitchLogFile,\n\tvmSwitchPath,\n\ttapIface,\n\tsubnet,\n\ttapDevMacAddr,\n\tdhcpScript string,\n\tconnFile *os.File) *exec.Cmd {\n\targs := []string{\n\t\tvmSwitchPath,\n\t\t\"-tap-interface\",\n\t\ttapIface,\n\t\t\"-subnet\",\n\t\tsubnet,\n\t\t\"-tap-mac-address\",\n\t\ttapDevMacAddr,\n\t\t\"-dhcp-script\",\n\t\tdhcpScript,\n\t}\n\tif vmSwitchLogFile != \"\" {\n\t\targs = append(args, \"-logfile\", vmSwitchLogFile)\n\t}\n\tif options.debug {\n\t\targs = append(args, \"-debug\")\n\t}\n\n\t//nolint:gosec // Arguments are ultimately controlled by our configs.\n\tvmSwitchCmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\tvmSwitchCmd.Stdout = os.Stdout\n\tvmSwitchCmd.Stderr = os.Stderr\n\n\t// Pass in the vsock connection as a FD to the vm-switch process.\n\tvmSwitchCmd.ExtraFiles = []*os.File{connFile}\n\treturn vmSwitchCmd\n}\n\nfunc createVethPair(defaultNS, peerNS netns.NsHandle, defaultNSVeth, rancherDesktopNSVeth string) error {\n\tveth := &netlink.Veth{\n\t\tLinkAttrs: netlink.LinkAttrs{\n\t\t\tName:      defaultNSVeth,\n\t\t\tNamespace: netlink.NsFd(defaultNS),\n\t\t},\n\t\tPeerName:      rancherDesktopNSVeth,\n\t\tPeerNamespace: netlink.NsFd(peerNS),\n\t}\n\tif err := netlink.LinkAdd(veth); err != nil {\n\t\treturn fmt.Errorf(\"failed to add veth link %+v: %w\", veth, err)\n\t}\n\tlogrus.Infof(\"created veth pair %s and %s\", defaultNSVeth, rancherDesktopNSVeth)\n\treturn nil\n}\n\n// Switch to the given (default) namespace, and tear down the veth if it exists.\n// Normally this should happen when the network namespace goes away.\nfunc cleanupVethLink(originNS netns.NsHandle) {\n\t// First, though, switch back to the default namespace if available.\n\t// This would fail if we already switched to it (and closed the handle).\n\t_ = netns.Set(originNS)\n\tif link, err := netlink.LinkByName(WSLVeth); err == nil {\n\t\terr = netlink.LinkDel(link)\n\t\tlogrus.Infof(\"tearing down link %s: %v\", WSLVeth, err)\n\t}\n}\n\n// Configure the address of the given network interface.  The interface must\n// be visible in the current network namespace.\nfunc configureVethPair(vethName, ipAddr string) error {\n\tveth, err := netlink.LinkByName(vethName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get link %s: %w\", vethName, err)\n\t}\n\n\tvethIP := net.IPNet{\n\t\tIP:   net.ParseIP(ipAddr),\n\t\tMask: net.CIDRMask(cidrOnes, cidrBits),\n\t}\n\n\taddr := &netlink.Addr{IPNet: &vethIP, Label: \"\"}\n\tif err := netlink.AddrAdd(veth, addr); err != nil {\n\t\treturn fmt.Errorf(\"failed to add addr %s to %s: %w\", addr, vethName, err)\n\t}\n\n\tif err := netlink.LinkSetUp(veth); err != nil {\n\t\treturn fmt.Errorf(\"failed to set up link %s: %w\", vethName, err)\n\t}\n\treturn nil\n}\n\nfunc unshareCmd(ctx context.Context, ns netns.NsHandle, args string) error {\n\tunshareCmd := exec.CommandContext( //nolint:gosec // no security concern with the potentially tainted command arguments\n\t\tctx,\n\t\tnsenter, fmt.Sprintf(\"-n/proc/%d/fd/%d\", os.Getpid(), ns), \"-F\",\n\t\tunshare, \"--pid\", \"--mount-proc\", \"--fork\", \"--propagation\", \"slave\", args)\n\tunshareCmd.Stdout = os.Stdout\n\tunshareCmd.Stderr = os.Stderr\n\tif err := unshareCmd.Start(); err != nil {\n\t\treturn fmt.Errorf(\"could not start the unshare process: %w\", err)\n\t}\n\n\tif err := writeWSLInitPid(unshareCmd.Process.Pid); err != nil {\n\t\treturn fmt.Errorf(\"writing wsl-init.pid failed: %w\", err)\n\t}\n\n\tlogrus.Debugf(\"successfully wrote wsl-init.pid with: %d\", unshareCmd.Process.Pid)\n\treturn nil\n}\n\nfunc writeWSLInitPid(pid int) error {\n\tunsharePID := strconv.Itoa(pid)\n\n\twritePermission := 0o600\n\terr := os.WriteFile(\"/run/wsl-init.pid\", []byte(unsharePID), fs.FileMode(writePermission))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc listenForHandshake(ctx context.Context) error {\n\tlogrus.Info(\"starting handshake process with host-switch\")\n\tl, err := vsock.Listen(vsock.CIDAny, vsockHandshakePort)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to listen on handshake port: %w\", err)\n\t}\n\tdefer l.Close()\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tl.Close()\n\t}()\n\tfor {\n\t\tconn, err := l.Accept()\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"listenForHandshake connection accept failed: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\t_, err = conn.Write([]byte(rdvsock.SignaturePhrase))\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"listenForHandshake writing signature phrase failed: %v\", err)\n\t\t}\n\n\t\t// verify that the host-switch is ready for us to establish the connection\n\t\tbuf := make([]byte, len(rdvsock.ReadySignal))\n\t\tif _, err := io.ReadFull(conn, buf); err != nil {\n\t\t\tlogrus.Errorf(\"listenForHandshake reading signature phrase failed: %v\", err)\n\t\t}\n\t\tif string(buf) == rdvsock.ReadySignal {\n\t\t\tbreak\n\t\t}\n\t\tconn.Close()\n\t}\n\tlogrus.Info(\"listenForHandshake successful handshake with host-switch\")\n\treturn nil\n}\n\n// Create a new network namespace, and return the new handle.\n// The thread will be left in the original namespace.\nfunc configureNamespace() (ns netns.NsHandle, err error) {\n\truntime.LockOSThread()\n\tdefer runtime.UnlockOSThread()\n\toldNS, err := netns.Get()\n\tif err != nil {\n\t\treturn netns.None(), err\n\t}\n\tdefer func() {\n\t\tif err2 := netns.Set(oldNS); err == nil && err2 != nil {\n\t\t\terr = err2\n\t\t}\n\t}()\n\tns, err = netns.New()\n\tif err != nil {\n\t\treturn netns.None(), fmt.Errorf(\"creating new namespace failed: %w\", err)\n\t}\n\n\tlogrus.Infof(\"created a new namespace %v %v\", ns, ns.String())\n\treturn ns, nil\n}\n\n// Find the PID of the systemd unit `network-namespace.service` and return it.\nfunc getNamespacePID(ctx context.Context) (int, error) {\n\tconn, err := dbus.NewSystemConnectionContext(ctx)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to connect to systemd system bus: %w\", err)\n\t}\n\tdefer conn.Close()\n\tprop, err := conn.GetServicePropertyContext(ctx, options.namespaceService, \"MainPID\")\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to get namespace service %s main pid: %w\", options.namespaceService, err)\n\t}\n\tpid, ok := prop.Value.Value().(uint32)\n\tif !ok {\n\t\tfmt.Printf(\"debug: prop is %+v (%v)\", prop.Value.Value(), reflect.ValueOf(prop.Value.Value()))\n\t\treturn 0, fmt.Errorf(\"failed to look up main pid of service %s: got value %+v\", options.namespaceService, prop)\n\t}\n\treturn int(pid), nil\n}\n"
  },
  {
    "path": "src/go/networking/cmd/proxy/wsl_integration_linux.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/log\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/portproxy\"\n)\n\nvar (\n\tdebug        bool\n\tlogFile      string\n\tsocketFile   string\n\tupstreamAddr string\n\tudpBuffer    int\n)\n\nconst (\n\tdefaultLogPath = \"/var/log/wsl-proxy.log\"\n\tdefaultSocket  = \"/run/wsl-proxy.sock\"\n\tbridgeIPAddr   = \"192.168.143.1\"\n\t// Set UDP buffer size to 8 MB\n\tdefaultUDPBufferSize = 8 * 1024 * 1024 // 8 MB in bytes\n)\n\nfunc main() {\n\tflag.BoolVar(&debug, \"debug\", false, \"enable additional debugging.\")\n\tflag.StringVar(&logFile, \"logfile\", defaultLogPath, \"path to the logfile for wsl-proxy process\")\n\tflag.StringVar(&socketFile, \"socketFile\", defaultSocket, \"path to the .sock file for UNIX socket\")\n\tflag.StringVar(&upstreamAddr, \"upstreamAddress\", bridgeIPAddr, \"IP address of the upstream server to forward to\")\n\tflag.IntVar(&udpBuffer, \"udpBuffer\", defaultUDPBufferSize, \"max buffer size in bytes for UDP socket I/O\")\n\tflag.Parse()\n\n\tsetupLogging(logFile)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tlistenerConfig := net.ListenConfig{}\n\tsocket, err := listenerConfig.Listen(ctx, \"unix\", socketFile)\n\tif err != nil {\n\t\tlogrus.Fatalf(\"failed to create listener for published ports: %s\", err)\n\t\treturn\n\t}\n\tproxyConfig := &portproxy.ProxyConfig{\n\t\tUpstreamAddress: upstreamAddr,\n\t\tUDPBufferSize:   udpBuffer,\n\t}\n\tproxy := portproxy.NewPortProxy(ctx, socket, proxyConfig)\n\n\t// Handle graceful shutdown\n\tsigCh := make(chan os.Signal, 1)\n\tsignal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\t<-sigCh\n\t\tlogrus.Println(\"Shutting down...\")\n\t\tif err := proxy.Close(); err != nil {\n\t\t\tlogrus.Errorf(\"proxy close error: %s\", err)\n\t\t}\n\t\tos.Exit(0)\n\t}()\n\n\terr = proxy.Start()\n\tif err != nil {\n\t\tlogrus.Errorf(\"failed to start accepting: %s\", err)\n\t\treturn\n\t}\n}\n\nfunc setupLogging(logFile string) {\n\tif err := log.SetOutputFile(logFile, logrus.StandardLogger()); err != nil {\n\t\tlogrus.Fatalf(\"setting logger's output file failed: %v\", err)\n\t}\n\n\tif debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t} else {\n\t\tlogrus.SetLevel(logrus.InfoLevel)\n\t}\n}\n"
  },
  {
    "path": "src/go/networking/cmd/vm/switch_linux.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/songgao/packets/ethernet\"\n\t\"github.com/songgao/water\"\n\t\"github.com/vishvananda/netlink\"\n\t\"gvisor.dev/gvisor/pkg/tcpip/header\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/log\"\n)\n\nvar (\n\tdebug            bool\n\tvsockFD          int\n\tdhcpScript       string\n\ttapIface         string\n\tlogFile          string\n\tsubnet           string\n\ttapDeviceMacAddr string\n)\n\nconst (\n\tdefaultTapDevice = \"eth0\"\n\tdefaultVsockFD   = 3\n\tmaxMTU           = 4000\n)\n\nfunc main() {\n\tflag.BoolVar(&debug, \"debug\", false, \"enable debug flag\")\n\tflag.StringVar(&tapIface, \"tap-interface\", defaultTapDevice, \"tap interface name, eg. eth0, eth1\")\n\tflag.IntVar(&vsockFD, \"vsock-fd\", defaultVsockFD, \"file descriptor for vsock connection\")\n\tflag.StringVar(&dhcpScript, \"dhcp-script\", \"\", \"script to run on DHCP events\")\n\tflag.StringVar(&tapDeviceMacAddr, \"tap-mac-address\", config.TapDeviceMacAddr,\n\t\t\"MAC address that is associated to the tap interface\")\n\tflag.StringVar(&subnet, \"subnet\", config.DefaultSubnet,\n\t\tfmt.Sprintf(\"Subnet range with CIDR suffix that is associated to the tap interface, e,g: %s\", config.DefaultSubnet))\n\tflag.StringVar(&logFile, \"logfile\", \"/var/log/vm-switch.log\", \"path to vm-switch process logfile\")\n\tflag.Parse()\n\n\tif err := log.SetOutputFile(logFile, logrus.StandardLogger()); err != nil {\n\t\tlogrus.Fatalf(\"setting logger's output file failed: %v\", err)\n\t}\n\n\tif debug {\n\t\tlogrus.SetLevel(logrus.DebugLevel)\n\t}\n\n\t// the FD is passed-in as an extra arg from exec.Command\n\t// of the parent process. This is for the AF_VSOCK connection that\n\t// is handed over from the default namespace to Rancher Desktop's\n\t// network namespace, the logic behind this approach is because\n\t// AF_VSOCK is affected by network namespaces, therefore we need\n\t// to open it before entering a new namespace (via unshare/nsenter)\n\tconnFile := os.NewFile(uintptr(vsockFD), \"vsock connection\")\n\n\tlogrus.Debugf(\"using a AF_VSOCK connection file from default namespace: %v\", connFile)\n\n\t// this should never happen\n\tif err := checkForExistingIface(tapIface); err != nil {\n\t\tlogrus.Fatal(err)\n\t}\n\n\t// catch user issued signals\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)\n\n\tfor {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tselect {\n\t\tcase s := <-sigChan:\n\t\t\tlogrus.Errorf(\"signal caught: %v\", s)\n\t\t\tcancel()\n\t\t\tconnFile.Close()\n\t\t\tos.Exit(1)\n\t\tdefault:\n\t\t\tif err := run(ctx, cancel, connFile); err != nil {\n\t\t\t\tlogrus.Error(err)\n\t\t\t}\n\t\t\t// Wait one second before attempting to re-establish connection.\n\t\t\ttime.Sleep(time.Second)\n\t\t}\n\t}\n}\n\nfunc run(ctx context.Context, cancel context.CancelFunc, connFile io.ReadWriteCloser) error {\n\ttap, err := water.New(water.Config{\n\t\tDeviceType: water.TAP,\n\t\tPlatformSpecificParams: water.PlatformSpecificParams{\n\t\t\tName: tapIface,\n\t\t},\n\t})\n\tif err != nil {\n\t\tlogrus.Fatalf(\"creating tap device %v failed: %s\", tapIface, err)\n\t}\n\tlogrus.Debugf(\"created tap device %s: %v\", tapIface, tap)\n\n\tdefer func() {\n\t\tconnFile.Close()\n\t\ttap.Close()\n\t\tlogrus.Debugf(\"closed tap device: %s\", tapIface)\n\t}()\n\n\tif err := linkUp(tapIface, tapDeviceMacAddr); err != nil {\n\t\tlogrus.Fatalf(\"setting mac address [%s] for %s tap device failed: %s\", tapDeviceMacAddr, tapIface, err)\n\t}\n\tif err := loopbackUp(); err != nil {\n\t\tlogrus.Fatalf(\"enabling loop back device failed: %s\", err)\n\t}\n\n\tlogrus.Debugf(\"setup complete for tap interface %s(%s) + loopback\", tapIface, tapDeviceMacAddr)\n\n\terrCh := make(chan error, 1)\n\tgo tx(ctx, connFile, tap, errCh, maxMTU)\n\tgo rx(ctx, connFile, tap, errCh, maxMTU)\n\tgo func() {\n\t\tif err := dhcp(ctx, tapIface); err != nil {\n\t\t\terrCh <- fmt.Errorf(\"dhcp error: %w\", err)\n\t\t\tcancel()\n\t\t}\n\t}()\n\n\treturn <-errCh\n}\n\nfunc loopbackUp() error {\n\tlo, err := netlink.LinkByName(\"lo\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn netlink.LinkSetUp(lo)\n}\n\nfunc linkUp(iface, mac string) error {\n\tlink, err := netlink.LinkByName(iface)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif mac == \"\" {\n\t\treturn netlink.LinkSetUp(link)\n\t}\n\thw, err := net.ParseMAC(mac)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := netlink.LinkSetHardwareAddr(link, hw); err != nil {\n\t\treturn err\n\t}\n\n\tlogrus.Debugf(\"successful link setup %+v\\n\", link)\n\treturn netlink.LinkSetUp(link)\n}\n\nfunc dhcp(ctx context.Context, iface string) error {\n\targs := []string{\"-f\", \"-i\", iface}\n\tif dhcpScript != \"\" {\n\t\targs = append(args, \"-s\", dhcpScript)\n\t}\n\tcmd := exec.CommandContext(ctx, \"udhcpc\", args...)\n\tcmd.Stderr = os.Stderr\n\tcmd.Stdout = os.Stdout\n\treturn cmd.Run()\n}\n\nfunc rx(ctx context.Context, conn io.Writer, tap *water.Interface, errCh chan error, mtu int) {\n\tlogrus.Info(\"waiting for packets...\")\n\tif mtu > math.MaxUint16 {\n\t\terrCh <- fmt.Errorf(\"invalid MTU %d\", mtu)\n\t\treturn\n\t}\n\tvar frame ethernet.Frame\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogrus.Info(\"exiting rx goroutine\")\n\t\t\treturn\n\t\tdefault:\n\t\t\tframe.Resize(mtu)\n\t\t\tn, err := tap.Read([]byte(frame))\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"reading packet from tap failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tframe = frame[:n]\n\n\t\t\tsize := make([]byte, 2)\n\t\t\tbinary.LittleEndian.PutUint16(size, uint16(n))\n\n\t\t\tif _, err := conn.Write(size); err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"writing size to the socket failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif _, err := conn.Write(frame); err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"writing packet to the socket failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif debug {\n\t\t\t\tpacket := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default)\n\t\t\t\tlogrus.Infof(\"wrote packet (vm -> host %d): %s\", size, packet.String())\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc tx(ctx context.Context, conn io.Reader, tap *water.Interface, errCh chan error, mtu int) {\n\tsizeBuf := make([]byte, 2)\n\tbuf := make([]byte, mtu+header.EthernetMinimumSize)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogrus.Info(\"exiting tx goroutine\")\n\t\t\treturn\n\t\tdefault:\n\t\t\tn, err := io.ReadFull(conn, sizeBuf)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"reading size from socket failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif n != 2 {\n\t\t\t\terrCh <- fmt.Errorf(\"unexpected size %d\", n)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsize := int(binary.LittleEndian.Uint16(sizeBuf[0:2]))\n\n\t\t\tif cap(buf) < size {\n\t\t\t\tbuf = make([]byte, size)\n\t\t\t}\n\n\t\t\tn, err = io.ReadFull(conn, buf[:size])\n\t\t\tif err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"reading payload from socket failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif n == 0 || n != size {\n\t\t\t\terrCh <- fmt.Errorf(\"unexpected size %d != %d\", n, size)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif _, err := tap.Write(buf[:size]); err != nil {\n\t\t\t\terrCh <- fmt.Errorf(\"writing packet to tap failed: %w\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif debug {\n\t\t\t\tpacket := gopacket.NewPacket(buf[:size], layers.LayerTypeEthernet, gopacket.Default)\n\t\t\t\tlogrus.Infof(\"read packet (host -> vm %d): %s\", size, packet.String())\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc checkForExistingIface(ifName string) error {\n\t// equivalent to: `ip link show`\n\tlinks, err := netlink.LinkList()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"getting link devices failed: %w\", err)\n\t}\n\n\tfor _, link := range links {\n\t\tif link.Attrs().Name == ifName {\n\t\t\treturn fmt.Errorf(\"%s interface already exist, exiting now\", ifName)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/networking/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/networking\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/Microsoft/go-winio v0.6.2\n\tgithub.com/containers/gvisor-tap-vsock v0.8.8\n\tgithub.com/coreos/go-systemd/v22 v22.7.0\n\tgithub.com/docker/go-connections v0.6.0\n\tgithub.com/dustin/go-humanize v1.0.1\n\tgithub.com/google/gopacket v1.1.19\n\tgithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2\n\tgithub.com/rancher-sandbox/rancher-desktop/src/go/guestagent v0.0.0-20240911164922-5443d1a11011\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/songgao/packets v0.0.0-20160404182456-549a10cd4091\n\tgithub.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/vishvananda/netlink v1.3.1\n\tgithub.com/vishvananda/netns v0.0.5\n\tgolang.org/x/net v0.52.0\n\tgolang.org/x/sync v0.20.0\n\tgolang.org/x/sys v0.42.0\n\tgvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f\n)\n\nrequire (\n\tgithub.com/apparentlymart/go-cidr v1.1.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/godbus/dbus/v5 v5.1.0 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 // indirect\n\tgithub.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mdlayher/socket v0.5.1 // indirect\n\tgithub.com/miekg/dns v1.1.72 // indirect\n\tgithub.com/nxadm/tail v1.4.11 // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.21 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "src/go/networking/go.sum",
    "content": "github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=\ngithub.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=\ngithub.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=\ngithub.com/containers/gvisor-tap-vsock v0.8.8 h1:5FznbOYMIuaCv8B6zQ7M6wjqP63Lasy0A6GpViEnjTg=\ngithub.com/containers/gvisor-tap-vsock v0.8.8/go.mod h1:m/PzhZWAS6T9pCRH1fLkq2OqbEd6QEUZWjm3FS5F+CE=\ngithub.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=\ngithub.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=\ngithub.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=\ngithub.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=\ngithub.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=\ngithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048 h1:jaqViOFFlZtkAwqvwZN+id37fosQqR5l3Oki9Dk4hz8=\ngithub.com/inetaf/tcpproxy v0.0.0-20250222171855-c4b9df066048/go.mod h1:Di7LXRyUcnvAcLicFhtM9/MlZl/TNgRSDHORM2c6CMI=\ngithub.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=\ngithub.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=\ngithub.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=\ngithub.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM=\ngithub.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=\ngithub.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=\ngithub.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=\ngithub.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=\ngithub.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=\ngithub.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=\ngithub.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=\ngithub.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rancher-sandbox/rancher-desktop/src/go/guestagent v0.0.0-20240911164922-5443d1a11011 h1:X8nBYGrEv9MLQWSnigWUBKHc0z6ztVZ1f8NKo1ixt3Q=\ngithub.com/rancher-sandbox/rancher-desktop/src/go/guestagent v0.0.0-20240911164922-5443d1a11011/go.mod h1:b1WHkY6WX8vcR97BCWkMRXXdjaQvmJvtc42mJZrx61I=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/songgao/packets v0.0.0-20160404182456-549a10cd4091 h1:1zN6ImoqhSJhN8hGXFaJlSC8msLmIbX8bFqOfWLKw0w=\ngithub.com/songgao/packets v0.0.0-20160404182456-549a10cd4091/go.mod h1:N20Z5Y8oye9a7HmytmZ+tr8Q2vlP0tAHP13kTHzwvQY=\ngithub.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=\ngithub.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=\ngithub.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=\ngithub.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=\ngithub.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4=\ngithub.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=\ngithub.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=\ngvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=\n"
  },
  {
    "path": "src/go/networking/pkg/config/config.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// config contains all the configuration that is required by host switch.\npackage config\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst (\n\t// Subnet range that is used by default if\n\t// one is not provided through the arguments.\n\tDefaultSubnet = \"192.168.127.0/24\"\n\t// Reserved Mac Address for the tap device eth0 that\n\t// is used by vm switch during the tap device\n\t// creation.\n\tTapDeviceMacAddr   = \"5a:94:ef:e4:0c:ee\"\n\tgatewayLastByte    = 1\n\tstaticDHCPLastByte = 2\n\tstaticHostLastByte = 254\n)\n\n// Subnet represents all the network properties\n// that are required by the host switch process.\ntype Subnet struct {\n\tGatewayIP       string\n\tStaticDHCPLease map[string]string\n\tStaticDNSHost   string\n\tSubnetCIDR      string\n}\n\n// ValidateSubnet validates a given IP CIDR format and\n// creates all the network addresses that are consumable\n// by the host switch process.\nfunc ValidateSubnet(subnet string) (*Subnet, error) {\n\tip, _, err := net.ParseCIDR(subnet)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"validating subnet: %w\", err)\n\t}\n\tipv4 := ip.To4()\n\treturn &Subnet{\n\t\tGatewayIP: gatewayIP(ipv4),\n\t\tStaticDHCPLease: map[string]string{\n\t\t\tTapDeviceIP(ipv4): TapDeviceMacAddr,\n\t\t},\n\t\tStaticDNSHost: staticDNSHost(ipv4),\n\t\tSubnetCIDR:    subnet,\n\t}, nil\n}\n\n// SearchDomains reads the content of the /etc/resolv.conf when\n// supported by the platform and returns an array of search domains.\nfunc SearchDomains() []string {\n\tif runtime.GOOS == \"darwin\" || runtime.GOOS == \"linux\" {\n\t\tf, err := os.Open(\"/etc/resolv.conf\")\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"open file error: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t\tdefer f.Close()\n\t\tsc := bufio.NewScanner(f)\n\t\tsearchPrefix := \"search \"\n\t\tfor sc.Scan() {\n\t\t\tif strings.HasPrefix(sc.Text(), searchPrefix) {\n\t\t\t\tsearchDomains := strings.Split(strings.TrimPrefix(sc.Text(), searchPrefix), \" \")\n\t\t\t\tlogrus.Debugf(\"Using search domains: %v\", searchDomains)\n\t\t\t\treturn searchDomains\n\t\t\t}\n\t\t}\n\t\tif err := sc.Err(); err != nil {\n\t\t\tlogrus.Errorf(\"scan file error: %v\", err)\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn nil\n}\n\n// ParsePortForwarding converts the input format of HostIP:Port=GuestIP:Port\n// into a map of {\"HostIP:Port\" : \"GuestIP:Port\"}\nfunc ParsePortForwarding(ipPorts []string) (map[string]string, error) {\n\tportForwards := make(map[string]string)\n\tfor _, v := range ipPorts {\n\t\tipPort := strings.Split(v, \"=\")\n\t\tif len(ipPort) != 2 {\n\t\t\treturn portForwards, fmt.Errorf(\"input %q not in expected format: HostIP:Port=GuestIP:Port\", ipPort)\n\t\t}\n\t\tif err := validateIPPort(ipPort); err != nil {\n\t\t\treturn portForwards, err\n\t\t}\n\t\t// \"HostIP:Port\" : \"GuestIP:Port\"\n\t\tportForwards[ipPort[0]] = ipPort[1]\n\t}\n\treturn portForwards, nil\n}\n\n// TapDeviceIP returns the allocated IP address for\n// the Tap Device.\nfunc TapDeviceIP(ip net.IP) string {\n\t// Tap device IP is always x.x.x.2\n\treturn net.IPv4(ip[0], ip[1], ip[2], staticDHCPLastByte).String()\n}\n\nfunc gatewayIP(ip net.IP) string {\n\t// Gateway is always x.x.x.1\n\treturn net.IPv4(ip[0], ip[1], ip[2], gatewayLastByte).String()\n}\n\nfunc staticDNSHost(ip net.IP) string {\n\t// Static DNS Host is always x.x.x.254\n\treturn net.IPv4(ip[0], ip[1], ip[2], staticHostLastByte).String()\n}\n\nfunc validateIPPort(ipPorts []string) error {\n\tfor _, ipPort := range ipPorts {\n\t\tip, port, err := net.SplitHostPort(ipPort)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tintPort, err := strconv.Atoi(port)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif intPort <= 0 || intPort > 65535 {\n\t\t\treturn fmt.Errorf(\"invalid port number provided: %d\", intPort)\n\t\t}\n\t\tif net.ParseIP(ip) == nil {\n\t\t\treturn fmt.Errorf(\"invalid IP address provided: %s\", ip)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/networking/pkg/log/log.go",
    "content": "package log\n\n/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport (\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst fileMode = 0o666\n\n// SetOutputFile sets the logger output with a given file\nfunc SetOutputFile(filePath string, logger *logrus.Logger) error {\n\tlogFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogger.SetOutput(logFile)\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/networking/pkg/portproxy/server.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage portproxy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\tgvisorTypes \"github.com/containers/gvisor-tap-vsock/pkg/types\"\n\t\"github.com/docker/go-connections/nat\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/utils\"\n)\n\ntype ProxyConfig struct {\n\tUpstreamAddress string\n\tUDPBufferSize   int\n}\n\ntype PortProxy struct {\n\tctx            context.Context\n\tconfig         *ProxyConfig\n\tlistener       net.Listener\n\tquit           chan struct{}\n\tlistenerConfig net.ListenConfig\n\t// map of TCP port number as a key to associated listener\n\tactiveListeners map[int]net.Listener\n\tlistenerMutex   sync.Mutex\n\t// map of UDP port number as a key to associated UDPConn\n\tactiveUDPConns map[int]*net.UDPConn\n\tudpConnMutex   sync.Mutex\n\twg             sync.WaitGroup\n}\n\nfunc NewPortProxy(ctx context.Context, listener net.Listener, cfg *ProxyConfig) *PortProxy {\n\tportProxy := &PortProxy{\n\t\tctx:             ctx,\n\t\tconfig:          cfg,\n\t\tlistener:        listener,\n\t\tquit:            make(chan struct{}),\n\t\tlistenerConfig:  net.ListenConfig{},\n\t\tactiveListeners: make(map[int]net.Listener),\n\t\tactiveUDPConns:  make(map[int]*net.UDPConn),\n\t}\n\treturn portProxy\n}\n\nfunc (p *PortProxy) Start() error {\n\tlogrus.Infof(\"Proxy server started accepting on %s, forwarding to %s\", p.listener.Addr(), p.config.UpstreamAddress)\n\tfor {\n\t\tconn, err := p.listener.Accept()\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-p.quit:\n\t\t\t\tlogrus.Debug(\"received a quit signal, exiting out of accept loop\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"failed to accept connection: %w\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tgo p.handleEvent(conn)\n\t\t}\n\t}\n}\n\nfunc (p *PortProxy) UDPPortMappings() map[int]*net.UDPConn {\n\tp.udpConnMutex.Lock()\n\tdefer p.udpConnMutex.Unlock()\n\treturn p.activeUDPConns\n}\n\nfunc (p *PortProxy) handleEvent(conn net.Conn) {\n\tdefer conn.Close()\n\n\tvar pm types.PortMapping\n\tif err := json.NewDecoder(conn).Decode(&pm); err != nil {\n\t\tlogrus.Errorf(\"port server decoding received payload error: %s\", err)\n\t\treturn\n\t}\n\tp.exec(pm)\n}\n\nfunc (p *PortProxy) exec(pm types.PortMapping) {\n\tfor portProto, portBindings := range pm.Ports {\n\t\tproto := strings.ToLower(portProto.Proto())\n\t\tlogrus.Debugf(\"received the following port: [%s] and protocol: [%s] from portMapping: %+v\", portProto.Port(), proto, pm)\n\n\t\tswitch gvisorTypes.TransportProtocol(proto) {\n\t\tcase gvisorTypes.TCP:\n\t\t\tp.handleTCP(portBindings, pm.Remove)\n\t\tcase gvisorTypes.UDP:\n\t\t\tp.handleUDP(portBindings, pm.Remove)\n\t\tdefault:\n\t\t\tlogrus.Warnf(\"unsupported protocol: [%s]\", proto)\n\t\t}\n\t}\n}\n\nfunc (p *PortProxy) handleUDP(portBindings []nat.PortBinding, remove bool) {\n\tfor _, portBinding := range portBindings {\n\t\tport, err := nat.ParsePort(portBinding.HostPort)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"parsing port error: %s\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif remove {\n\t\t\tp.udpConnMutex.Lock()\n\t\t\tif udpConn, exist := p.activeUDPConns[port]; exist {\n\t\t\t\tif err := udpConn.Close(); err != nil {\n\t\t\t\t\tlogrus.Errorf(\"error closing UDPConn for port [%s]: %s\", portBinding.HostPort, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete(p.activeUDPConns, port)\n\t\t\tp.udpConnMutex.Unlock()\n\t\t\tlogrus.Debugf(\"closing UDPConn for port: %d\", port)\n\t\t\tcontinue\n\t\t}\n\n\t\t// the localAddress IP section can either be 0.0.0.0 or 127.0.0.1\n\t\tlocalAddress := net.JoinHostPort(portBinding.HostIP, portBinding.HostPort)\n\t\tsourceAddr, err := net.ResolveUDPAddr(\"udp\", localAddress)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"failed to resolve UDP source address [%s]: %s\", sourceAddr, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tc, err := net.ListenUDP(\"udp\", sourceAddr)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"failed creating listener for published port [%s]: %s\", portBinding.HostPort, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tforwardAddr := net.JoinHostPort(p.config.UpstreamAddress, portBinding.HostPort)\n\t\ttargetAddr, err := net.ResolveUDPAddr(\"udp\", forwardAddr)\n\t\tif err != nil {\n\t\t\tc.Close()\n\t\t\tlogrus.Errorf(\"failed to resolve UDP target address [%s]: %s\", targetAddr, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tp.udpConnMutex.Lock()\n\t\tp.activeUDPConns[port] = c\n\t\tp.udpConnMutex.Unlock()\n\t\tlogrus.Debugf(\"created UDPConn for: %v\", sourceAddr)\n\n\t\tgo p.acceptUDPConn(c, targetAddr)\n\t}\n}\n\nfunc (p *PortProxy) acceptUDPConn(sourceConn *net.UDPConn, targetAddr *net.UDPAddr) {\n\ttargetConn, err := net.DialUDP(\"udp\", nil, targetAddr)\n\tif err != nil {\n\t\tlogrus.Errorf(\"failed to connect to target address: %s : %s\", targetAddr, err)\n\t\treturn\n\t}\n\tdefer targetConn.Close()\n\tp.wg.Add(1)\n\tfor {\n\t\tb := make([]byte, p.config.UDPBufferSize)\n\t\tn, addr, err := sourceConn.ReadFromUDP(b)\n\t\tif err != nil && n == 0 {\n\t\t\tlogrus.Errorf(\"error reading UDP packet from source: %s : %s\", addr, err)\n\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\tp.wg.Done()\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Debugf(\"received %d data from %s\", n, addr)\n\n\t\tn, err = targetConn.Write(b[:n])\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"error forwarding UDP packet to target: %s : %s\", targetAddr, err)\n\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\tp.wg.Done()\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Debugf(\"sent %d data to %s\", n, targetAddr)\n\t}\n}\n\nfunc (p *PortProxy) handleTCP(portBindings []nat.PortBinding, remove bool) {\n\tfor _, portBinding := range portBindings {\n\t\tport, err := nat.ParsePort(portBinding.HostPort)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"parsing port error: %s\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif remove {\n\t\t\tp.listenerMutex.Lock()\n\t\t\tif listener, exist := p.activeListeners[port]; exist {\n\t\t\t\tlogrus.Debugf(\"closing listener for port: %d\", port)\n\t\t\t\tif err := listener.Close(); err != nil {\n\t\t\t\t\tlogrus.Errorf(\"error closing listener for port [%s]: %s\", portBinding.HostPort, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tdelete(p.activeListeners, port)\n\t\t\tp.listenerMutex.Unlock()\n\t\t\tcontinue\n\t\t}\n\t\taddr := net.JoinHostPort(portBinding.HostIP, portBinding.HostPort)\n\t\tl, err := p.listenerConfig.Listen(p.ctx, \"tcp\", addr)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"failed creating listener for published port [%s]: %s\", portBinding.HostPort, err)\n\t\t\tcontinue\n\t\t}\n\t\tp.listenerMutex.Lock()\n\t\tp.activeListeners[port] = l\n\t\tp.listenerMutex.Unlock()\n\t\tlogrus.Debugf(\"created listener for: %s\", addr)\n\t\tgo p.acceptTraffic(l, portBinding.HostPort)\n\t}\n}\n\nfunc (p *PortProxy) acceptTraffic(listener net.Listener, port string) {\n\tforwardAddr := net.JoinHostPort(p.config.UpstreamAddress, port)\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\t// Check if the error is due to listener being closed\n\t\t\tif errors.Is(err, net.ErrClosed) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlogrus.Errorf(\"port proxy listener failed to accept: %s\", err)\n\t\t\tcontinue\n\t\t}\n\t\tlogrus.Debugf(\"port proxy accepted TCP connection from %s\", conn.RemoteAddr())\n\t\tp.wg.Add(1)\n\n\t\tgo func(conn net.Conn) {\n\t\t\tdefer p.wg.Done()\n\t\t\tdefer conn.Close()\n\t\t\tutils.Pipe(p.ctx, conn, forwardAddr)\n\t\t}(conn)\n\t}\n}\n\nfunc (p *PortProxy) Close() error {\n\t// Close all the active listeners\n\tp.cleanupListeners()\n\n\t// Close all active UDP connections\n\tp.cleanupUDPConns()\n\n\t// Close the listener first to prevent new connections.\n\terr := p.listener.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Signal the quit channel to stop accepting new connections.\n\tclose(p.quit)\n\n\t// Wait for all pending connections to finish.\n\tp.wg.Wait()\n\n\treturn nil\n}\n\nfunc (p *PortProxy) cleanupListeners() {\n\tp.listenerMutex.Lock()\n\tdefer p.listenerMutex.Unlock()\n\tfor _, l := range p.activeListeners {\n\t\t_ = l.Close()\n\t}\n}\n\nfunc (p *PortProxy) cleanupUDPConns() {\n\tp.udpConnMutex.Lock()\n\tdefer p.udpConnMutex.Unlock()\n\tfor _, c := range p.activeUDPConns {\n\t\t_ = c.Close()\n\t}\n}\n"
  },
  {
    "path": "src/go/networking/pkg/portproxy/server_test.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage portproxy_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"syscall\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/docker/go-connections/nat\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/net/nettest\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/guestagent/pkg/types\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/networking/pkg/portproxy\"\n)\n\nfunc TestNewPortProxyUDP(t *testing.T) {\n\ttestServerIP, err := availableIP()\n\trequire.NoError(t, err, \"cannot continue with the test since there are no available IP addresses\")\n\n\tremoteAddr := net.JoinHostPort(testServerIP, \"0\")\n\ttargetAddr, err := net.ResolveUDPAddr(\"udp\", remoteAddr)\n\trequire.NoError(t, err)\n\ttargetConn, err := net.ListenUDP(\"udp\", targetAddr)\n\trequire.NoError(t, err)\n\n\tt.Logf(\"created the following UDP target listener: %s\", targetConn.LocalAddr().String())\n\n\tlocalListener, err := nettest.NewLocalListener(\"unix\")\n\trequire.NoError(t, err)\n\tdefer localListener.Close()\n\n\tproxyConfig := &portproxy.ProxyConfig{\n\t\tUpstreamAddress: testServerIP,\n\t\tUDPBufferSize:   1024,\n\t}\n\tportProxy := portproxy.NewPortProxy(t.Context(), localListener, proxyConfig)\n\tgo portProxy.Start()\n\n\t_, testPort, err := net.SplitHostPort(targetConn.LocalAddr().String())\n\trequire.NoError(t, err)\n\n\tport, err := nat.NewPort(\"udp\", testPort)\n\trequire.NoError(t, err)\n\n\tportMapping := types.PortMapping{\n\t\tRemove: false,\n\t\tPorts: nat.PortMap{\n\t\t\tport: []nat.PortBinding{\n\t\t\t\t{\n\t\t\t\t\tHostIP:   \"127.0.0.1\",\n\t\t\t\t\tHostPort: testPort,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tt.Logf(\"sending the following portMapping to portProxy: %+v\", portMapping)\n\terr = marshalAndSend(t.Context(), localListener, portMapping)\n\trequire.NoError(t, err)\n\n\t// indicate when UDP mappings are ready\n\tfor len(portProxy.UDPPortMappings()) == 0 {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\n\tt.Log(\"UDP port mappings are set up\")\n\n\tlocalAddr := net.JoinHostPort(\"127.0.0.1\", testPort)\n\tsourceAddr, err := net.ResolveUDPAddr(\"udp\", localAddr)\n\trequire.NoError(t, err)\n\tsourceConn, err := net.DialUDP(\"udp\", nil, sourceAddr)\n\trequire.NoError(t, err)\n\tt.Logf(\"dialing in to the following UDP connection: %s\", localAddr)\n\n\texpectedString := \"this is what we expect\"\n\t_, err = sourceConn.Write([]byte(expectedString))\n\trequire.NoError(t, err)\n\n\ttargetConn.SetDeadline(time.Now().Add(time.Second * 5))\n\n\tb := make([]byte, len(expectedString))\n\tn, _, err := targetConn.ReadFromUDP(b)\n\trequire.NoError(t, err)\n\trequire.Equal(t, n, len(expectedString))\n\trequire.Equal(t, string(b), expectedString)\n\n\ttargetConn.Close()\n\tsourceConn.Close()\n\tportProxy.Close()\n}\n\nfunc TestNewPortProxyTCP(t *testing.T) {\n\texpectedResponse := \"called the upstream server\"\n\n\ttestServerIP, err := availableIP()\n\trequire.NoError(t, err, \"cannot continue with the test since there are no available IP addresses\")\n\n\tlistenerConfig := &net.ListenConfig{}\n\tlistener, err := listenerConfig.Listen(t.Context(), \"tcp\", fmt.Sprintf(\"%s:\", testServerIP))\n\trequire.NoError(t, err)\n\tdefer listener.Close()\n\n\ttestServer := http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfmt.Fprint(w, expectedResponse)\n\t\t}),\n\t}\n\tdefer testServer.Close()\n\ttestServer.SetKeepAlivesEnabled(false)\n\tgo testServer.Serve(listener)\n\n\t_, testPort, err := net.SplitHostPort(listener.Addr().String())\n\trequire.NoError(t, err)\n\n\tlocalListener, err := nettest.NewLocalListener(\"unix\")\n\trequire.NoError(t, err)\n\tdefer localListener.Close()\n\n\tproxyConfig := &portproxy.ProxyConfig{\n\t\tUpstreamAddress: testServerIP,\n\t}\n\tportProxy := portproxy.NewPortProxy(t.Context(), localListener, proxyConfig)\n\tgo portProxy.Start()\n\n\tgetURL := fmt.Sprintf(\"http://localhost:%s\", testPort)\n\tresp, err := httpGetRequest(t.Context(), getURL)\n\trequire.ErrorIsf(t, err, syscall.ECONNREFUSED, \"no listener should be available for port: %s\", testPort)\n\tif resp != nil {\n\t\tresp.Body.Close()\n\t}\n\n\tport, err := nat.NewPort(\"tcp\", testPort)\n\trequire.NoError(t, err)\n\n\tportMapping := types.PortMapping{\n\t\tRemove: false,\n\t\tPorts: nat.PortMap{\n\t\t\tport: []nat.PortBinding{\n\t\t\t\t{\n\t\t\t\t\tHostIP:   \"127.0.0.1\",\n\t\t\t\t\tHostPort: testPort,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tt.Logf(\"sending the following portMapping to portProxy: %+v\", portMapping)\n\terr = marshalAndSend(t.Context(), localListener, portMapping)\n\trequire.NoError(t, err)\n\n\tresp, err = httpGetRequest(t.Context(), getURL)\n\trequire.NoError(t, err)\n\trequire.Equal(t, resp.StatusCode, http.StatusOK)\n\tdefer resp.Body.Close()\n\tbodyBytes, err := io.ReadAll(resp.Body)\n\trequire.NoError(t, err)\n\trequire.Equal(t, string(bodyBytes), expectedResponse)\n\n\tportMapping = types.PortMapping{\n\t\tRemove: true,\n\t\tPorts: nat.PortMap{\n\t\t\tport: []nat.PortBinding{\n\t\t\t\t{\n\t\t\t\t\tHostIP:   \"127.0.0.1\",\n\t\t\t\t\tHostPort: testPort,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = marshalAndSend(t.Context(), localListener, portMapping)\n\trequire.NoError(t, err)\n\n\tresp, err = httpGetRequest(t.Context(), getURL)\n\trequire.Errorf(t, err, \"the listener for port: %s should already be closed\", testPort)\n\trequire.ErrorIs(t, err, syscall.ECONNREFUSED)\n\tif resp != nil {\n\t\tresp.Body.Close()\n\t}\n\n\ttestServer.Close()\n\tportProxy.Close()\n}\n\nfunc httpGetRequest(ctx context.Context, url string) (*http.Response, error) {\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn resp, nil\n}\n\nfunc marshalAndSend(ctx context.Context, listener net.Listener, portMapping types.PortMapping) error {\n\tb, err := json.Marshal(portMapping)\n\tif err != nil {\n\t\treturn err\n\t}\n\ttestDialer := net.Dialer{\n\t\tTimeout: 5 * time.Second,\n\t}\n\tc, err := testDialer.DialContext(ctx, listener.Addr().Network(), listener.Addr().String())\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = c.Write(b)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.Close()\n}\n\nfunc availableIP() (string, error) {\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tfor _, iface := range ifaces {\n\t\tif iface.Flags&net.FlagUp == 0 {\n\t\t\tcontinue // interface down\n\t\t}\n\t\tif iface.Flags&net.FlagLoopback != 0 {\n\t\t\tcontinue // loopback interface\n\t\t}\n\t\taddrs, err := iface.Addrs()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tfor _, addr := range addrs {\n\t\t\tvar ip net.IP\n\t\t\tswitch v := addr.(type) {\n\t\t\tcase *net.IPNet:\n\t\t\t\tip = v.IP\n\t\t\tcase *net.IPAddr:\n\t\t\t\tip = v.IP\n\t\t\t}\n\t\t\tif ip == nil || ip.IsLoopback() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tip = ip.To4()\n\t\t\tif ip == nil {\n\t\t\t\tcontinue // not an ipv4 address\n\t\t\t}\n\t\t\treturn ip.String(), nil\n\t\t}\n\t}\n\treturn \"\", errors.New(\"are you connected to the network?\")\n}\n"
  },
  {
    "path": "src/go/networking/pkg/utils/pipe.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage utils\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nfunc Pipe(ctx context.Context, conn net.Conn, upstreamAddr string) {\n\tdialer := net.Dialer{\n\t\tTimeout: 5 * time.Second,\n\t}\n\tupstream, err := dialer.DialContext(ctx, \"tcp\", upstreamAddr)\n\tif err != nil {\n\t\tlogrus.Errorf(\"Failed to dial upstream %s: %s\", upstreamAddr, err)\n\t\treturn\n\t}\n\tgo func() {\n\t\tif _, err := io.Copy(upstream, conn); err != nil {\n\t\t\tlogrus.Debugf(\"Error copying to upstream: %s\", err)\n\t\t}\n\t\tif err = upstream.Close(); err != nil {\n\t\t\tlogrus.Debugf(\"error closing connection while writing to upstream: %s\", err)\n\t\t}\n\t}()\n\n\tif _, err := io.Copy(conn, upstream); err != nil {\n\t\tlogrus.Debugf(\"Error copying from upstream: %s\", err)\n\t}\n\tif err = upstream.Close(); err != nil {\n\t\tlogrus.Debugf(\"error closing connection: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "src/go/networking/pkg/vsock/conn_windows.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage vsock\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/Microsoft/go-winio\"\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n)\n\nfunc Listen(vmGUID hvsock.GUID, vsockPort uint32) (net.Listener, error) {\n\tsvcPort, err := hvsock.GUIDFromString(winio.VsockServiceID(vsockPort).String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Listen, could not parse Hyper-v service GUID: %v\", err)\n\t}\n\n\taddr := hvsock.Addr{\n\t\tVMID:      vmGUID,\n\t\tServiceID: svcPort,\n\t}\n\n\treturn hvsock.Listen(addr)\n}\n"
  },
  {
    "path": "src/go/networking/pkg/vsock/constants.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage vsock\n\nconst (\n\tSignaturePhrase = \"github.com/rancher-sandbox/rancher-desktop/src/go/networking\"\n\tReadySignal     = \"READY\"\n)\n"
  },
  {
    "path": "src/go/networking/pkg/vsock/handshake_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n    http://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage vsock\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/Microsoft/go-winio\"\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows/registry\"\n)\n\n// GetVMGUID retrieves the GUID for a correct hyper-v VM (most likely WSL).\n// It performs a handshake with a running vsock-peer in the WSL distro\n// to make sure we establish the AF_VSOCK connection with a right VM.\nfunc GetVMGUID(ctx context.Context, signature string, handshakePort uint32, timeout <-chan time.Time) (hvsock.GUID, error) {\n\tkey, err := registry.OpenKey(\n\t\tregistry.LOCAL_MACHINE,\n\t\t`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\HostComputeService\\VolatileStore\\ComputeSystem`,\n\t\tregistry.ENUMERATE_SUB_KEYS)\n\tif err != nil {\n\t\treturn hvsock.GUIDZero, fmt.Errorf(\"could not open registry key, is WSL VM running? %v\", err)\n\t}\n\n\tnames, err := key.ReadSubKeyNames(0)\n\tif err != nil {\n\t\treturn hvsock.GUIDZero, fmt.Errorf(\"machine IDs cannot be read in registry: %v\", err)\n\t}\n\tif len(names) == 0 {\n\t\treturn hvsock.GUIDZero, errors.New(\"no running WSL VM found\")\n\t}\n\n\tkey.Close()\n\n\tctx, cancel := context.WithCancel(ctx)\n\tfound := make(chan hvsock.GUID, len(names))\n\n\tfor _, name := range names {\n\t\tvmGUID, err := hvsock.GUIDFromString(name)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"invalid VM name: [%s], err: %v\", name, err)\n\t\t\tcontinue\n\t\t}\n\t\tgo handshake(ctx, vmGUID, handshakePort, signature, found)\n\t}\n\treturn tryFindGUID(cancel, found, timeout)\n}\n\n// GetVsockConnection establishes a new AF_VSOCK connection with\n// the provided VM GUID and port, the caller is responsible for closing the connection\nfunc GetVsockConnection(vmGUID hvsock.GUID, port uint32) (net.Conn, error) {\n\tsvcPort, err := hvsock.GUIDFromString(winio.VsockServiceID(port).String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\taddr := hvsock.Addr{\n\t\tVMID:      vmGUID,\n\t\tServiceID: svcPort,\n\t}\n\n\treturn hvsock.Dial(addr)\n}\n\n// handshake with the Hyper-V VM by verifying the fixed signature over AF_VSOCK once\n// per second, in order to identify the VM running the WSL distro.\nfunc handshake(ctx context.Context, vmGUID hvsock.GUID, peerHandshakePort uint32, signaturePhrase string, found chan<- hvsock.GUID) {\n\tattemptInterval := time.NewTicker(time.Second * 1)\n\tattempt := 0\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tlogrus.Infof(\"attempt to handshake with [%s], goroutine is terminated\", vmGUID.String())\n\t\t\treturn\n\t\tcase <-attemptInterval.C:\n\t\t\t// Spawn a goroutine here to ensure we don't\n\t\t\t// get stuck on a timeout from hvsock.Dial\n\t\t\tgo func() {\n\t\t\t\tconn, err := GetVsockConnection(vmGUID, peerHandshakePort)\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t\tif err != nil {\n\t\t\t\t\tattempt++\n\t\t\t\t\tlogrus.Debugf(\"handshake attempt[%v] to dial into VM [%s], looking for vsock-peer failed: %v\", attempt, vmGUID.String(), err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tsignature, err := readSignature(conn, signaturePhrase)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Errorf(\"handshake attempt to read the signature: %v\", err)\n\t\t\t\t}\n\t\t\t\tif err := conn.Close(); err != nil {\n\t\t\t\t\tlogrus.Errorf(\"handshake closing connection: %v\", err)\n\t\t\t\t}\n\t\t\t\tif signature == signaturePhrase {\n\t\t\t\t\tlogrus.Infof(\"successfully established a handshake with a peer: %s\", vmGUID.String())\n\t\t\t\t\tfound <- vmGUID\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tlogrus.Infof(\"handshake failed to match the signature phrase with a peer running in: %s\", vmGUID.String())\n\t\t\t}()\n\t\t}\n\t}\n}\n\n// tryFindGuid waits on a found channel to receive a GUID until\n// deadline of 10s is reached\nfunc tryFindGUID(cancel context.CancelFunc, found chan hvsock.GUID, timeout <-chan time.Time) (hvsock.GUID, error) {\n\tdefer cancel()\n\tfor {\n\t\tselect {\n\t\tcase vmGUID := <-found:\n\t\t\treturn vmGUID, nil\n\t\tcase <-timeout:\n\t\t\treturn hvsock.GUIDZero, errors.New(\"could not find vsock-peer process on any hyper-v VM(s)\")\n\t\t}\n\t}\n}\n\n// readSignature reads the signature that was received from the peer process\n// in the vm, and writes its own signature immediately after read. This\n// will allow the peer process to also confirm the host daemon.\nfunc readSignature(conn net.Conn, signaturePhrase string) (string, error) {\n\tsignature := make([]byte, len(signaturePhrase))\n\tif _, err := io.ReadFull(conn, signature); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(signature), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/README.md",
    "content": "# rdctl\n\nThis is the command-line interface (CLI) for Rancher Desktop.\n\n## Prerequisites\n\nThis tool depends on code generated during `yarn postinstall`.\n\n## Usage\n\nRun `rdctl --help` for usage information.\n\nMuch of `rdctl`'s functionality depends on the HTTP server running within Rancher Desktop,\nbut some functionality is available when it isn't running, in particular the\n`rdctl factory-reset` command, which can be used to remove files generated by Rancher Desktop,\nthose required for its functionality, and state files, such as a VM snapshot.\n"
  },
  {
    "path": "src/go/rdctl/cmd/api.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\nvar apiSettings struct {\n\tMethod    string\n\tInputFile string\n\tBody      string\n}\n\n// apiCmd represents the api command\nvar apiCmd = &cobra.Command{\n\tUse:   \"api\",\n\tShort: \"Run API endpoints directly\",\n\tLong: `Runs API endpoints directly.\nDefault method is PUT if a body or input file is specified, GET otherwise.\n\nTwo ways of specifying a body:\n1. --input FILE: For example, '--input .../rancher-desktop/settings.json'. Specify '-' for standard input.\n\n2. --body|-b string: For the 'PUT /settings' endpoint, this must be a valid JSON string.\n\nThe API is currently at version 1, but is still considered internal and experimental, and\nis subject to change without any advance notice.\n`,\n\tRunE: doAPICommand,\n}\n\nfunc init() {\n\trootCmd.AddCommand(apiCmd)\n\tapiCmd.Flags().StringVarP(&apiSettings.Method, \"method\", \"X\", \"\", \"method to use\")\n\tapiCmd.Flags().StringVarP(&apiSettings.InputFile, \"input\", \"\", \"\", \"file containing JSON payload to upload (- for standard input)\")\n\tapiCmd.Flags().StringVarP(&apiSettings.Body, \"body\", \"b\", \"\", \"string containing JSON payload to upload\")\n}\n\nfunc doAPICommand(cmd *cobra.Command, args []string) error {\n\tvar result []byte\n\tvar contents []byte\n\tvar err error\n\tvar errorPacket *client.APIError\n\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\n\tif len(args) == 0 || args[0] == \"\" {\n\t\treturn fmt.Errorf(\"api command: no endpoint specified\")\n\t}\n\tif len(args) > 1 {\n\t\treturn fmt.Errorf(\"api command: too many endpoints specified (%v); exactly one must be specified\", args)\n\t}\n\tendpoint := args[0]\n\tif endpoint != \"/\" && regexp.MustCompile(`^/v\\d+(?:/|$)`).FindString(endpoint) == \"\" {\n\t\tendpoint = fmt.Sprintf(\"/%s\", client.VersionCommand(client.APIVersion, endpoint))\n\t}\n\tif apiSettings.InputFile != \"\" && apiSettings.Body != \"\" {\n\t\treturn fmt.Errorf(\"api command: --body and --input options cannot both be specified\")\n\t}\n\t// No longer emit usage info on errors\n\tcmd.SilenceUsage = true\n\tif apiSettings.InputFile != \"\" {\n\t\tif apiSettings.Method == \"\" {\n\t\t\tapiSettings.Method = \"PUT\"\n\t\t}\n\t\tif apiSettings.InputFile == \"-\" {\n\t\t\tcontents, err = io.ReadAll(os.Stdin)\n\t\t} else {\n\t\t\tcontents, err = os.ReadFile(apiSettings.InputFile)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tmethod := apiSettings.Method\n\t\tpayload := bytes.NewBuffer(contents)\n\t\tresult, errorPacket, err = client.ProcessRequestForAPI(rdClient.DoRequestWithPayload(cmd.Context(), method, endpoint, payload))\n\t} else if apiSettings.Body != \"\" {\n\t\tif apiSettings.Method == \"\" {\n\t\t\tapiSettings.Method = \"PUT\"\n\t\t}\n\t\tmethod := apiSettings.Method\n\t\tpayload := bytes.NewBufferString(apiSettings.Body)\n\t\tresult, errorPacket, err = client.ProcessRequestForAPI(rdClient.DoRequestWithPayload(cmd.Context(), method, endpoint, payload))\n\t} else {\n\t\tif apiSettings.Method == \"\" {\n\t\t\tapiSettings.Method = \"GET\"\n\t\t}\n\t\tresult, errorPacket, err = client.ProcessRequestForAPI(rdClient.DoRequest(cmd.Context(), apiSettings.Method, endpoint))\n\t}\n\treturn displayAPICallResult(result, errorPacket, err)\n}\n\nfunc displayAPICallResult(result []byte, errorPacket *client.APIError, err error) error {\n\tif err != nil {\n\t\treturn err\n\t}\n\t// If we got an error packet from the server:\n\t//   write the packet to stdout\n\t//   write the result body, if there is one to stderr\n\t//   exit status 1 (do not have cobra deal with the error, because it writes it to stderr\n\t// Otherwise:\n\t//   Write the result body to stdout\n\t//   Return nil error (=> exit status 0)\n\tif len(result) > 0 {\n\t\tif errorPacket == nil {\n\t\t\tfmt.Fprintln(os.Stdout, string(result))\n\t\t} else {\n\t\t\tfmt.Fprintln(os.Stderr, string(result))\n\t\t}\n\t}\n\tif errorPacket == nil {\n\t\treturn nil\n\t}\n\terrorPacketBytes, err := json.Marshal(*errorPacket)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error converting error message info: %w\", err)\n\t}\n\tfmt.Fprintln(os.Stdout, string(errorPacketBytes))\n\tos.Exit(1)\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/createProfile.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/plist\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/reg\"\n)\n\nconst plistFormat = \"plist\"\nconst regFormat = \"reg\"\nconst defaultsType = \"defaults\"\nconst lockedType = \"locked\"\n\n// The distinction between 'system' and 'user' is only needed for registry output\n// because it gets written into the generated .reg data, while on macOS the distinction\n// is based on which directory the generated file is placed in (and what name it's given).\nconst systemHive = \"system\"\nconst userHive = \"user\"\n\nvar outputSettingsFlags struct {\n\tFormat              string\n\tRegistryHive        string // Should be USER or SYSTEM!\n\tRegistryProfileType string\n}\nvar InputFile string\nvar JSONBody string\nvar UseCurrentSettings bool\n\n// createProfileCmd represents the createProfile command\nvar createProfileCmd = &cobra.Command{\n\tUse:   \"create-profile\",\n\tShort: \"Generate a deployment profile in either macOS plist or Windows registry format\",\n\tLong: `Use this to generate deployment profiles for Rancher Desktop settings.\nYou can either convert the current listings in operation, or\nspecify a JSON snippet, and convert that to the desired target.\nmacOS plist files can be placed in the appropriate directory, while \".reg\" files\ncan be imported into the Windows registry using the \"eg import FILE\" command.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tresult, err := createProfile(cmd.Context())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Println(result)\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(createProfileCmd)\n\tcreateProfileCmd.Flags().StringVar(&outputSettingsFlags.Format, \"output\", \"\", fmt.Sprintf(\"output format: %s|%s\", plistFormat, regFormat))\n\tcreateProfileCmd.Flags().StringVar(&outputSettingsFlags.RegistryHive, \"hive\", \"\", fmt.Sprintf(`registry hive: %s|%s (default %q)`, reg.HklmRegistryHive, reg.HkcuRegistryHive, reg.HklmRegistryHive))\n\tcreateProfileCmd.Flags().StringVar(&outputSettingsFlags.RegistryProfileType, \"type\", \"\", fmt.Sprintf(`registry section: %s|%s (default %q)`, defaultsType, lockedType, defaultsType))\n\tcreateProfileCmd.Flags().StringVar(&InputFile, \"input\", \"\", \"File containing a JSON document (- for standard input)\")\n\tcreateProfileCmd.Flags().StringVarP(&JSONBody, \"body\", \"b\", \"\", \"Command-line option containing a JSON document\")\n\tcreateProfileCmd.Flags().BoolVar(&UseCurrentSettings, \"from-settings\", false, \"Use current settings\")\n}\n\nfunc createProfile(ctx context.Context) (string, error) {\n\terr := validateProfileFormatFlags()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar output []byte\n\n\tif JSONBody != \"\" {\n\t\toutput = []byte(JSONBody)\n\t} else if InputFile != \"\" {\n\t\tif InputFile == \"-\" {\n\t\t\toutput, err = io.ReadAll(os.Stdin)\n\t\t} else {\n\t\t\toutput, err = os.ReadFile(InputFile)\n\t\t}\n\t} else {\n\t\tif !UseCurrentSettings {\n\t\t\t// This should have been caught in validateProfileFormatFlags\n\t\t\treturn \"\", fmt.Errorf(`no input format specified: must specify exactly one input format of \"--input FILE|-\", \"--body|-b STRING\", or \"--from-settings\"`)\n\t\t}\n\t\tconnectionInfo, err2 := config.GetConnectionInfo(false)\n\t\tif err2 != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get connection info: %w\", err2)\n\t\t}\n\t\trdClient := client.NewRDClient(connectionInfo)\n\t\tcommand := client.VersionCommand(\"\", \"settings\")\n\t\toutput, err = client.ProcessRequestForUtility(rdClient.DoRequest(ctx, http.MethodGet, command))\n\t}\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tswitch outputSettingsFlags.Format {\n\tcase regFormat:\n\t\tlines, err := reg.JSONToReg(outputSettingsFlags.RegistryHive, outputSettingsFlags.RegistryProfileType, string(output))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn strings.Join(lines, \"\\n\"), nil\n\tcase plistFormat:\n\t\treturn plist.JSONToPlist(string(output))\n\t}\n\treturn \"\", fmt.Errorf(`internal error: expecting an output format of %q or %q, got %q`, regFormat, plistFormat, outputSettingsFlags.Format)\n}\n\nfunc validateProfileFormatFlags() error {\n\tif outputSettingsFlags.Format == \"\" {\n\t\treturn fmt.Errorf(`an \"--output FORMAT\" option of either %q or %q must be specified`, plistFormat, regFormat)\n\t}\n\tif outputSettingsFlags.Format != plistFormat && outputSettingsFlags.Format != regFormat {\n\t\treturn fmt.Errorf(`received unrecognized \"--output FORMAT\" option of %q; %q or %q must be specified`, outputSettingsFlags.Format, plistFormat, regFormat)\n\t}\n\tif InputFile == \"\" && JSONBody == \"\" && !UseCurrentSettings {\n\t\treturn fmt.Errorf(`no input format specified: must specify exactly one input format of \"--input FILE|-\", \"--body|-b STRING\", or \"--from-settings\"`)\n\t}\n\tif (InputFile != \"\" && (JSONBody != \"\" || UseCurrentSettings)) || (JSONBody != \"\" && UseCurrentSettings) {\n\t\treturn fmt.Errorf(`too many input formats specified: must specify exactly one input format of \"--input FILE|-\", \"--body|-b STRING\", or \"--from-settings\"`)\n\t}\n\n\tif outputSettingsFlags.Format == plistFormat {\n\t\tif outputSettingsFlags.RegistryHive != \"\" || outputSettingsFlags.RegistryProfileType != \"\" {\n\t\t\treturn fmt.Errorf(`registry hive and type can't be specified with \"plist\"`)\n\t\t}\n\t\treturn nil\n\t}\n\n\tswitch strings.ToLower(outputSettingsFlags.RegistryHive) {\n\tcase reg.HklmRegistryHive, reg.HkcuRegistryHive:\n\t\toutputSettingsFlags.RegistryHive = strings.ToLower(outputSettingsFlags.RegistryHive)\n\tcase \"\":\n\t\toutputSettingsFlags.RegistryHive = reg.HklmRegistryHive\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid registry hive of %q specified, must be %q or %q\", outputSettingsFlags.RegistryHive, systemHive, userHive)\n\t}\n\tswitch strings.ToLower(outputSettingsFlags.RegistryProfileType) {\n\tcase defaultsType, lockedType:\n\t\toutputSettingsFlags.RegistryProfileType = strings.ToLower(outputSettingsFlags.RegistryProfileType)\n\tcase \"\":\n\t\toutputSettingsFlags.RegistryProfileType = defaultsType\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid registry type of %q specified, must be %q or %q\", outputSettingsFlags.RegistryProfileType, defaultsType, lockedType)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/enum.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport \"fmt\"\n\n// enumValue describes an enumeration for use with github.com/spf13/pflag\ntype enumValue struct {\n\tallowed []string // Allowed values\n\tval     string   // Current value\n}\n\nfunc (v *enumValue) String() string {\n\treturn v.val\n}\n\nfunc (v *enumValue) Set(newVal string) error {\n\tfor _, candidate := range v.allowed {\n\t\tif candidate == newVal {\n\t\t\tv.val = candidate\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"value %q is not one of the allowed values: %+v\", newVal, v.allowed)\n}\n\nfunc (v *enumValue) Type() string {\n\treturn \"enum\"\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/extension.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// extensionCmd represents the extension command\nvar extensionCmd = &cobra.Command{\n\tShort: \"Manage extensions\",\n\tLong: `rdctl extension - manage installed extensions\n`,\n\tUse: \"extension [install | uninstall | list] [options...]\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn fmt.Errorf(\"no subcommand given.\\n\\nUsage: rdctl %s\", cmd.Use)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(extensionCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/extensionInstall.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\n// installCmd represents the 'rdctl extensions install' command\nvar installCmd = &cobra.Command{\n\tUse:   \"install\",\n\tShort: \"Install an RDX extension\",\n\tLong: `rdctl extension install [--force] <image-id>\n--force: avoid any interactivity.\nThe <image-id> is an image reference, e.g. splatform/epinio-docker-desktop:latest (the tag is optional).`,\n\tArgs: cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn installExtension(cmd.Context(), args)\n\t},\n}\n\nfunc init() {\n\textensionCmd.AddCommand(installCmd)\n}\n\nfunc installExtension(ctx context.Context, args []string) error {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\timageID := args[0]\n\tendpoint := fmt.Sprintf(\"/%s/extensions/install?id=%s\", client.APIVersion, imageID)\n\t// https://stackoverflow.com/questions/20847357/golang-http-client-always-escaped-the-url\n\t// Looks like http.NewRequest(method, url) escapes the URL\n\n\tresult, errorPacket, err := client.ProcessRequestForAPI(rdClient.DoRequest(ctx, http.MethodPost, endpoint))\n\tif errorPacket != nil || err != nil {\n\t\treturn displayAPICallResult(result, errorPacket, err)\n\t}\n\tmsg := \"no output from server\"\n\tif result != nil {\n\t\tmsg = string(result)\n\t}\n\tfmt.Printf(\"Installing image %s: %s\\n\", imageID, msg)\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/extensionList.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\n// listCmd represents the list command\nvar listCmd = &cobra.Command{\n\tUse:     \"list\",\n\tAliases: []string{\"ls\"},\n\tShort:   \"List currently installed images\",\n\tLong:    `List currently installed images.`,\n\tArgs:    cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn listExtensions(cmd.Context())\n\t},\n}\n\nfunc init() {\n\textensionCmd.AddCommand(listCmd)\n}\n\nfunc listExtensions(ctx context.Context) error {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\tendpoint := fmt.Sprintf(\"/%s/extensions\", client.APIVersion)\n\tresult, errorPacket, err := client.ProcessRequestForAPI(rdClient.DoRequest(ctx, http.MethodGet, endpoint))\n\tif errorPacket != nil || err != nil {\n\t\treturn displayAPICallResult([]byte{}, errorPacket, err)\n\t}\n\textensionList := map[string]struct {\n\t\tVersion string `json:\"version\"`\n\t}{}\n\terr = json.Unmarshal(result, &extensionList)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal extension list API response: %w\", err)\n\t}\n\tif len(extensionList) == 0 {\n\t\tfmt.Println(\"No extensions are installed.\")\n\t\treturn nil\n\t}\n\textensionIDs := make([]string, 0, len(extensionList))\n\tfor id, info := range extensionList {\n\t\textensionIDs = append(extensionIDs, fmt.Sprintf(\"%s:%s\", id, info.Version))\n\t}\n\tsort.Slice(extensionIDs, func(i, j int) bool { return strings.ToLower(extensionIDs[i]) < strings.ToLower(extensionIDs[j]) })\n\n\tfmt.Print(\"Extension IDs\\n\\n\")\n\tfor _, extensionID := range extensionIDs {\n\t\tfmt.Println(extensionID)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/extensionUninstall.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\nvar uninstallCmd = &cobra.Command{\n\tUse:   \"uninstall\",\n\tShort: \"Uninstall an RDX extension\",\n\tLong: `rdctl extension uninstall <image-id>\nThe <image-id> is an image reference, e.g. splatform/epinio-docker-desktop:latest (the tag is optional).`,\n\tArgs: cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn uninstallExtension(cmd.Context(), args)\n\t},\n}\n\nfunc init() {\n\textensionCmd.AddCommand(uninstallCmd)\n}\n\nfunc uninstallExtension(ctx context.Context, args []string) error {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\timageID := args[0]\n\tendpoint := fmt.Sprintf(\"/%s/extensions/uninstall?id=%s\", client.APIVersion, imageID)\n\tresult, errorPacket, err := client.ProcessRequestForAPI(rdClient.DoRequest(ctx, http.MethodPost, endpoint))\n\tif errorPacket != nil || err != nil {\n\t\treturn displayAPICallResult(result, errorPacket, err)\n\t}\n\tmsg := \"no output from server\"\n\tif result != nil {\n\t\tmsg = string(result)\n\t}\n\tfmt.Printf(\"Uninstalling image %s: %s\\n\", imageID, msg)\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/factoryReset.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\nvar removeKubernetesCache bool\n\n// Note that this command supports a `--remove-kubernetes-cache` flag,\n// but the server takes an optional flag meaning the opposite (as per issues\n// https://github.com/rancher-sandbox/rancher-desktop/issues/1701 and\n// https://github.com/rancher-sandbox/rancher-desktop/issues/2408)\n\nvar factoryResetCmd = &cobra.Command{\n\tUse:    \"factory-reset\",\n\tHidden: true, // Hidden for backwards compatibility, use 'rdctl reset --factory' instead\n\tShort:  \"Clear all the Rancher Desktop state and shut it down.\",\n\tLong: `Clear all the Rancher Desktop state and shut it down.\nUse the --remove-kubernetes-cache=BOOLEAN flag to also remove the cached Kubernetes images.`,\n\tDeprecated: \"Use 'rdctl reset --factory' instead.\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.SilenceUsage = true\n\t\treturn performFactoryReset(cmd.Context(), removeKubernetesCache)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(factoryResetCmd)\n\tfactoryResetCmd.Flags().BoolVar(&removeKubernetesCache, \"remove-kubernetes-cache\", false, \"If specified, also removes the cached Kubernetes images.\")\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/info.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/command\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/info\"\n)\n\nvar infoSettings struct {\n\tField  string\n\tOutput string\n}\n\n// infoCmd represents the `rdctl info` command\nvar infoCmd = &cobra.Command{\n\tUse:   \"info\",\n\tShort: \"Return information about Rancher Desktop\",\n\tLong:  infoLongHelp(),\n\tRunE:  doInfoCommand,\n}\n\nfunc init() {\n\trootCmd.AddCommand(infoCmd)\n\tinfoCmd.Flags().StringVarP(&infoSettings.Field, \"field\", \"f\", \"\", \"return only a specific field\")\n\tinfoCmd.Flags().VarP(&enumValue{\n\t\tval:     \"text\",\n\t\tallowed: []string{\"text\", \"json\"},\n\t}, \"output\", \"o\", \"output format\")\n}\n\n// Generates help text for each field available.\nfunc infoLongHelp() string {\n\tvar builder strings.Builder\n\n\t_, _ = builder.WriteString(\"Returns information about Rancher Desktop.  The command returns all\\n\")\n\t_, _ = builder.WriteString(\"fields by default, but a single field can be selected with '--field'.\\n\")\n\t_, _ = builder.WriteString(\"\\n\")\n\t_, _ = builder.WriteString(\"The available fields are:\\n\")\n\n\ttyp := reflect.TypeFor[info.Info]()\n\tfor i := range typ.NumField() {\n\t\tfield := typ.Field(i)\n\t\thelpText := field.Tag.Get(\"help\")\n\t\tif helpText == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tfieldName := strings.SplitN(field.Tag.Get(\"json\"), \",\", 2)[0]\n\t\t_, _ = fmt.Fprintf(&builder, \"  %-10s    %s\\n\", fieldName, helpText)\n\t}\n\treturn builder.String()\n}\n\nfunc doInfoCommand(cmd *cobra.Command, args []string) error {\n\tvar result info.Info\n\tvar rdClient client.RDClient\n\n\tctx := command.WithCommandName(cmd.Context(), cmd.CommandPath())\n\n\tif connectionInfo, err := config.GetConnectionInfo(false); err == nil {\n\t\trdClient = client.NewRDClient(connectionInfo)\n\t}\n\n\tif infoSettings.Field != \"\" {\n\t\thandler, ok := info.Handlers[infoSettings.Field]\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"unknown field %q\", infoSettings.Field)\n\t\t}\n\n\t\t// No longer emit usage info on errors\n\t\tcmd.SilenceUsage = true\n\n\t\tif err := handler(ctx, &result, rdClient); err != nil {\n\t\t\tvar fatalError command.FatalError\n\t\t\tif errors.As(err, &fatalError) {\n\t\t\t\tif fatalError.Error() != \"\" {\n\t\t\t\t\t_, _ = fmt.Fprintln(os.Stderr, fatalError)\n\t\t\t\t}\n\t\t\t\tos.Exit(fatalError.ExitCode())\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tvalue := reflect.ValueOf(result)\n\t\ttyp := value.Type()\n\t\tfor i := range typ.NumField() {\n\t\t\tfield := typ.Field(i)\n\t\t\ttag := strings.SplitN(field.Tag.Get(\"json\"), \",\", 2)[0]\n\t\t\tif tag == infoSettings.Field {\n\t\t\t\t_, err := fmt.Println(value.Field(i).Interface())\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\treturn fmt.Errorf(\"failed to find JSON field %q\", infoSettings.Field)\n\t}\n\n\t// No longer emit usage info on errors\n\tcmd.SilenceUsage = true\n\n\tfor _, handler := range info.Handlers {\n\t\tif err := handler(ctx, &result, rdClient); err != nil {\n\t\t\tvar fatalError command.FatalError\n\t\t\tif errors.As(err, &fatalError) {\n\t\t\t\tif fatalError.Error() != \"\" {\n\t\t\t\t\t_, _ = fmt.Fprintln(os.Stderr, fatalError)\n\t\t\t\t}\n\t\t\t\tos.Exit(fatalError.ExitCode())\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\n\tswitch cmd.Flags().Lookup(\"output\").Value.String() {\n\tcase \"json\":\n\t\tencoder := json.NewEncoder(os.Stdout)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\tif err := encoder.Encode(result); err != nil {\n\t\t\treturn err\n\t\t}\n\tdefault:\n\t\twriter := tabwriter.NewWriter(os.Stdout, 0, 4, 1, ' ', 0)\n\t\tvalue := reflect.ValueOf(result)\n\t\tfor i := range value.NumField() {\n\t\t\tfield := value.Type().Field(i)\n\t\t\tname, ok := field.Tag.Lookup(\"name\")\n\t\t\tif !ok {\n\t\t\t\tname = field.Name\n\t\t\t}\n\t\t\tif _, err := fmt.Fprintf(writer, \"%s:\\t%s\\n\", name, value.Field(i)); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\tif err := writer.Flush(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/internal.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// internalCmd represents the `rdctl internal` command, which is used for\n// native code.\nvar internalCmd = &cobra.Command{\n\tUse:    \"internal\",\n\tShort:  \"Rancher Desktop internal commands\",\n\tLong:   `rdctl internal provides commands for Rancher Desktop internal use`,\n\tHidden: true,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn fmt.Errorf(\"%q expects subcommands\", cmd.CommandPath())\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(internalCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/internalProcess.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n)\n\n// internalCmd represents the `rdctl internal process` command, which contains\n// commands for dealing with (host) processes.\nvar internalProcessCmd = &cobra.Command{\n\tUse:   \"process\",\n\tShort: \"Process-related subcommands\",\n\tLong:  `rdctl internal process contains subcommands dealing with processes.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn fmt.Errorf(\"%q expects subcommands\", cmd.CommandPath())\n\t},\n}\n\nfunc init() {\n\tinternalCmd.AddCommand(internalProcessCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/internalProcessWaitKill.go",
    "content": "//go:build unix\n\n/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd implements the rdctl commands\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\n// internalCmd represents the `rdctl internal process` command, which contains\n// commands for dealing with (host) processes.\nvar internalProcessWaitKillCmd = &cobra.Command{\n\tUse:   \"wait-kill\",\n\tShort: \"Wait for a process and then kill of the processes in its group.\",\n\tLong: `The 'rdctl internal process wait-kill' command waits for the specified process to\nexit, and once it does, terminates all processes within the same process group.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\tpid, err := cmd.Flags().GetInt(\"pid\")\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get process ID: %w\", err)\n\t\t}\n\t\treturn process.KillProcessGroup(pid, true)\n\t},\n}\n\nfunc init() {\n\tinternalProcessCmd.AddCommand(internalProcessWaitKillCmd)\n\tinternalProcessWaitKillCmd.Flags().Int(\"pid\", 0, \"process to wait for\")\n\t_ = internalProcessWaitKillCmd.MarkFlagRequired(\"pid\")\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/listSettings.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\n// listSettingsCmd represents the listSettings command\nvar listSettingsCmd = &cobra.Command{\n\tUse:   \"list-settings\",\n\tShort: \"Lists the current settings.\",\n\tLong:  `Lists the current settings in JSON format.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.SilenceUsage = true\n\t\tresult, err := getListSettings(cmd.Context())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Println(string(result))\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(listSettingsCmd)\n}\n\nfunc getListSettings(ctx context.Context) ([]byte, error) {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\tcommand := client.VersionCommand(\"\", \"settings\")\n\treturn client.ProcessRequestForUtility(rdClient.DoRequest(ctx, http.MethodGet, command))\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/paths.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\tp \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nvar pathsCmd = &cobra.Command{\n\tHidden: true,\n\tUse:    \"paths\",\n\tShort:  \"Print the paths to directories that Rancher Desktop uses\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tpaths, err := p.GetPaths()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to construct Paths: %w\", err)\n\t\t}\n\t\tencoder := json.NewEncoder(os.Stdout)\n\t\terr = encoder.Encode(paths)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to output paths: %w\", err)\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(pathsCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/reset.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/factoryreset\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/shutdown\"\n)\n\nvar (\n\tvmReset      bool\n\tk8sReset     bool\n\tcacheReset   bool\n\tfactoryReset bool\n)\n\nvar resetCmd = &cobra.Command{\n\tUse:   \"reset\",\n\tShort: \"Reset Rancher Desktop\",\n\tLong: `Delete parts of Rancher Desktop settings.\nSome reset options are combinations of others:\n\n  * --factory includes --vm and --k8s (but not --cache)\n  * --vm includes --k8s\n\nAt least one option must be specified.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.SilenceUsage = true\n\n\t\t// Check if any options are specified\n\t\tif !vmReset && !k8sReset && !cacheReset && !factoryReset {\n\t\t\treturn fmt.Errorf(\"no reset options specified. Use --help to see available options\")\n\t\t}\n\n\t\t// Handle factory reset (includes VM, K8s and possibly cache reset)\n\t\tif factoryReset {\n\t\t\treturn performFactoryReset(cmd.Context(), cacheReset)\n\t\t}\n\t\tif vmReset || k8sReset {\n\t\t\tresetType := \"wipe\"\n\t\t\tif !vmReset {\n\t\t\t\tresetType = \"fast\"\n\t\t\t}\n\t\t\tresult, err := doReset(cmd.Context(), resetType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfmt.Println(string(result))\n\t\t}\n\t\t// Handle cache reset if requested (and not already handled by factory reset)\n\t\tif cacheReset {\n\t\t\treturn factoryreset.DeleteCacheData()\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\n// resetPayload defines the payload structure for reset requests\ntype resetPayload struct {\n\tMode string `json:\"mode\"`\n}\n\n// performFactoryReset performs a factory reset with the given context and cache removal option\nfunc performFactoryReset(ctx context.Context, removeCache bool) error {\n\tpathsCfg, err := paths.GetPaths()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get paths: %w\", err)\n\t}\n\tcommonShutdownSettings.WaitForShutdown = false\n\t_, err = doShutdown(ctx, &commonShutdownSettings, shutdown.FactoryReset)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn factoryreset.DeleteData(ctx, pathsCfg, removeCache)\n}\n\n// doReset performs a reset with the specified mode\nfunc doReset(ctx context.Context, mode string) ([]byte, error) {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\tcommand := client.VersionCommand(\"\", \"k8s_reset\")\n\n\tpayload := resetPayload{\n\t\tMode: mode,\n\t}\n\tjsonBuffer, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn []byte{}, err\n\t}\n\tbuf := bytes.NewBuffer(jsonBuffer)\n\tresult, err := client.ProcessRequestForUtility(rdClient.DoRequestWithPayload(ctx, http.MethodPut, command, buf))\n\tif err != nil {\n\t\treturn result, err\n\t}\n\n\treturn result, err\n}\n\nfunc init() {\n\trootCmd.AddCommand(resetCmd)\n\tresetCmd.Flags().BoolVar(&vmReset, \"vm\", false, \"Delete VM and create a new one with current settings\")\n\tresetCmd.Flags().BoolVar(&k8sReset, \"k8s\", false, \"Delete deployed Kubernetes workloads\")\n\tresetCmd.Flags().BoolVar(&cacheReset, \"cache\", false, \"Delete cached Kubernetes images\")\n\tresetCmd.Flags().BoolVar(&factoryReset, \"factory\", false, \"Delete VM and show first-run dialog on next start\")\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/root.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd is the main package for this CLI\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:               \"rdctl\",\n\tShort:             \"A CLI for Rancher Desktop\",\n\tLong:              `The eventual goal of this CLI is to enable any UI-based operation to be done from the command-line as well.`,\n\tPersistentPreRunE: config.PersistentPreRunE,\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\tif len(os.Args) > 1 {\n\t\tmainCommand := os.Args[1]\n\t\tif mainCommand == \"-h\" || mainCommand == \"help\" || mainCommand == \"--help\" {\n\t\t\tif len(os.Args) > 2 {\n\t\t\t\tmainCommand = os.Args[2]\n\t\t\t}\n\t\t}\n\t\tif mainCommand == \"shell\" || mainCommand == \"version\" || mainCommand == \"completion\" {\n\t\t\treturn\n\t\t}\n\t}\n\tconfig.DefineGlobalFlags(rootCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/set.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n)\n\n// setCmd represents the set command\nvar setCmd = &cobra.Command{\n\tUse:   \"set\",\n\tShort: \"Update selected fields in the Rancher Desktop UI and restart the backend.\",\n\tLong:  `Update selected fields in the Rancher Desktop UI and restart the backend.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn doSetCommand(cmd)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(setCmd)\n\toptions.UpdateCommonStartAndSetCommands(setCmd)\n}\n\nfunc doSetCommand(cmd *cobra.Command) error {\n\tconnectionInfo, err := config.GetConnectionInfo(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get connection info: %w\", err)\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\n\tchangedSettings, err := options.UpdateFieldsForJSON(cmd.Flags())\n\tif err != nil {\n\t\tcmd.SilenceUsage = true\n\t\treturn err\n\t} else if changedSettings == nil {\n\t\treturn fmt.Errorf(\"%s command: no settings to change were given\", cmd.Name())\n\t}\n\tcmd.SilenceUsage = true\n\tjsonBuffer, err := json.Marshal(changedSettings)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcommand := client.VersionCommand(\"\", \"settings\")\n\tbuf := bytes.NewBuffer(jsonBuffer)\n\tresult, err := client.ProcessRequestForUtility(rdClient.DoRequestWithPayload(cmd.Context(), http.MethodPut, command, buf))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(result) > 0 {\n\t\tfmt.Printf(\"Status: %s.\\n\", string(result))\n\t} else {\n\t\tfmt.Printf(\"Operation successfully returned with no output.\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/setup.go",
    "content": "package cmd\n\nimport (\n\t\"errors\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart\"\n)\n\nvar setupSettings struct {\n\tAutoStart bool\n}\n\nvar setupCmd = &cobra.Command{\n\tHidden: true,\n\tUse:    \"setup\",\n\tShort:  \"Configure the system without modifying settings\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif cmd.Flags().Changed(\"auto-start\") {\n\t\t\treturn autostart.EnsureAutostart(cmd.Context(), setupSettings.AutoStart)\n\t\t}\n\t\treturn errors.New(\"no changes were specified\")\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(setupCmd)\n\tsetupCmd.Flags().BoolVar(&setupSettings.AutoStart, \"auto-start\", false, \"Whether to start Rancher Desktop at login\")\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/shell.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/command\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/shell\"\n)\n\n// shellCmd represents the shell command\nvar shellCmd = &cobra.Command{\n\tUse:   \"shell\",\n\tShort: \"Run an interactive shell or a command in a Rancher Desktop-managed VM\",\n\tLong: `Run an interactive shell or a command in a Rancher Desktop-managed VM. For example:\n\n> rdctl shell\n-- Runs an interactive shell\n> rdctl shell ls -CF /tmp\n-- Runs 'ls -CF' from /tmp on the VM\n> rdctl shell bash -c \"cd .. ; pwd\"\n-- Usual way of running multiple statements on a single call\n`,\n\tDisableFlagParsing: true,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// Do manual flag parsing looking to see if we should give help instead.\n\t\tif len(args) > 0 && (args[0] == \"-h\" || args[0] == \"--help\") {\n\t\t\treturn cmd.Help()\n\t\t}\n\t\treturn doShellCommand(cmd, args)\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(shellCmd)\n}\n\nfunc doShellCommand(cmd *cobra.Command, args []string) error {\n\tcmd.SilenceUsage = true\n\n\tctx := command.WithCommandName(cmd.Context(), cmd.CommandPath())\n\tshellCommand, err := shell.SpawnCommand(ctx, args...)\n\tif err != nil {\n\t\tvar fatalError command.FatalError\n\t\tif errors.As(err, &fatalError) {\n\t\t\tif fatalError.Error() != \"\" {\n\t\t\t\t_, _ = fmt.Fprintln(os.Stderr, fatalError)\n\t\t\t}\n\t\t\tos.Exit(fatalError.ExitCode())\n\t\t}\n\t\treturn err\n\t}\n\tshellCommand.Stdin = os.Stdin\n\tshellCommand.Stdout = os.Stdout\n\tshellCommand.Stderr = os.Stderr\n\treturn shellCommand.Run()\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/shutdown.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/shutdown\"\n)\n\ntype shutdownSettingsStruct struct {\n\tWaitForShutdown bool\n}\n\nvar commonShutdownSettings shutdownSettingsStruct\n\n// shutdownCmd represents the shutdown command\nvar shutdownCmd = &cobra.Command{\n\tUse:   \"shutdown\",\n\tShort: \"Shuts down the running Rancher Desktop application\",\n\tLong:  `Shuts down the running Rancher Desktop application.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcmd.SilenceUsage = true\n\t\tresult, err := doShutdown(cmd.Context(), &commonShutdownSettings, shutdown.Shutdown)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif result != nil {\n\t\t\tfmt.Println(string(result))\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(shutdownCmd)\n\tshutdownCmd.Flags().BoolVar(&commonShutdownSettings.WaitForShutdown, \"wait\", true, \"wait for shutdown to be confirmed\")\n}\n\nfunc doShutdown(ctx context.Context, shutdownSettings *shutdownSettingsStruct, initiatingCommand shutdown.InitiatingCommand) ([]byte, error) {\n\tvar output []byte\n\tconnectionInfo, err := config.GetConnectionInfo(true)\n\tif err == nil && connectionInfo != nil {\n\t\trdClient := client.NewRDClient(connectionInfo)\n\t\tcommand := client.VersionCommand(\"\", \"shutdown\")\n\t\toutput, _ = client.ProcessRequestForUtility(rdClient.DoRequest(ctx, http.MethodPut, command))\n\t\tlogrus.WithError(err).Trace(\"Shut down requested\")\n\t}\n\terr = shutdown.FinishShutdown(ctx, shutdownSettings.WaitForShutdown, initiatingCommand)\n\treturn output, err\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshot.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\ntype errorPayloadType struct {\n\t// The error message.\n\tError string `json:\"error,omitempty\"`\n\t// Whether a data reset was done as a result of the error.\n\tDataReset bool `json:\"dataReset,omitempty\"`\n}\n\nvar outputJSONFormat bool\n\nvar snapshotCmd = &cobra.Command{\n\tUse:   \"snapshot\",\n\tShort: \"Manage Rancher Desktop snapshots\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(snapshotCmd)\n}\n\nfunc exitWithJSONOrErrorCondition(e error) error {\n\tif outputJSONFormat {\n\t\texitStatus := 0\n\t\tif e != nil {\n\t\t\texitStatus = 1\n\t\t\terrorPayload := errorPayloadType{\n\t\t\t\tError:     e.Error(),\n\t\t\t\tDataReset: errors.Is(e, snapshot.ErrDataReset),\n\t\t\t}\n\t\t\tjsonBuffer, err := json.Marshal(errorPayload)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"error json-converting error messages: %w\", err)\n\t\t\t}\n\t\t\tfmt.Fprintln(os.Stdout, string(jsonBuffer))\n\t\t}\n\t\tos.Exit(exitStatus)\n\t}\n\treturn e\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotCreate.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\nvar snapshotDescription string\nvar snapshotDescriptionFrom string\n\nvar snapshotCreateCmd = &cobra.Command{\n\tUse:   \"create <name>\",\n\tShort: \"Create a snapshot\",\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif snapshotDescription != \"\" && snapshotDescriptionFrom != \"\" {\n\t\t\treturn fmt.Errorf(`can't specify more than one option from \"--description\" and \"--description-from\"`)\n\t\t}\n\t\tcmd.SilenceUsage = true\n\t\tif snapshotDescriptionFrom != \"\" {\n\t\t\tvar bytes []byte\n\t\t\tvar err error\n\t\t\tif snapshotDescriptionFrom == \"-\" {\n\t\t\t\tbytes, err = io.ReadAll(os.Stdin)\n\t\t\t} else {\n\t\t\t\tbytes, err = os.ReadFile(snapshotDescriptionFrom)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tsnapshotDescription = string(bytes)\n\t\t}\n\t\treturn exitWithJSONOrErrorCondition(createSnapshot(cmd.Context(), args))\n\t},\n}\n\nfunc init() {\n\tsnapshotCmd.AddCommand(snapshotCreateCmd)\n\tsnapshotCreateCmd.Flags().BoolVar(&outputJSONFormat, \"json\", false, \"output json format\")\n\tsnapshotCreateCmd.Flags().StringVar(&snapshotDescription, \"description\", \"\", \"snapshot description\")\n\tsnapshotCreateCmd.Flags().StringVar(&snapshotDescriptionFrom, \"description-from\", \"\", \"snapshot description from a file (or - for stdin)\")\n}\n\nfunc createSnapshot(ctx context.Context, args []string) error {\n\tname := args[0]\n\tmanager, err := snapshot.NewManager()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot manager: %w\", err)\n\t}\n\t// Report on invalid names before locking and shutting down the backend\n\tif err := manager.ValidateName(name); err != nil {\n\t\treturn err\n\t}\n\n\t// Ideally we would not use the deprecated syscall package,\n\t// but it works well with all expected scenarios and allows us\n\t// to avoid platform-specific signal handling code.\n\tnotifyCtx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)\n\tdefer stop()\n\tstopAfterFunc := context.AfterFunc(notifyCtx, func() {\n\t\tif !outputJSONFormat {\n\t\t\tfmt.Println(\"Cancelling snapshot creation...\")\n\t\t}\n\t})\n\tdefer stopAfterFunc()\n\t_, err = manager.Create(notifyCtx, name, snapshotDescription)\n\tif err != nil && !errors.Is(err, runner.ErrContextDone) {\n\t\treturn fmt.Errorf(\"failed to create snapshot: %w\", err)\n\t}\n\n\t// exclude snapshots directory from time machine backups if on macOS\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn nil\n\t}\n\t//nolint:gosec // manager.Snapshots is not a user input\n\texecCmd := exec.CommandContext(ctx, \"tmutil\", \"addexclusion\", manager.Snapshots)\n\toutput, err := execCmd.CombinedOutput()\n\tif err != nil {\n\t\tmsg := fmt.Errorf(\"`tmutil addexclusion` failed to add exclusion to TimeMachine: %w: %s\", err, output)\n\t\tif outputJSONFormat {\n\t\t\treturn msg\n\t\t} else {\n\t\t\tlogrus.Errorln(msg)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotDelete.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\nvar snapshotDeleteCmd = &cobra.Command{\n\tUse:   \"delete <id>\",\n\tShort: \"Delete a snapshot\",\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\terr := deleteSnapshot(cmd, args)\n\t\treturn exitWithJSONOrErrorCondition(err)\n\t},\n}\n\nfunc init() {\n\tsnapshotCmd.AddCommand(snapshotDeleteCmd)\n\tsnapshotDeleteCmd.Flags().BoolVarP(&outputJSONFormat, \"json\", \"\", false, \"output json format\")\n}\n\nfunc deleteSnapshot(_ *cobra.Command, args []string) error {\n\tmanager, err := snapshot.NewManager()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot manager: %w\", err)\n\t}\n\tif err = manager.Delete(args[0]); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete snapshot %q: %w\", args[0], err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotList.go",
    "content": "package cmd\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\t\"text/tabwriter\"\n\t\"time\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\nconst (\n\t// The maximum number of runes to output for tabular output.\n\ttableMaxRunes = 63\n)\n\n// SortableSnapshots are []snapshot.Snapshot sortable by date created.\ntype SortableSnapshots []snapshot.Snapshot\n\nfunc (snapshots SortableSnapshots) Len() int {\n\treturn len(snapshots)\n}\n\nfunc (snapshots SortableSnapshots) Less(i, j int) bool {\n\treturn snapshots[i].Created.Sub(snapshots[j].Created) < 0\n}\n\nfunc (snapshots SortableSnapshots) Swap(i, j int) {\n\tsnapshots[i], snapshots[j] = snapshots[j], snapshots[i]\n}\n\nvar snapshotListCmd = &cobra.Command{\n\tUse:     \"list\",\n\tAliases: []string{\"ls\"},\n\tShort:   \"List snapshots\",\n\tArgs:    cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn exitWithJSONOrErrorCondition(listSnapshot())\n\t},\n}\n\nfunc init() {\n\tsnapshotCmd.AddCommand(snapshotListCmd)\n\tsnapshotListCmd.Flags().BoolVar(&outputJSONFormat, \"json\", false, \"output json format\")\n}\n\nfunc listSnapshot() error {\n\tmanager, err := snapshot.NewManager()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot manager: %w\", err)\n\t}\n\tsnapshots, err := manager.List(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list snapshots: %w\", err)\n\t}\n\tsort.Sort(SortableSnapshots(snapshots))\n\tif outputJSONFormat {\n\t\treturn jsonOutput(snapshots)\n\t}\n\treturn tabularOutput(snapshots)\n}\n\nfunc jsonOutput(snapshots []snapshot.Snapshot) error {\n\tfor _, aSnapshot := range snapshots {\n\t\taSnapshot.ID = \"\"\n\t\tjsonBuffer, err := json.Marshal(aSnapshot)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tfmt.Println(string(jsonBuffer))\n\t}\n\treturn nil\n}\n\nfunc tabularOutput(snapshots []snapshot.Snapshot) error {\n\tif len(snapshots) == 0 {\n\t\tfmt.Fprintln(os.Stderr, \"No snapshots present.\")\n\t\treturn nil\n\t}\n\twriter := tabwriter.NewWriter(os.Stdout, 0, 4, 4, ' ', 0)\n\tfmt.Fprintf(writer, \"NAME\\tCREATED\\tDESCRIPTION\\n\")\n\tfor _, aSnapshot := range snapshots {\n\t\tprettyCreated := aSnapshot.Created.Format(time.RFC1123)\n\t\tdesc := truncateAtNewlineOrMaxRunes(aSnapshot.Description, tableMaxRunes)\n\t\tfmt.Fprintf(writer, \"%s\\t%s\\t%s\\n\", aSnapshot.Name, prettyCreated, desc)\n\t}\n\twriter.Flush()\n\treturn nil\n}\n\n// Truncates a string to either the first newline or a maximum number of\n// runes. Also removes leading and trailing whitespace.\nfunc truncateAtNewlineOrMaxRunes(input string, maxRunes int) string {\n\ttruncated := false\n\tinput = strings.TrimSpace(input)\n\tif newlineIndex := strings.Index(input, \"\\n\"); newlineIndex >= 0 {\n\t\tinput = input[:newlineIndex]\n\t\ttruncated = true\n\t}\n\truneInput := []rune(input)\n\tif len(runeInput) > maxRunes-1 {\n\t\truneInput = runeInput[:maxRunes-1]\n\t\ttruncated = true\n\t}\n\tif truncated {\n\t\treturn string(runeInput) + \"…\"\n\t}\n\treturn string(runeInput)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotList_test.go",
    "content": "package cmd\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTruncateToNewlineOrMaxRunes(t *testing.T) {\n\ttestCases := []struct {\n\t\tInput     string\n\t\tMaxLength int\n\t\tExpected  string\n\t}{\n\t\t{\n\t\t\tInput:     \"this string 🔥✨🎉 is 40 bytes\\nlong\",\n\t\t\tMaxLength: 14,\n\t\t\tExpected:  \"this string 🔥…\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"this string 🔥✨🎉 is 40 bytes\\nlong\",\n\t\t\tMaxLength: 15,\n\t\t\tExpected:  \"this string 🔥✨…\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"this string 🔥✨🎉 is 40 bytes\\nlong\",\n\t\t\tMaxLength: 22,\n\t\t\tExpected:  \"this string 🔥✨🎉 is 40…\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"this string 🔥✨🎉 is 40 bytes\\nlong\",\n\t\t\tMaxLength: 31,\n\t\t\tExpected:  \"this string 🔥✨🎉 is 40 bytes…\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"this string 🔥✨🎉 is 40 bytes\\nlong\",\n\t\t\tMaxLength: 28,\n\t\t\tExpected:  \"this string 🔥✨🎉 is 40 bytes…\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"\",\n\t\t\tMaxLength: 15,\n\t\t\tExpected:  \"\",\n\t\t},\n\t\t{\n\t\t\tInput:     \"\\nthis is a test\\n\",\n\t\t\tMaxLength: 20,\n\t\t\tExpected:  \"this is a test\",\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tdescription := fmt.Sprintf(\"truncate case %+v\", testCase)\n\t\tt.Run(description, func(t *testing.T) {\n\t\t\tresult := truncateAtNewlineOrMaxRunes(testCase.Input, testCase.MaxLength)\n\t\t\tassert.Equal(t, testCase.Expected, result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotRestore.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\nvar snapshotRestoreCmd = &cobra.Command{\n\tUse:   \"restore <id>\",\n\tShort: \"Restore a snapshot\",\n\tArgs:  cobra.ExactArgs(1),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn exitWithJSONOrErrorCondition(restoreSnapshot(args[0]))\n\t},\n}\n\nfunc init() {\n\tsnapshotCmd.AddCommand(snapshotRestoreCmd)\n\tsnapshotRestoreCmd.Flags().BoolVarP(&outputJSONFormat, \"json\", \"\", false, \"output json format\")\n}\n\nfunc restoreSnapshot(name string) error {\n\tmanager, err := snapshot.NewManager()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot manager: %w\", err)\n\t}\n\n\t// Ideally we would not use the deprecated syscall package,\n\t// but it works well with all expected scenarios and allows us\n\t// to avoid platform-specific signal handling code.\n\tctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGHUP, syscall.SIGTERM)\n\tdefer stop()\n\tstopAfterFunc := context.AfterFunc(ctx, func() {\n\t\tif !outputJSONFormat {\n\t\t\tfmt.Println(\"Cancelling snapshot restoration...\")\n\t\t}\n\t})\n\tdefer stopAfterFunc()\n\terr = manager.Restore(ctx, name)\n\tif err != nil && !errors.Is(err, runner.ErrContextDone) {\n\t\treturn fmt.Errorf(\"failed to restore snapshot %q: %w\", name, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/snapshotUnlock.go",
    "content": "package cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/snapshot\"\n)\n\nvar snapshotUnlockCmd = &cobra.Command{\n\tUse:   \"unlock\",\n\tShort: \"Remove snapshot lock\",\n\tLong: `If an error occurs while doing a snapshot operation, the filesystem\nlock that is used to prevent simultaneous snapshot operations can be\nleft behind. It then becomes impossible to run any snapshot operations.\nThis command removes the filesystem lock. It should not be needed under\nnormal circumstances.`,\n\tArgs: cobra.NoArgs,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\treturn exitWithJSONOrErrorCondition(unlockSnapshot(cmd.Context()))\n\t},\n}\n\nfunc init() {\n\tsnapshotCmd.AddCommand(snapshotUnlockCmd)\n\tsnapshotUnlockCmd.Flags().BoolVarP(&outputJSONFormat, \"json\", \"\", false, \"output json format\")\n}\n\nfunc unlockSnapshot(ctx context.Context) error {\n\tmanager, err := snapshot.NewManager()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot manager: %w\", err)\n\t}\n\treturn manager.Unlock(ctx, manager.Paths, false)\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/start.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\n// startCmd represents the start command\nvar startCmd = &cobra.Command{\n\tUse:   \"start\",\n\tShort: \"Start up Rancher Desktop, or update its settings.\",\n\tLong: `Starts up Rancher Desktop with the specified settings.\nIf it's running, behaves the same as 'rdctl set ...'.\n`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tif err := cobra.NoArgs(cmd, args); err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn doStartOrSetCommand(cmd)\n\t},\n}\n\nvar applicationPath string\nvar noModalDialogs bool\n\nfunc init() {\n\trootCmd.AddCommand(startCmd)\n\toptions.UpdateCommonStartAndSetCommands(startCmd)\n\tstartCmd.Flags().StringVarP(&applicationPath, \"path\", \"p\", \"\", \"path to main executable\")\n\tstartCmd.Flags().BoolVarP(&noModalDialogs, \"no-modal-dialogs\", \"\", false, \"avoid displaying dialog boxes\")\n}\n\n/**\n * If Rancher Desktop is currently running, treat this like a `set` command, and pass all the args to that.\n */\nfunc doStartOrSetCommand(cmd *cobra.Command) error {\n\t_, err := getListSettings(cmd.Context())\n\tif err == nil {\n\t\t// Unavoidable race condition here.\n\t\t// There's no system-wide mutex that will let us guarantee that if rancher desktop is running when\n\t\t// we test it (easiest to just try to get the settings), that it will still be running when we\n\t\t// try to upload the settings (if any were specified).\n\t\tif applicationPath != \"\" {\n\t\t\t// `--path | -p` is not a valid option for `rdctl set...`\n\t\t\treturn fmt.Errorf(\"--path %q specified but Rancher Desktop is already running\", applicationPath)\n\t\t}\n\t\treturn doSetCommand(cmd)\n\t}\n\tcmd.SilenceUsage = true\n\treturn doStartCommand(cmd)\n}\n\nfunc doStartCommand(cmd *cobra.Command) error {\n\tcommandLineArgs, err := options.GetCommandLineArgsForStartCommand(cmd.Flags())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !cmd.Flags().Changed(\"path\") {\n\t\tapplicationPath, err = paths.GetRDLaunchPath(cmd.Context())\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to locate main Rancher Desktop executable: %w\\nplease retry with the --path option\", err)\n\t\t}\n\t}\n\tif noModalDialogs {\n\t\tcommandLineArgs = append(commandLineArgs, \"--no-modal-dialogs\")\n\t}\n\treturn launchApp(cmd.Context(), applicationPath, commandLineArgs)\n}\n\nfunc launchApp(ctx context.Context, applicationPath string, commandLineArgs []string) error {\n\tvar commandName string\n\tvar args []string\n\n\tif runtime.GOOS == \"darwin\" {\n\t\tcommandName = \"/usr/bin/open\"\n\t\targs = []string{\"-a\", applicationPath}\n\t\tif len(commandLineArgs) > 0 {\n\t\t\targs = append(args, \"--args\")\n\t\t\targs = append(args, commandLineArgs...)\n\t\t}\n\t} else {\n\t\tcommandName = applicationPath\n\t\targs = commandLineArgs\n\t}\n\t// Include this output because there's a delay before the UI comes up.\n\t// Without this line, it might look like the command doesn't work.\n\tlogrus.Infof(\"About to launch %s %s ...\\n\", commandName, strings.Join(args, \" \"))\n\tcmd := exec.CommandContext(ctx, commandName, args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Start()\n}\n"
  },
  {
    "path": "src/go/rdctl/cmd/version.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/version\"\n)\n\n// showVersionCmd represents the showVersion command\nvar showVersionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Shows the CLI version.\",\n\tLong:  `Shows the CLI version.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t_, err := fmt.Printf(\"rdctl client version: %s, targeting server version: %s\\n\", version.Version, client.APIVersion)\n\t\treturn err\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(showVersionCmd)\n}\n"
  },
  {
    "path": "src/go/rdctl/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/rdctl\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/docker/cli v29.3.0+incompatible\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/hashicorp/go-multierror v1.1.1\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/text v0.35.0\n)\n\nrequire (\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.5 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgotest.tools/v3 v3.5.2 // indirect\n)\n"
  },
  {
    "path": "src/go/rdctl/go.sum",
    "content": "github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=\ngithub.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=\ngithub.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\n"
  },
  {
    "path": "src/go/rdctl/main.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/cmd\"\n)\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/autostart/autostart_darwin.go",
    "content": "package autostart\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"text/template\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nconst launchAgentFileTemplateContents = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n    <key>Label</key>\n    <string>io.rancherdesktop.autostart</string>\n    <key>ProgramArguments</key>\n    <array>\n        <string>/usr/bin/open</string>\n        <string>-a</string>\n        <string>{{ .RancherDesktopPath }}</string>\n    </array>\n    <key>ProcessType</key>\n    <string>Interactive</string>\n    <key>RunAtLoad</key>\n    <true/>\n    <key>KeepAlive</key>\n    <false/>\n</dict>\n</plist>\n`\n\ntype launchAgentFileData struct {\n\tRancherDesktopPath string\n}\n\nfunc EnsureAutostart(ctx context.Context, autostartDesired bool) error {\n\t// get path to LaunchAgent file\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find home directory: %w\", err)\n\t}\n\tlaunchAgentFilePath := filepath.Join(homeDir, \"Library\", \"LaunchAgents\", \"io.rancherdesktop.autostart.plist\")\n\n\tif autostartDesired {\n\t\t// ensure LaunchAgent directory is created\n\t\tlaunchAgentDir := filepath.Dir(launchAgentFilePath)\n\t\terr := os.MkdirAll(launchAgentDir, 0o755)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to create LaunchAgent directory: %w\", err)\n\t\t}\n\n\t\t// get current contents of LaunchAgent file\n\t\tcurrentContents, err := os.ReadFile(launchAgentFilePath)\n\t\tif err != nil && !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"failed to get current LaunchAgent file contents: %w\", err)\n\t\t}\n\n\t\t// get desired contents of LaunchAgent file\n\t\tdesiredContents, err := getDesiredLaunchAgentFileContents(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get desired LaunchAgent file contents: %w\", err)\n\t\t}\n\n\t\t// update LaunchAgent file if contents differ\n\t\tif !bytes.Equal(currentContents, desiredContents) {\n\t\t\terr = os.WriteFile(launchAgentFilePath, desiredContents, 0o644)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to write LaunchAgent file: %w\", err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\terr := os.RemoveAll(launchAgentFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove LaunchAgent file: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getDesiredLaunchAgentFileContents(ctx context.Context) ([]byte, error) {\n\trancherDesktopPath, err := paths.GetRDLaunchPath(ctx)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to get path to main Rancher Desktop executable: %w\", err)\n\t}\n\n\t// get desired contents of LaunchAgent file\n\tlaunchAgentFileTemplate, err := template.New(\"launchAgentFile\").Parse(launchAgentFileTemplateContents)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to parse LaunchAgent file template: %w\", err)\n\t}\n\tdesiredContentsBuffer := &bytes.Buffer{}\n\ttemplateData := launchAgentFileData{\n\t\tRancherDesktopPath: rancherDesktopPath,\n\t}\n\terr = launchAgentFileTemplate.ExecuteTemplate(desiredContentsBuffer, \"launchAgentFile\", templateData)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to fill LaunchAgent file template: %w\", err)\n\t}\n\treturn desiredContentsBuffer.Bytes(), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/autostart/autostart_linux.go",
    "content": "// In order to understand this file, you need to understand that\n// there are two types of .desktop files. One, referred to here as\n// \"application\" desktop files, make the application show up in\n// the launcher (and possibly other places). The other kind, referred\n// to here as \"autostart\" .desktop files, cause the application to\n// start upon login.\npackage autostart\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"text/template\"\n\n\t\"github.com/adrg/xdg\"\n\n\tp \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nconst autostartFileTemplateContents = `[Desktop Entry]\nName=Rancher Desktop\nExec={{ .Exec }}\nTerminal=false\nType=Application\nIcon=rancher-desktop\nStartupWMClass=Rancher Desktop\nCategories=Development;\n`\n\ntype autostartFileData struct {\n\tExec string\n}\n\nvar autostartDirPath string\nvar autostartFilePath string\nvar errApplicationFileNotFound = errors.New(\"failed to find application .desktop file\")\nvar applicationFileNameRegex *regexp.Regexp\nvar autostartFileTemplate *template.Template\n\nfunc init() {\n\tautostartDirPath = filepath.Join(xdg.ConfigHome, \"autostart\")\n\tautostartFilePath = filepath.Join(autostartDirPath, \"rancher-desktop.desktop\")\n\t// Application .desktop file names in the following formats are anticipated:\n\t// - rancher-desktop.desktop\n\t// - appimagekit_f8f0a5bb1016c0e50d21af6c04672f3e-Rancher_Desktop.desktop\n\tapplicationFileNameRegex = regexp.MustCompile(`^.*[rR]ancher[-_][dD]esktop\\.desktop$`)\n\tautostartFileTemplate = template.Must(template.New(\"autostartDesktopFile\").Parse(autostartFileTemplateContents))\n}\n\nfunc EnsureAutostart(ctx context.Context, autostartDesired bool) error {\n\terr := os.MkdirAll(autostartDirPath, 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif autostartDesired {\n\t\tcurrentContents, err := os.ReadFile(autostartFilePath)\n\t\tif err != nil && !os.IsNotExist(err) {\n\t\t\treturn fmt.Errorf(\"failed to read current autostart .desktop file: %w\", err)\n\t\t}\n\t\tdesiredContents, err := getDesiredAutostartFileContents(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get desired contents of autostart .desktop file: %w\", err)\n\t\t}\n\t\tif !bytes.Equal(currentContents, desiredContents) {\n\t\t\terr = os.WriteFile(autostartFilePath, desiredContents, 0o644)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to write autostart .desktop file: %w\", err)\n\t\t\t}\n\t\t}\n\t} else {\n\t\terr := os.RemoveAll(autostartFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove autostart .desktop file: %w\", err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getDesiredAutostartFileContents(ctx context.Context) ([]byte, error) {\n\t// Look for existing application .desktop files in expected locations.\n\t// This part applies to rpm, deb and AppImageLauncher installs.\n\t// We use existing application .desktop files so that there is no\n\t// discrepancy between the application and autostart .desktop files.\n\tapplicationFilePath, err := findApplicationFilePath()\n\tif err == nil {\n\t\tcontents, err := os.ReadFile(applicationFilePath)\n\t\tif err != nil {\n\t\t\treturn []byte{}, fmt.Errorf(\"failed to read contents of application .desktop file %s: %w\", applicationFilePath, err)\n\t\t}\n\t\treturn contents, nil\n\t} else if !errors.Is(err, errApplicationFileNotFound) {\n\t\treturn []byte{}, err\n\t}\n\n\t// Come up with the contents of an autostart .desktop file.\n\t// This should be needed only when Rancher Desktop is installed\n\t// via AppImage, but the user has not used AppImageLauncher to\n\t// integrate it with the system.\n\tautostartData, err := getAutostartFileData(ctx)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to get autostart file data: %w\", err)\n\t}\n\tcontents := bytes.Buffer{}\n\terr = autostartFileTemplate.ExecuteTemplate(&contents, \"autostartDesktopFile\", autostartData)\n\tif err != nil {\n\t\treturn []byte{}, fmt.Errorf(\"failed to fill autostart file template: %w\", err)\n\t}\n\treturn contents.Bytes(), nil\n}\n\n// Searches the system for a valid application .desktop file,\n// and returns the absolute path to it.\nfunc findApplicationFilePath() (string, error) {\n\tdataDirs := []string{xdg.DataHome}\n\tdataDirs = append(dataDirs, xdg.DataDirs...)\n\tfor _, dataDir := range dataDirs {\n\t\tapplicationDir := filepath.Join(dataDir, \"applications\")\n\t\tdirEntries, err := os.ReadDir(applicationDir)\n\t\tif err != nil {\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn \"\", fmt.Errorf(\"failed to read data dir %q: %w\", dataDir, err)\n\t\t}\n\t\tfor _, dirEntry := range dirEntries {\n\t\t\tfileName := dirEntry.Name()\n\t\t\tif applicationFileNameRegex.MatchString(fileName) {\n\t\t\t\treturn filepath.Join(applicationDir, fileName), nil\n\t\t\t}\n\t\t}\n\t}\n\treturn \"\", errApplicationFileNotFound\n}\n\n// Gathers the info that is needed to fill out the autostart .desktop\n// file template.\nfunc getAutostartFileData(ctx context.Context) (autostartFileData, error) {\n\texecutablePath := \"\"\n\tif _, ok := os.LookupEnv(\"APPIMAGE\"); ok {\n\t\t// If we're running under AppImage, then we need to look up\n\t\t// ~/.rd/bin/rancher-desktop and resolve it to find the executable path.\n\t\t// We only expect to be run from the UI here, so the environment\n\t\t// variable should be set correctly.\n\t\tpaths, err := p.GetPaths()\n\t\tif err != nil {\n\t\t\treturn autostartFileData{}, fmt.Errorf(\"failed to get paths: %w\", err)\n\t\t}\n\t\trancherDesktopSymlinkPath := filepath.Join(paths.Integration, \"rancher-desktop\")\n\t\texecutablePath, err = filepath.EvalSymlinks(rancherDesktopSymlinkPath)\n\t\tif err != nil {\n\t\t\treturn autostartFileData{}, fmt.Errorf(\"failed to resolve %q: %w\", rancherDesktopSymlinkPath, err)\n\t\t}\n\t} else {\n\t\t// We're not running under AppImage; we should have a normal install\n\t\t// (either an extracted zip file, or RPM), so resolving the application\n\t\t// executable relative to the current executable path should be fine.\n\t\tvar err error\n\t\texecutablePath, err = p.GetRDLaunchPath(ctx)\n\t\tif err != nil {\n\t\t\treturn autostartFileData{}, fmt.Errorf(\"failed to get Rancher Desktop executable: %w\", err)\n\t\t}\n\t}\n\n\treturn autostartFileData{\n\t\tExec: executablePath,\n\t}, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/autostart/autostart_windows.go",
    "content": "package autostart\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"golang.org/x/sys/windows/registry\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nconst relativeKey = `Software\\Microsoft\\Windows\\CurrentVersion\\Run`\nconst nameValue = \"RancherDesktop\"\n\nvar absoluteKey string\n\nfunc init() {\n\tabsoluteKey = fmt.Sprintf(`%s\\%s`, \"HKCU\", relativeKey)\n}\n\nfunc EnsureAutostart(ctx context.Context, autostartDesired bool) error {\n\tautostartKey, err := registry.OpenKey(registry.CURRENT_USER, relativeKey, registry.SET_VALUE)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open registry key: %w\", err)\n\t}\n\tdefer autostartKey.Close()\n\n\tif autostartDesired {\n\t\trancherDesktopPath, err := paths.GetRDLaunchPath(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get path to Rancher Desktop.exe: %w\", err)\n\t\t}\n\t\terr = autostartKey.SetStringValue(nameValue, rancherDesktopPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to set name value %q of registry key %q: %w\", nameValue, absoluteKey, err)\n\t\t}\n\t} else {\n\t\terr = autostartKey.DeleteValue(nameValue)\n\t\tif err != nil && !errors.Is(err, registry.ErrNotExist) {\n\t\t\treturn fmt.Errorf(\"failed to remove name value %q of registry key %q: %w\", nameValue, absoluteKey, err)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/client/client.go",
    "content": "package client\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n)\n\nconst (\n\tAPIVersion = \"v1\"\n)\n\nvar ErrConnectionRefused = errors.New(\"connection refused\")\n\ntype BackendState struct {\n\tVMState string `json:\"vmState\"`\n\tLocked  bool   `json:\"locked\"`\n}\n\n// APIError - type for representing errors from API calls.\ntype APIError struct {\n\tMessage          *string `json:\"message,omitempty\"`\n\tDocumentationURL *string `json:\"documentation_url,omitempty\"`\n}\n\ntype RDClient interface {\n\tDoRequest(ctx context.Context, method string, command string) (*http.Response, error)\n\tDoRequestWithPayload(ctx context.Context, method string, command string, payload io.Reader) (*http.Response, error)\n\tGetBackendState(ctx context.Context) (BackendState, error)\n\tUpdateBackendState(ctx context.Context, state BackendState) error\n}\n\nfunc validateBackendState(state BackendState) error {\n\tvalidStates := []string{\"STOPPED\", \"STARTING\", \"STARTED\", \"STOPPING\", \"ERROR\", \"DISABLED\"}\n\tif slices.Contains(validStates, state.VMState) {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"invalid backend state %q\", state.VMState)\n}\n\ntype RDClientImpl struct {\n\tconnectionInfo *config.ConnectionInfo\n}\n\nfunc NewRDClient(connectionInfo *config.ConnectionInfo) *RDClientImpl {\n\treturn &RDClientImpl{\n\t\tconnectionInfo: connectionInfo,\n\t}\n}\n\nfunc (client *RDClientImpl) makeURL(host string, port int, command string) string {\n\tif strings.HasPrefix(command, \"/\") {\n\t\treturn fmt.Sprintf(\"http://%s:%d%s\", host, port, command)\n\t}\n\treturn fmt.Sprintf(\"http://%s:%d/%s\", host, port, command)\n}\n\nfunc (client *RDClientImpl) DoRequest(ctx context.Context, method, command string) (*http.Response, error) {\n\treq, err := client.getRequestObject(ctx, method, command)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn http.DefaultClient.Do(req)\n}\n\nfunc (client *RDClientImpl) DoRequestWithPayload(ctx context.Context, method, command string, payload io.Reader) (*http.Response, error) {\n\turl := client.makeURL(client.connectionInfo.Host, client.connectionInfo.Port, command)\n\treq, err := http.NewRequestWithContext(ctx, method, url, payload)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.SetBasicAuth(client.connectionInfo.User, client.connectionInfo.Password)\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Close = true\n\treturn http.DefaultClient.Do(req)\n}\n\nfunc (client *RDClientImpl) getRequestObject(ctx context.Context, method, command string) (*http.Request, error) {\n\turl := client.makeURL(client.connectionInfo.Host, client.connectionInfo.Port, command)\n\treq, err := http.NewRequestWithContext(ctx, method, url, http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.SetBasicAuth(client.connectionInfo.User, client.connectionInfo.Password)\n\treq.Header.Add(\"Content-Type\", \"text/plain\")\n\treq.Close = true\n\treturn req, nil\n}\n\nfunc (client *RDClientImpl) GetBackendState(ctx context.Context) (BackendState, error) {\n\tcommand := VersionCommand(\"\", \"backend_state\")\n\tbody, err := ProcessRequestForUtility(client.DoRequest(ctx, http.MethodGet, command))\n\tif err != nil {\n\t\treturn BackendState{}, err\n\t}\n\tstate := BackendState{}\n\tif err := json.Unmarshal(body, &state); err != nil {\n\t\treturn BackendState{}, fmt.Errorf(\"failed to unmarshal backend state: %w\", err)\n\t}\n\tif err := validateBackendState(state); err != nil {\n\t\treturn BackendState{}, err\n\t}\n\treturn state, nil\n}\n\nfunc (client *RDClientImpl) UpdateBackendState(ctx context.Context, state BackendState) error {\n\tbuf := &bytes.Buffer{}\n\tencoder := json.NewEncoder(buf)\n\tif err := encoder.Encode(state); err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal backend state: %w\", err)\n\t}\n\tcommand := VersionCommand(\"\", \"backend_state\")\n\t_, err := ProcessRequestForUtility(client.DoRequestWithPayload(ctx, http.MethodPut, command, buf))\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/client/handle_unix.go",
    "content": "//go:build unix\n\npackage client\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\nfunc handleConnectionRefused(err error) error {\n\tif errors.Is(err, unix.ECONNREFUSED) {\n\t\treturn ErrConnectionRefused\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/client/handle_windows.go",
    "content": "package client\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc handleConnectionRefused(err error) error {\n\tif errors.Is(err, windows.WSAECONNREFUSED) {\n\t\treturn ErrConnectionRefused\n\t}\n\treturn err\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/client/utils.go",
    "content": "package client\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc VersionCommand(version, command string) string {\n\tif version == \"\" {\n\t\tversion = APIVersion\n\t}\n\tif strings.HasPrefix(command, \"/\") {\n\t\treturn fmt.Sprintf(\"%s%s\", version, command)\n\t}\n\treturn fmt.Sprintf(\"%s/%s\", version, command)\n}\n\nfunc ProcessRequestForAPI(response *http.Response, err error) ([]byte, *APIError, error) {\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\terrorPacket := APIError{}\n\tpErrorPacket := &errorPacket\n\tif response.StatusCode < 200 || response.StatusCode >= 300 {\n\t\terrorPacket.Message = &response.Status\n\t} else {\n\t\tpErrorPacket = nil\n\t}\n\tdefer response.Body.Close()\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\tif pErrorPacket != nil {\n\t\t\treturn nil, pErrorPacket, nil\n\t\t}\n\t\t// Only return this error if there is nothing else to report\n\t\treturn nil, nil, err\n\t}\n\treturn body, pErrorPacket, nil\n}\n\nfunc ProcessRequestForUtility(response *http.Response, err error) ([]byte, error) {\n\t// Combine platform-specific connection refused errors into a\n\t// platform-agnostic connection refused error to keep consumers\n\t// of this code clean.\n\tif err := handleConnectionRefused(err); err != nil {\n\t\treturn nil, err\n\t}\n\tif response != nil && response.Body != nil {\n\t\tdefer response.Body.Close()\n\t}\n\n\tstatusMessage := \"\"\n\tif response.StatusCode < 200 || response.StatusCode >= 300 {\n\t\t// Note that response.Status includes response.StatusCode\n\t\tswitch response.StatusCode {\n\t\tcase http.StatusBadRequest: // 400\n\t\t\tstatusMessage = response.Status\n\t\t\t// Prefer the error message in the body written by the command-server, not the one from the http server.\n\t\tcase http.StatusUnauthorized: // 401\n\t\t\treturn nil, fmt.Errorf(\"%s: user/password not accepted\", response.Status)\n\t\tcase http.StatusRequestEntityTooLarge: // 413\n\t\t\treturn nil, fmt.Errorf(\"%s\", response.Status)\n\t\tcase http.StatusInternalServerError: // 500\n\t\t\treturn nil, fmt.Errorf(\"%s: server-side problem: please consult the server logs for more information\", response.Status)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"%s (unexpected server error)\", response.Status)\n\t\t}\n\t}\n\n\tbody, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\tif statusMessage != \"\" {\n\t\t\treturn nil, fmt.Errorf(\"server error return-code %d: %s\", response.StatusCode, statusMessage)\n\t\t}\n\t\treturn nil, err\n\t} else if statusMessage != \"\" {\n\t\treturn nil, fmt.Errorf(\"%s\", string(body))\n\t}\n\treturn body, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/command/command.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package command implements helpers for command-line handling.\npackage command\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime\"\n)\n\ntype commandNameContextKey struct{}\n\n// WithCommandName returns a new context that keeps track of the command being\n// invoked.\nfunc WithCommandName(ctx context.Context, commandName string) context.Context {\n\treturn context.WithValue(ctx, commandNameContextKey{}, commandName)\n}\n\n// FatalError is an error that should stop execution immediately, without using\n// the normal cobra error handling.\ntype FatalError interface {\n\terror\n\t// ExitCode returns the process exit code that should be set.\n\tExitCode() int\n}\n\n// simpleFatalError implements FatalError\ntype simpleFatalError struct {\n\tmessage  string\n\texitCode int\n}\n\nfunc (e *simpleFatalError) Error() string {\n\treturn e.message\n}\n\nfunc (e *simpleFatalError) ExitCode() int {\n\treturn e.exitCode\n}\n\n// NewFatalError returns an error implementing FatalError\nfunc NewFatalError(message string, exitCode int) error {\n\treturn &simpleFatalError{\n\t\tmessage:  message,\n\t\texitCode: exitCode,\n\t}\n}\n\nconst restartDirective = \"Either run 'rdctl start' or start the Rancher Desktop application first\"\n\n// NewVMStateError returns an error stating that the Rancher Desktop VM (or WSL\n// distribution) is not in the correct state.  If actualState is the empty\n// string, then it signifies that the VM does not exist.\nfunc NewVMStateError(ctx context.Context, desiredState, actualState string) error {\n\tcommandName := \"rdctl\"\n\tif value, ok := ctx.Value(commandNameContextKey{}).(string); ok {\n\t\tcommandName = value\n\t}\n\n\tstatus := fmt.Sprintf(\"needs to be running in order to execute '%s', but it currently is not.\", commandName)\n\tif actualState != \"\" {\n\t\tstatus = fmt.Sprintf(\"needs to be in state %q in order to execute '%s', but it is current in state %q.\", desiredState, commandName, actualState)\n\t}\n\tvm := \"VM\"\n\tif runtime.GOOS == \"windows\" {\n\t\tvm = \"WSL distribution\"\n\t}\n\treturn NewFatalError(\n\t\tfmt.Sprintf(\"The Rancher Desktop %s %s\\n%s\", vm, status, restartDirective),\n\t\t1)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/config/config.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package config handles all the config-related parts of rdctl\npackage config\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\n// ConnectionInfo stores the parameters needed to connect to an HTTP server\ntype ConnectionInfo struct {\n\tUser     string\n\tPassword string\n\tHost     string\n\tPort     int\n}\n\nvar (\n\tconnectionSettings ConnectionInfo\n\tverbose            bool\n\n\tconfigPath string\n\t// DefaultConfigPath - used to differentiate not being able to find a user-specified config file from the default\n\tDefaultConfigPath string\n\n\twslDistroEnvs = []string{\"WSL_DISTRO_NAME\", \"WSL_INTEROP\", \"WSLENV\"}\n\t// lstatFunc allows tests to inject a stub for /bin/wslpath checks.\n\tlstatFunc = os.Lstat\n)\n\n// DefineGlobalFlags sets up the global flags, available for all sub-commands\nfunc DefineGlobalFlags(rootCmd *cobra.Command) {\n\tvar configDir string\n\tif runtime.GOOS == \"linux\" && isWSLDistro() {\n\t\tctx := rootCmd.Context()\n\t\tif ctx == nil {\n\t\t\tctx = context.Background()\n\t\t}\n\t\tif wslConfigDir, err := wslifyConfigDir(ctx); err == nil {\n\t\t\twindowsConfigPath := filepath.Join(wslConfigDir, \"rancher-desktop\", \"rd-engine.json\")\n\t\t\tif _, statErr := os.Stat(windowsConfigPath); statErr == nil {\n\t\t\t\tconfigDir = filepath.Join(wslConfigDir, \"rancher-desktop\")\n\t\t\t}\n\t\t}\n\t}\n\tif configDir == \"\" {\n\t\tappPaths, err := paths.GetPaths()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"failed to get paths: %s\", err)\n\t\t}\n\t\tconfigDir = appPaths.AppHome\n\t}\n\tDefaultConfigPath = filepath.Join(configDir, \"rd-engine.json\")\n\trootCmd.PersistentFlags().StringVar(&configPath, \"config-path\", \"\", fmt.Sprintf(\"config file (default %s)\", DefaultConfigPath))\n\trootCmd.PersistentFlags().StringVar(&connectionSettings.User, \"user\", \"\", \"overrides the user setting in the config file\")\n\trootCmd.PersistentFlags().StringVar(&connectionSettings.Host, \"host\", \"\", \"default is 127.0.0.1; most useful for WSL\")\n\trootCmd.PersistentFlags().IntVar(&connectionSettings.Port, \"port\", 0, \"overrides the port setting in the config file\")\n\trootCmd.PersistentFlags().StringVar(&connectionSettings.Password, \"password\", \"\", \"overrides the password setting in the config file\")\n\trootCmd.PersistentFlags().BoolVar(&verbose, \"verbose\", false, \"Be verbose\")\n}\n\n// GetConnectionInfo returns the connection details of the application API server.\n// As a special case this function may return a nil *ConnectionInfo and nil error\n// when the config file has not been specified explicitly, the default config file\n// does not exist, and the mayBeMissing parameter is true.\nfunc GetConnectionInfo(mayBeMissing bool) (*ConnectionInfo, error) {\n\tvar settings ConnectionInfo\n\n\tif configPath == \"\" {\n\t\tconfigPath = DefaultConfigPath\n\t}\n\tcontent, readFileError := os.ReadFile(configPath)\n\tif readFileError != nil {\n\t\t// It is ok if the default config path doesn't exist; the user may have specified the required settings on the commandline.\n\t\t// But it is an error if the file specified via --config-path cannot be read.\n\t\tif configPath != DefaultConfigPath || !errors.Is(readFileError, os.ErrNotExist) {\n\t\t\treturn nil, readFileError\n\t\t}\n\t} else if err := json.Unmarshal(content, &settings); err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing config file %q: %w\", configPath, err)\n\t}\n\n\t// CLI options override file settings\n\tif connectionSettings.Host != \"\" {\n\t\tsettings.Host = connectionSettings.Host\n\t}\n\tif settings.Host == \"\" {\n\t\tsettings.Host = \"127.0.0.1\"\n\t}\n\tif connectionSettings.User != \"\" {\n\t\tsettings.User = connectionSettings.User\n\t}\n\tif connectionSettings.Password != \"\" {\n\t\tsettings.Password = connectionSettings.Password\n\t}\n\tif connectionSettings.Port != 0 {\n\t\tsettings.Port = connectionSettings.Port\n\t}\n\tif settings.Port == 0 || settings.User == \"\" || settings.Password == \"\" {\n\t\t// Missing the default config file may or may not be considered an error\n\t\tif readFileError != nil {\n\t\t\tif mayBeMissing {\n\t\t\t\treadFileError = nil\n\t\t\t}\n\t\t\treturn nil, readFileError\n\t\t}\n\t\treturn nil, errors.New(\"insufficient connection settings (missing one or more of: port, user, and password)\")\n\t}\n\n\treturn &settings, nil\n}\n\n// determines if we are running in a wsl linux distro\n// by checking for availability of wslpath and see if it's a symlink\nfunc isWSLDistro() bool {\n\tfi, err := lstatFunc(\"/bin/wslpath\")\n\tif err != nil {\n\t\treturn false\n\t}\n\tif fi.Mode()&os.ModeSymlink != os.ModeSymlink {\n\t\treturn false\n\t}\n\treturn hasWSLEnvs()\n}\n\n// hasWSLEnvs reports whether any WSL environment marker is present.\nfunc hasWSLEnvs() bool {\n\tfor _, envName := range wslDistroEnvs {\n\t\tif _, ok := os.LookupEnv(envName); ok {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getLocalAppDataPath(ctx context.Context) (string, error) {\n\tvar outBuf bytes.Buffer\n\t// changes the codepage to 65001 which is UTF-8\n\tsubCommand := `chcp 65001 >nul & echo %LOCALAPPDATA%`\n\tcmd := exec.CommandContext(ctx, \"cmd.exe\", \"/c\", subCommand)\n\tcmd.Stdout = &outBuf\n\t// We are intentionally not using CombinedOutput and\n\t// excluding the stderr since it could contain some\n\t// warnings when rdctl is triggered from a non WSL mounted directory\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimRight(outBuf.String(), \"\\r\\n\"), nil\n}\n\nfunc wslifyConfigDir(ctx context.Context) (string, error) {\n\tpath, err := getLocalAppDataPath(ctx)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tvar outBuf bytes.Buffer\n\tcmd := exec.CommandContext(ctx, \"/bin/wslpath\", path)\n\tcmd.Stdout = &outBuf\n\tif err := cmd.Run(); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn strings.TrimRight(outBuf.String(), \"\\r\\n\"), err\n}\n\n// PersistentPreRunE is meant to be executed as the cobra hook\nfunc PersistentPreRunE(cmd *cobra.Command, args []string) error {\n\tif verbose {\n\t\tlogrus.SetLevel(logrus.TraceLevel)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/config/config_test.go",
    "content": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype fakeFileInfo struct {\n\tmode os.FileMode\n}\n\nfunc (info fakeFileInfo) Name() string       { return \"wslpath\" }\nfunc (info fakeFileInfo) Size() int64        { return 0 }\nfunc (info fakeFileInfo) Mode() os.FileMode  { return info.mode }\nfunc (info fakeFileInfo) ModTime() time.Time { return time.Time{} }\nfunc (info fakeFileInfo) IsDir() bool        { return info.mode.IsDir() }\nfunc (info fakeFileInfo) Sys() any           { return nil }\n\nfunc saveWSLEnvs(t *testing.T) {\n\toriginalEnvs := map[string]string{}\n\tfor _, envName := range wslDistroEnvs {\n\t\tif value, ok := os.LookupEnv(envName); ok {\n\t\t\toriginalEnvs[envName] = value\n\t\t}\n\t}\n\tt.Cleanup(func() {\n\t\tfor _, envName := range wslDistroEnvs {\n\t\t\tif value, present := originalEnvs[envName]; present {\n\t\t\t\tos.Setenv(envName, value)\n\t\t\t} else {\n\t\t\t\tos.Unsetenv(envName)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestGetConnectionInfo_ValidConfigFile(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tconfigFile := filepath.Join(tmpDir, \"rd-engine.json\")\n\n\tconfig := ConnectionInfo{\n\t\tUser:     \"example_user\",\n\t\tPassword: \"example_password\",\n\t\tHost:     \"192.168.1.1\",\n\t\tPort:     8080,\n\t}\n\n\tdata, err := json.Marshal(config)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFile, data, 0600)\n\trequire.NoError(t, err)\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t})\n\n\tconfigPath = configFile\n\tDefaultConfigPath = configFile\n\n\tresult, err := GetConnectionInfo(false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"example_user\", result.User)\n\tassert.Equal(t, \"example_password\", result.Password)\n\tassert.Equal(t, \"192.168.1.1\", result.Host)\n\tassert.Equal(t, 8080, result.Port)\n}\n\nfunc TestGetConnectionInfo_MissingConfigFile_MayBeMissing(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tnonExistentFile := filepath.Join(tmpDir, \"nonexistent.json\")\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t})\n\n\tconfigPath = \"\"\n\tDefaultConfigPath = nonExistentFile\n\n\tresult, err := GetConnectionInfo(true)\n\tassert.Nil(t, result)\n\tassert.Nil(t, err)\n}\n\nfunc TestGetConnectionInfo_MissingConfigFile_Required(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tnonExistentFile := filepath.Join(tmpDir, \"nonexistent.json\")\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t})\n\n\tconfigPath = nonExistentFile\n\tDefaultConfigPath = nonExistentFile\n\n\tresult, err := GetConnectionInfo(false)\n\tassert.Nil(t, result)\n\tassert.Error(t, err)\n}\n\nfunc TestGetConnectionInfo_InvalidJSON(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tconfigFile := filepath.Join(tmpDir, \"rd-engine.json\")\n\n\terr := os.WriteFile(configFile, []byte(\"not valid json\"), 0600)\n\trequire.NoError(t, err)\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t})\n\n\tconfigPath = configFile\n\tDefaultConfigPath = configFile\n\n\tresult, err := GetConnectionInfo(false)\n\tassert.Nil(t, result)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"error parsing config file\")\n}\n\nfunc TestGetConnectionInfo_CLIOverrides(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tconfigFile := filepath.Join(tmpDir, \"rd-engine.json\")\n\n\tconfig := ConnectionInfo{\n\t\tUser:     \"config_user\",\n\t\tPassword: \"config_password\",\n\t\tHost:     \"config_host\",\n\t\tPort:     9999,\n\t}\n\n\tdata, err := json.Marshal(config)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFile, data, 0600)\n\trequire.NoError(t, err)\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\toriginalConnectionSettings := connectionSettings\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t\tconnectionSettings = originalConnectionSettings\n\t})\n\n\tconfigPath = configFile\n\tDefaultConfigPath = configFile\n\tconnectionSettings = ConnectionInfo{\n\t\tUser:     \"override_user\",\n\t\tPassword: \"override_password\",\n\t\tHost:     \"override_host\",\n\t\tPort:     1234,\n\t}\n\n\tresult, err := GetConnectionInfo(false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"override_user\", result.User)\n\tassert.Equal(t, \"override_password\", result.Password)\n\tassert.Equal(t, \"override_host\", result.Host)\n\tassert.Equal(t, 1234, result.Port)\n}\n\nfunc TestGetConnectionInfo_DefaultHost(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tconfigFile := filepath.Join(tmpDir, \"rd-engine.json\")\n\n\tconfig := ConnectionInfo{\n\t\tUser:     \"example_user\",\n\t\tPassword: \"example_password\",\n\t\tPort:     8080,\n\t}\n\n\tdata, err := json.Marshal(config)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFile, data, 0600)\n\trequire.NoError(t, err)\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\toriginalConnectionSettings := connectionSettings\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t\tconnectionSettings = originalConnectionSettings\n\t})\n\n\tconfigPath = configFile\n\tDefaultConfigPath = configFile\n\tconnectionSettings = ConnectionInfo{}\n\n\tresult, err := GetConnectionInfo(false)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"127.0.0.1\", result.Host)\n}\n\nfunc TestGetConnectionInfo_MissingRequiredFields(t *testing.T) {\n\ttmpDir := t.TempDir()\n\tconfigFile := filepath.Join(tmpDir, \"rd-engine.json\")\n\n\tconfig := ConnectionInfo{\n\t\tHost: \"example_host\",\n\t}\n\n\tdata, err := json.Marshal(config)\n\trequire.NoError(t, err)\n\terr = os.WriteFile(configFile, data, 0600)\n\trequire.NoError(t, err)\n\n\toriginalConfigPath := configPath\n\toriginalDefaultConfigPath := DefaultConfigPath\n\toriginalConnectionSettings := connectionSettings\n\tt.Cleanup(func() {\n\t\tconfigPath = originalConfigPath\n\t\tDefaultConfigPath = originalDefaultConfigPath\n\t\tconnectionSettings = originalConnectionSettings\n\t})\n\n\tconfigPath = configFile\n\tDefaultConfigPath = configFile\n\tconnectionSettings = ConnectionInfo{}\n\n\tresult, err := GetConnectionInfo(false)\n\tassert.Nil(t, result)\n\tassert.Error(t, err)\n\tassert.Contains(t, err.Error(), \"insufficient connection settings\")\n}\n\nfunc TestIsWSLDistro(t *testing.T) {\n\tfor _, symlinkMode := range []os.FileMode{os.ModeSymlink, 0} {\n\t\tsymlinkText := map[os.FileMode]string{\n\t\t\tos.ModeSymlink: \"with wslpath symlink\",\n\t\t\t0:              \"without wslpath symlink\",\n\t\t}[symlinkMode]\n\t\tfor _, hasEnvs := range []bool{true, false} {\n\t\t\tenvText := map[bool]string{\n\t\t\t\ttrue:  \"with WSL envs\",\n\t\t\t\tfalse: \"without WSL envs\",\n\t\t\t}[hasEnvs]\n\t\t\texpected := symlinkMode != 0 && hasEnvs\n\t\t\ttestName := fmt.Sprintf(\"returns %t %s %s\", expected, symlinkText, envText)\n\t\t\tt.Run(testName, func(t *testing.T) {\n\t\t\t\tsaveWSLEnvs(t)\n\t\t\t\tfor _, envName := range wslDistroEnvs {\n\t\t\t\t\tos.Unsetenv(envName)\n\t\t\t\t}\n\t\t\t\toriginalLstat := lstatFunc\n\t\t\t\tt.Cleanup(func() { lstatFunc = originalLstat })\n\t\t\t\tlstatFunc = func(_ string) (os.FileInfo, error) {\n\t\t\t\t\treturn fakeFileInfo{mode: symlinkMode}, nil\n\t\t\t\t}\n\t\t\t\tif hasEnvs {\n\t\t\t\t\tos.Setenv(wslDistroEnvs[0], \"Ubuntu\")\n\t\t\t\t}\n\t\t\t\tif expected {\n\t\t\t\t\tassert.True(t, isWSLDistro(), \"expected isWSLDistro to be true\")\n\t\t\t\t} else {\n\t\t\t\t\tassert.False(t, isWSLDistro(), \"expected isWSLDistro to be false\")\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestHasWSLEnvs(t *testing.T) {\n\tt.Run(\"returns false when none set\", func(t *testing.T) {\n\t\tsaveWSLEnvs(t)\n\t\tfor _, envName := range wslDistroEnvs {\n\t\t\tos.Unsetenv(envName)\n\t\t}\n\t\tassert.False(t, hasWSLEnvs(), \"expected hasWSLEnvs to be false without WSL envs\")\n\t})\n\n\tt.Run(\"returns true when any set\", func(t *testing.T) {\n\t\tsaveWSLEnvs(t)\n\t\tfor _, envName := range wslDistroEnvs {\n\t\t\tos.Unsetenv(envName)\n\t\t}\n\t\tos.Setenv(wslDistroEnvs[0], \"Ubuntu\")\n\t\tassert.True(t, hasWSLEnvs(), \"expected hasWSLEnvs to be true with WSL envs\")\n\t})\n}\n\nfunc TestIsWSLDistroLstatError(t *testing.T) {\n\tsaveWSLEnvs(t)\n\toriginalLstat := lstatFunc\n\tt.Cleanup(func() { lstatFunc = originalLstat })\n\tlstatFunc = func(_ string) (os.FileInfo, error) {\n\t\treturn nil, errors.New(\"lstat failed\")\n\t}\n\tos.Setenv(wslDistroEnvs[0], \"Ubuntu\")\n\tassert.False(t, isWSLDistro(), \"expected isWSLDistro to be false when lstat fails\")\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/directories.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage directories\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n)\n\ntype rdctlOverrideKeyType struct{}\n\nvar rdctlOverrideKey = rdctlOverrideKeyType{}\n\n// OverrideRdctlPath produces a context that will override the path of the rdctl\n// executable.  This should only be used in tests.\nfunc OverrideRdctlPath(ctx context.Context, rdctlPath string) context.Context {\n\tif !testing.Testing() {\n\t\tpanic(\"WithOverride can only be used for testing\")\n\t}\n\treturn context.WithValue(ctx, rdctlOverrideKey, rdctlPath)\n}\n\n// GetApplicationDirectory returns the installation directory of the application.\nfunc GetApplicationDirectory(ctx context.Context) (string, error) {\n\tvar exePathWithSymlinks string\n\tvar err error\n\toverride, ok := ctx.Value(rdctlOverrideKey).(string)\n\tif ok {\n\t\texePathWithSymlinks = override\n\t} else {\n\t\tif exePathWithSymlinks, err = os.Executable(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\texePath, err := filepath.EvalSymlinks(exePathWithSymlinks)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif info, err := os.Stat(exePathWithSymlinks); err != nil {\n\t\treturn \"\", fmt.Errorf(\"rdctl executable does not exist: %w\", err)\n\t} else if info.IsDir() {\n\t\treturn \"\", fmt.Errorf(\"rdctl executable is a directory\")\n\t}\n\n\tplatform := runtime.GOOS\n\tif runtime.GOOS == \"windows\" {\n\t\t// On Windows, we use \"win32\" instead of \"windows\".\n\t\tplatform = \"win32\"\n\t}\n\n\t// Given the path to the exe, find its directory, and drop the\n\t// \"resources\\win32\\bin\" suffix (possibly with another \"resources\" in front).\n\t// On mac, we need to drop \"Contents/Resources/resources/darwin/bin\".\n\tresultDir := filepath.Dir(exePath)\n\tfor _, part := range []string{\"bin\", platform, \"resources\", \"Resources\", \"Contents\"} {\n\t\tfor filepath.Base(resultDir) == part {\n\t\t\tresultDir = filepath.Dir(resultDir)\n\t\t}\n\t}\n\treturn resultDir, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/directories_test.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage directories_test\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc TestGetApplicationDirectory(t *testing.T) {\n\tt.Parallel()\n\tplatformDirs := map[string][]string{\n\t\t\"darwin\":  {\"Contents\", \"Resources\", \"resources\", \"darwin\", \"bin\"},\n\t\t\"windows\": {\"resources\", \"resources\", \"win32\", \"bin\"},\n\t}[runtime.GOOS]\n\tif len(platformDirs) == 0 {\n\t\tplatformDirs = []string{\"resources\", \"resources\", runtime.GOOS, \"bin\"}\n\t}\n\tt.Run(\"should go up the directory\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\tbinDir := filepath.Join(append([]string{dir}, platformDirs...)...)\n\t\trequire.NoError(t, os.MkdirAll(binDir, 0o755))\n\t\ttestExe, err := os.Executable()\n\t\trequire.NoError(t, err)\n\t\texePath := filepath.Join(binDir, filepath.Base(testExe))\n\t\texe, err := os.Create(exePath)\n\t\trequire.NoError(t, err)\n\t\tdefer exe.Close()\n\t\tctx := directories.OverrideRdctlPath(context.Background(), exePath)\n\t\tactual, err := directories.GetApplicationDirectory(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, dir, actual)\n\t})\n\tt.Run(\"invalid executable path\", func(t *testing.T) {\n\t\tctx := directories.OverrideRdctlPath(context.Background(), \"\")\n\t\t_, err := directories.GetApplicationDirectory(ctx)\n\t\tassert.Error(t, err)\n\t})\n\tt.Run(\"nonexistent executable file\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\texePath := filepath.Join(dir, \"does-not-exist\")\n\t\tctx := directories.OverrideRdctlPath(context.Background(), exePath)\n\t\t_, err = directories.GetApplicationDirectory(ctx)\n\t\tassert.Error(t, err)\n\t})\n\tt.Run(\"should resolve symbolic links\", func(t *testing.T) {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"Test is not supported on Windows\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\tbinDir := filepath.Join(append([]string{dir}, platformDirs...)...)\n\t\trequire.NoError(t, os.MkdirAll(binDir, 0o755))\n\t\ttestExe, err := os.Executable()\n\t\trequire.NoError(t, err)\n\t\texePath := filepath.Join(binDir, filepath.Base(testExe))\n\t\texe, err := os.Create(exePath)\n\t\trequire.NoError(t, err)\n\t\tdefer exe.Close()\n\t\tlink := filepath.Join(dir, \"symbolic-link\")\n\t\trequire.NoError(t, os.Symlink(exePath, link))\n\t\tctx := directories.OverrideRdctlPath(context.Background(), exePath)\n\t\tactual, err := directories.GetApplicationDirectory(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, dir, actual)\n\t})\n\tt.Run(\"symbolic link loop\", func(t *testing.T) {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tt.Skip(\"Test is not supported on Windows\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\texePath := filepath.Join(dir, \"executable\")\n\t\trequire.NoError(t, os.Symlink(exePath, exePath))\n\t\tctx := directories.OverrideRdctlPath(context.Background(), exePath)\n\t\t_, err = directories.GetApplicationDirectory(ctx)\n\t\tassert.Error(t, err)\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/directories_windows.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage directories\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n// The initial buffer size for use with InvokeWin32WithBuffer\nconst initialBufferSize = uint32(256)\n\n// InvokeWin32WithBuffer calls the given function with increasing buffer sizes\n// until it does not return ERROR_INSUFFICIENT_BUFFER.\nfunc InvokeWin32WithBuffer(cb func(size uint32) error) error {\n\tsize := initialBufferSize\n\tfor {\n\t\terr := cb(size)\n\t\tif err == nil {\n\t\t\treturn nil\n\t\t}\n\t\tif !errors.Is(err, windows.ERROR_INSUFFICIENT_BUFFER) {\n\t\t\treturn err\n\t\t}\n\t\tif size > (1 << 30) {\n\t\t\treturn err\n\t\t}\n\t\tsize *= 2\n\t}\n}\n\nfunc GetLocalAppDataDirectory() (string, error) {\n\tdir, err := getKnownFolder(windows.FOLDERID_LocalAppData)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not get the AppData folder: %w\", err)\n\t}\n\treturn dir, nil\n}\n\nfunc GetRoamingAppDataDirectory() (string, error) {\n\tdir, err := getKnownFolder(windows.FOLDERID_RoamingAppData)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not get the RoamingAppData folder: %w\", err)\n\t}\n\treturn dir, nil\n}\n\nvar (\n\tole32Dll   = windows.MustLoadDLL(\"Ole32.dll\")\n\tshell32Dll = windows.MustLoadDLL(\"Shell32.dll\")\n)\n\n// getKnownFolder gets a Windows known folder.  See https://git.io/JMpgD\nfunc getKnownFolder(folder *windows.KNOWNFOLDERID) (string, error) {\n\tSHGetKnownFolderPath, err := shell32Dll.FindProc(\"SHGetKnownFolderPath\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find SHGetKnownFolderPath: %w\", err)\n\t}\n\tCoTaskMemFree, err := ole32Dll.FindProc(\"CoTaskMemFree\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not find CoTaskMemFree: %w\", err)\n\t}\n\tvar result *uint16\n\thr, _, _ := SHGetKnownFolderPath.Call(\n\t\tuintptr(unsafe.Pointer(folder)),\n\t\t0,\n\t\tuintptr(unsafe.Pointer(nil)),\n\t\tuintptr(unsafe.Pointer(&result)),\n\t)\n\t// SHGetKnownFolderPath documentation says we _must_ free the result with\n\t// CoTaskMemFree, even if the call failed.\n\t// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath\n\tdefer func() { _, _, _ = CoTaskMemFree.Call(uintptr(unsafe.Pointer(result))) }()\n\tif hr != 0 {\n\t\treturn \"\", windows.Errno(hr)\n\t}\n\n\t// result at this point contains the path, as a PWSTR\n\treturn windows.UTF16PtrToString(result), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/directories_windows_test.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage directories\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc TestGetKnownFolder(t *testing.T) {\n\tt.Run(\"AppData\", func(t *testing.T) {\n\t\texpected := os.Getenv(\"APPDATA\")\n\t\tactual, err := getKnownFolder(windows.FOLDERID_RoamingAppData)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, expected, actual)\n\t\t}\n\t})\n\tt.Run(\"LocalAppData\", func(t *testing.T) {\n\t\texpected := os.Getenv(\"LOCALAPPDATA\")\n\t\tactual, err := getKnownFolder(windows.FOLDERID_LocalAppData)\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, expected, actual)\n\t\t}\n\t})\n\tt.Run(\"invalid folder\", func(t *testing.T) {\n\t\tzeroGUID := windows.KNOWNFOLDERID{}\n\t\t_, err := getKnownFolder(&zeroGUID)\n\t\tif assert.Error(t, err) {\n\t\t\tnotFound := 0x80070002 // HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)\n\t\t\tassert.Equal(t, windows.Errno(notFound), err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/empty.go",
    "content": "//go:build !windows\n\n/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage directories\n\nimport \"fmt\"\n\nfunc GetLocalAppDataDirectory() (string, error) {\n\treturn \"\", fmt.Errorf(\"internal error: GetLocalAppDataDirectory shouldn't be called\")\n}\n\nfunc GetRoamingAppDataDirectory() (string, error) {\n\treturn \"\", fmt.Errorf(\"internal error: GetRoamingAppDataDirectory shouldn't be called\")\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/directories/lima_home.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage directories\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n)\n\nfunc SetupLimaHome(appHome string) error {\n\tcandidatePath := filepath.Join(appHome, \"lima\")\n\tstat, err := os.Stat(candidatePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"can't find the lima-home directory at %q: %w\", candidatePath, err)\n\t}\n\tif !stat.Mode().IsDir() {\n\t\treturn fmt.Errorf(\"path %q exists but isn't a directory\", candidatePath)\n\t}\n\treturn os.Setenv(\"LIMA_HOME\", candidatePath)\n}\n\nfunc GetLimactlPath() (string, error) {\n\texecPath, err := os.Executable()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\texecPath, err = filepath.EvalSymlinks(execPath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresult := filepath.Join(filepath.Dir(filepath.Dir(execPath)), \"lima\", \"bin\", \"limactl\")\n\tif runtime.GOOS == \"windows\" {\n\t\tresult += \".exe\"\n\t}\n\treturn result, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage factoryreset\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\n\tdockerconfig \"github.com/docker/cli/cli/config\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\ntype dockerConfigType map[string]any\n\ntype PartialMeta struct {\n\tMetadata struct {\n\t\tDescription string\n\t}\n}\n\n/**\n * cleanupDockerContextFiles - normally RD will remove any contexts from .docker/contexts/meta that it owns.\n * This function checks the dir for any contexts that were left behind, and deletes them.\n */\nfunc cleanupDockerContextFiles() {\n\tos.RemoveAll(path.Join(dockerconfig.Dir(), \"contexts\", \"meta\", \"b547d66a5de60e5f0843aba28283a8875c2ad72e99ba076060ef9ec7c09917c8\"))\n}\n\nfunc clearDockerContext() error {\n\t// Ignore failure to delete this next file:\n\tos.Remove(path.Join(dockerconfig.Dir(), \"plaintext-credentials.config.json\"))\n\n\tcleanupDockerContextFiles()\n\n\tconfigFilePath := path.Join(dockerconfig.Dir(), \"config.json\")\n\tdockerConfigContents := make(dockerConfigType)\n\tcontents, err := os.ReadFile(configFilePath)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// Nothing left to do here, since the file doesn't exist\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"factory-reset: error trying to read docker config.json: %w\", err)\n\t}\n\tif err = json.Unmarshal(contents, &dockerConfigContents); err != nil {\n\t\t// If we can't json-unmarshal ~/.docker/config, nothing left to do\n\t\treturn nil\n\t}\n\tcurrentContextName, ok := dockerConfigContents[\"currentContext\"]\n\tif !ok {\n\t\treturn nil\n\t}\n\tif currentContextName != \"rancher-desktop\" {\n\t\treturn nil\n\t}\n\tdelete(dockerConfigContents, \"currentContext\")\n\tcontents, err = json.MarshalIndent(dockerConfigContents, \"\", \"  \")\n\tif err != nil {\n\t\treturn err\n\t}\n\tscratchFile, err := os.CreateTemp(dockerconfig.Dir(), \"tmpconfig.json\")\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = os.WriteFile(scratchFile.Name(), contents, 0o600)\n\tscratchFile.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn os.Rename(scratchFile.Name(), configFilePath)\n}\n\nfunc deleteLimaVM(ctx context.Context) error {\n\tappPaths, err := paths.GetPaths()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := directories.SetupLimaHome(appPaths.AppHome); err != nil {\n\t\treturn err\n\t}\n\tlimactl, err := directories.GetLimactlPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn exec.CommandContext(ctx, limactl, \"delete\", \"-f\", \"0\").Run()\n}\n\n// DeleteCacheData deletes the application cache\nfunc DeleteCacheData() error {\n\tappPaths, err := paths.GetPaths()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Delete the cache directory\n\tif err := os.RemoveAll(appPaths.Cache); err != nil {\n\t\treturn fmt.Errorf(\"failed to delete cache directory: %w\", err)\n\t}\n\n\tfmt.Println(\"Cache cleared successfully\")\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data_darwin.go",
    "content": "package factoryreset\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\nfunc DeleteData(ctx context.Context, appPaths *paths.Paths, removeKubernetesCache bool) error {\n\tif err := autostart.EnsureAutostart(ctx, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to remove autostart configuration: %s\", err)\n\t}\n\n\tif err := process.TerminateProcessInDirectory(appPaths.ExtensionRoot, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to stop extension processes, ignoring: %s\", err)\n\t}\n\n\tpathList := []string{\n\t\tappPaths.AltAppHome,\n\t\tappPaths.Config,\n\t\tappPaths.Logs,\n\t\tappPaths.ExtensionRoot,\n\t\tappPaths.OldUserData,\n\t}\n\tpathList = append(pathList, appHomeDirectories(appPaths)...)\n\n\t// Get path that electron-updater stores cache data in. Technically this\n\t// is the wrong directory to use for cache data, but it is set by electron-updater.\n\t// TODO: investigate changing the directory electron-updater uses\n\tconfigDir, err := os.UserConfigDir()\n\tif err != nil {\n\t\tlogrus.Errorf(\"failed to get config dir: %s\", err)\n\t} else {\n\t\tpathList = append(pathList, filepath.Join(configDir, \"Caches\", \"rancher-desktop-updater\"))\n\t}\n\n\tif removeKubernetesCache {\n\t\tpathList = append(pathList, appPaths.Cache)\n\t} else {\n\t\tpathList = append(pathList, filepath.Join(appPaths.Cache, \"updater-longhorn.json\"))\n\t}\n\treturn deleteUnixLikeData(ctx, appPaths, pathList)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data_linux.go",
    "content": "package factoryreset\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\nfunc DeleteData(ctx context.Context, appPaths *paths.Paths, removeKubernetesCache bool) error {\n\tif err := autostart.EnsureAutostart(ctx, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to remove autostart configuration: %s\", err)\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\tlogrus.Errorf(\"Error getting home directory: %s\", err)\n\t}\n\n\tif err := process.TerminateProcessInDirectory(appPaths.ExtensionRoot, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to stop extension processes, ignoring: %s\", err)\n\t}\n\n\tpathList := []string{\n\t\tappPaths.AltAppHome,\n\t\tappPaths.Config,\n\t\tappPaths.Logs,\n\t\tappPaths.OldUserData,\n\t\tfilepath.Join(homeDir, \".local\", \"state\", \"rancher-desktop\"),\n\t}\n\n\t// Electron stores things in ~/.config/Rancher Desktop. This is difficult\n\t// to change. We should still clean up the directory on factory reset.\n\tconfigPath, err := os.UserConfigDir()\n\tif err != nil {\n\t\tlogrus.Errorf(\"Error getting config directory: %s\", err)\n\t} else {\n\t\tpathList = append(pathList, filepath.Join(configPath, \"Rancher Desktop\"))\n\t}\n\n\tif removeKubernetesCache {\n\t\tpathList = append(pathList, appPaths.Cache)\n\t} else {\n\t\tpathList = append(pathList, filepath.Join(appPaths.Cache, \"updater-longhorn.json\"))\n\t}\n\tpathList = append(pathList, appHomeDirectories(appPaths)...)\n\treturn deleteUnixLikeData(ctx, appPaths, pathList)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data_unix.go",
    "content": "//go:build unix\n\n/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage factoryreset\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\tdockerconfig \"github.com/docker/cli/cli/config\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\n// appHomeDirectories() returns the path to the AppHome directory,\n// if it can be deleted. There may be some subdirectories inside it\n// that need to be preserved across a factory reset, so if any of\n// those exist and are non-empty, then a list of all files/directories\n// that don't match the exclusion list will be returned instead.\nfunc appHomeDirectories(appPaths *paths.Paths) []string {\n\t// Use lowercase names for comparison in case the user created the subdirectory manually\n\t// with the wrong case on a case-preserving filesystem (default on macOS).\n\texcludeDir := map[string]string{\n\t\tstrings.ToLower(appPaths.Snapshots):       appPaths.Snapshots,\n\t\tstrings.ToLower(appPaths.ContainerdShims): appPaths.ContainerdShims,\n\t}\n\thaveExclusions := false\n\tfor _, dirname := range excludeDir {\n\t\tfiles, err := os.ReadDir(dirname)\n\t\tif err == nil && len(files) > 0 {\n\t\t\thaveExclusions = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !haveExclusions {\n\t\treturn []string{appPaths.AppHome}\n\t}\n\tappHomeFiles, err := os.ReadDir(appPaths.AppHome)\n\tif err != nil {\n\t\tlogrus.Errorf(\"failed to read contents of dir %s: %s\", appPaths.AppHome, err)\n\t\treturn []string{}\n\t}\n\tpathList := make([]string, 0, len(appHomeFiles))\n\tfor _, file := range appHomeFiles {\n\t\tfullname := strings.ToLower(filepath.Join(appPaths.AppHome, file.Name()))\n\t\tif _, ok := excludeDir[fullname]; !ok {\n\t\t\tpathList = append(pathList, fullname)\n\t\t}\n\t}\n\treturn pathList\n}\n\n// Most of the errors in this function are reported, but we continue to try to delete things,\n// because there isn't really a dependency graph here.\n// For example, if we can't delete the Lima VM, that doesn't mean we can't remove docker files\n// or pull the path settings out of the shell profile files.\nfunc deleteUnixLikeData(ctx context.Context, appPaths *paths.Paths, pathList []string) error {\n\tif err := deleteLimaVM(ctx); err != nil {\n\t\tlogrus.Errorf(\"Error trying to delete the Lima VM: %s\\n\", err)\n\t}\n\tfor _, currentPath := range pathList {\n\t\tif err := os.RemoveAll(currentPath); err != nil {\n\t\t\tlogrus.Errorf(\"Error trying to remove %s: %s\", currentPath, err)\n\t\t}\n\t}\n\tif err := clearDockerContext(); err != nil {\n\t\tlogrus.Errorf(\"Error trying to clear the docker context %s\", err)\n\t}\n\tif err := removeDockerCliPlugins(appPaths.AltAppHome); err != nil {\n\t\tlogrus.Errorf(\"Error trying to remove docker plugins %s\", err)\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\t// If we can't get home directory, none of the below code is valid\n\t\tlogrus.Errorf(\"Error trying to get home dir: %s\", err)\n\t\treturn nil\n\t}\n\trawPaths := []string{\n\t\t\".bashrc\",\n\t\t\".bash_profile\",\n\t\t\".bash_login\",\n\t\t\".profile\",\n\t\t\".zshrc\",\n\t\t\".cshrc\",\n\t\t\".tcshrc\",\n\t}\n\tfor i, s := range rawPaths {\n\t\trawPaths[i] = path.Join(homeDir, s)\n\t}\n\trawPaths = append(rawPaths, path.Join(homeDir, \".config\", \"fish\", \"config.fish\"))\n\n\treturn removePathManagement(rawPaths)\n}\n\nfunc removeDockerCliPlugins(altAppHomePath string) error {\n\tcliPluginsDir := path.Join(dockerconfig.Dir(), \"cli-plugins\")\n\tentries, err := os.ReadDir(cliPluginsDir)\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\t// Nothing left to do here, since there is no cli-plugins dir\n\t\t\treturn nil\n\t\t}\n\t\treturn err\n\t}\n\tfor _, entry := range entries {\n\t\tif entry.Type()&os.ModeSymlink != os.ModeSymlink {\n\t\t\tcontinue\n\t\t}\n\t\tfullPathName := path.Join(cliPluginsDir, entry.Name())\n\t\ttarget, err := os.Readlink(fullPathName)\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"Failed to follow the symbolic link for file %s: error: %s\\n\", fullPathName, err)\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(target, path.Join(altAppHomePath, \"bin\")+\"/\") {\n\t\t\tos.Remove(fullPathName)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc removePathManagement(dotFiles []string) error {\n\tconst startTarget = `### MANAGED BY RANCHER DESKTOP START \\(DO NOT EDIT\\)`\n\tconst endTarget = `### MANAGED BY RANCHER DESKTOP END \\(DO NOT EDIT\\)`\n\n\t// bash files etc. break if they contain \\r's, so don't worry about them\n\tptn := regexp.MustCompile(fmt.Sprintf(`(?ms)^(?P<preMarkerText>.*?)(?P<preMarkerNewlines>\\n*)^%s.*?^%s\\s*?$(?P<postMarkerNewlines>\\n*)(?P<postMarkerText>.*)$`, startTarget, endTarget))\n\n\tfor _, dotFile := range dotFiles {\n\t\tbyteContents, err := os.ReadFile(dotFile)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\t\tlogrus.Errorf(\"Error trying to read %s: %s\\n\", dotFile, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tcontents := string(byteContents)\n\t\tparts := ptn.FindStringSubmatch(contents)\n\t\tif len(parts) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpreMarkerTextIndex := ptn.SubexpIndex(\"preMarkerText\")\n\t\tpreMarkerNewlineIndex := ptn.SubexpIndex(\"preMarkerNewlines\")\n\t\tpostMarkerNewlineIndex := ptn.SubexpIndex(\"postMarkerNewlines\")\n\t\tpostMarkerTextIndex := ptn.SubexpIndex(\"postMarkerText\")\n\t\tif parts[preMarkerTextIndex] == \"\" && parts[postMarkerTextIndex] == \"\" {\n\t\t\t// Nothing of interest left in this file, so delete it\n\t\t\terr = os.RemoveAll(dotFile)\n\t\t\tif err != nil {\n\t\t\t\t// but continue processing the other files\n\t\t\t\tlogrus.Errorf(\"Failed to delete file %s (error %s)\\n\", dotFile, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tnewParts := []string{parts[preMarkerTextIndex]}\n\n\t\tpreMarkerNewlines := parts[preMarkerNewlineIndex]\n\t\tpostMarkerNewlines := parts[postMarkerNewlineIndex]\n\t\tif len(preMarkerNewlines) == 1 {\n\t\t\tnewParts = append(newParts, preMarkerNewlines)\n\t\t} else if len(preMarkerNewlines) > 1 {\n\t\t\t// One of the newlines was inserted by the dotfile manager, but keep the others\n\t\t\tnewParts = append(newParts, preMarkerNewlines[1:])\n\t\t}\n\t\tif parts[postMarkerTextIndex] != \"\" {\n\t\t\tif len(postMarkerNewlines) > 1 {\n\t\t\t\t// Either there was a newline before the marker block, and we have copied\n\t\t\t\t// it into the new file,\n\t\t\t\t// or the marker block was at the start of the file, in which case we can\n\t\t\t\t// drop one of the post-marker block newlines\n\t\t\t\tnewParts = append(newParts, postMarkerNewlines[1:])\n\t\t\t}\n\t\t\tnewParts = append(newParts, parts[postMarkerTextIndex])\n\t\t}\n\t\tnewContents := strings.Join(newParts, \"\")\n\t\tfilestat, err := os.Stat(dotFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"error trying to stat %q: %w\", dotFile, err)\n\t\t}\n\t\tif err = os.WriteFile(dotFile, []byte(newContents), filestat.Mode()); err != nil {\n\t\t\tlogrus.Errorf(\"error trying to update %s: %s\\n\", dotFile, err)\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data_unix_test.go",
    "content": "//go:test !windows\n//go:build !windows\n\n/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage factoryreset\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n/**\n * Copy all the dotfiles we care about into TMP/rd-dotfiles-copies and TMP/rd-dotfiles-working\n * Add fake management blocks to each file in TMP/rd-dotfiles-working\n * One of the files deliberately has no newline between its last line and the management block\n * Then call the removePathManagement() function on the working dot files\n * Then verify they're identical to the copied files\n * Clean up the temp dirs if everything matched\n */\n\nvar tempOriginalDotfilesDir string\nvar tempWorkingDotfilesDir string\n\nvar filenames []string\nvar predefinedContents map[string]string = map[string]string{\n\t\"ends-with-text-eol\":         \"# line1a\\n# line2a\\n\",\n\t\"ends-with-blank-eol\":        \"# line1b\\n# line2b\\n\\n\",\n\t\"ends-with-no-eol\":           \"# line1c\\n# line2c\",\n\t\"will-have-no-extra-newline\": \"# line1d\\n# line2d\\n\",\n\t\"content-no-EOF-newline\":     \"# line1d\\n# line2d\\n\",\n\t\"empty-file-no-EOF-newline\":  \"\",\n\t\"empty-file\":                 \"\",\n}\n\nvar expectedAfterContents map[string]string = map[string]string{\n\t\"ends-with-no-eol\": \"# line1c\\n# line2c\\n\",\n}\n\nfunc getExpectedContents(filename string) (string, error) {\n\ttext, ok := expectedAfterContents[filename]\n\tif ok {\n\t\treturn text, nil\n\t}\n\ttext, ok = predefinedContents[filename]\n\tif ok {\n\t\treturn text, nil\n\t}\n\treturn \"\", fmt.Errorf(\"Can't find contents for dotfile %q\", filename)\n}\n\nfunc populateFiles() error {\n\tfor baseName, text := range predefinedContents {\n\t\tfullPath := path.Join(tempWorkingDotfilesDir, baseName)\n\t\tf, err := os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.Write([]byte(text))\n\t\tfilenames = append(filenames, fullPath)\n\t\tf.Close()\n\t\t// And write the data into the original dir for reference\n\t\tfullPath = path.Join(tempOriginalDotfilesDir, baseName)\n\t\tf, err = os.OpenFile(fullPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tf.Write([]byte(text))\n\t\tf.Close()\n\t}\n\treturn nil\n}\n\nfunc setup() error {\n\ttempOriginalDotfilesDir = path.Join(os.TempDir(), \"rd-dotfiles-copies\")\n\terr := os.Mkdir(tempOriginalDotfilesDir, 0755)\n\tif err != nil && !os.IsExist(err) {\n\t\treturn err\n\t}\n\ttempWorkingDotfilesDir = path.Join(os.TempDir(), \"rd-dotfiles-working\")\n\terr = os.Mkdir(tempWorkingDotfilesDir, 0755)\n\tif err != nil && !os.IsExist(err) {\n\t\treturn err\n\t}\n\treturn populateFiles()\n}\n\nfunc shutdown() {\n\tfor _, dir := range []string{tempOriginalDotfilesDir, tempWorkingDotfilesDir} {\n\t\terr := os.RemoveAll(dir)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"Failed to delete tmpdir %s: %s\\n\", dir, err)\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc TestMain(m *testing.M) {\n\tif err := setup(); err != nil {\n\t\tfmt.Println(\"Failed to setup...\")\n\t\tfmt.Println(err)\n\t\tos.Exit(1)\n\t}\n\tcode := m.Run()\n\tif code != 0 {\n\t\tos.Exit(code)\n\t}\n\tshutdown()\n}\n\nconst startTarget = \"### MANAGED BY RANCHER DESKTOP START (DO NOT EDIT)\"\nconst endTarget = \"### MANAGED BY RANCHER DESKTOP END (DO NOT EDIT)\"\n\nfunc addBlock(dotFile string) error {\n\tbyteContents, err := os.ReadFile(dotFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcontents := string(byteContents)\n\tstartPoint := strings.LastIndex(contents, startTarget)\n\tif startPoint >= 0 {\n\t\treturn nil\n\t}\n\t// Overwrite the file...\n\tfilestat, err := os.Stat(dotFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\tf, err := os.OpenFile(dotFile, os.O_APPEND|os.O_WRONLY, filestat.Mode())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(contents) > 0 && contents[len(contents)-1] != '\\n' {\n\t\tf.Write([]byte(\"\\n\"))\n\t}\n\tif len(contents) > 0 && !strings.HasSuffix(dotFile, \"will-have-no-extra-newline\") {\n\t\tf.Write([]byte(\"\\n\"))\n\t}\n\tf.Write([]byte(startTarget))\n\tf.Write([]byte(\"\\n\"))\n\tf.Write([]byte(\"# SHAZBAT!\\n\"))\n\tf.Write([]byte(endTarget))\n\tif !strings.HasSuffix(dotFile, \"no-EOF-newline\") {\n\t\tf.Write([]byte(\"\\n\"))\n\t}\n\treturn f.Close()\n}\n\nfunc TestAddManagedBlock(t *testing.T) {\n\tfor _, path := range filenames {\n\t\tassert.NoError(t, addBlock(path))\n\t}\n\tfor _, filename := range filenames {\n\t\tcontents, err := os.ReadFile(filename)\n\t\tassert.NoError(t, err)\n\t\tassert.Contains(t, string(contents), \"MANAGED BY RANCHER DESKTOP\")\n\t}\n}\n\nfunc verifyMgmtRemoved(t *testing.T, dotFile string) {\n\tbaseName := path.Base(dotFile)\n\tif strings.HasPrefix(baseName, \"empty-file\") {\n\t\t_, err := os.Stat(dotFile)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t\treturn\n\t}\n\tbyteContents, err := os.ReadFile(dotFile)\n\tassert.NoError(t, err)\n\texpectedContents, err := getExpectedContents(path.Base(dotFile))\n\tassert.NoError(t, err)\n\tassert.Equal(t, expectedContents, string(byteContents))\n}\n\nfunc TestRemoveManagedBlock(t *testing.T) {\n\tmodifiedFileList := append(filenames, path.Join(tempWorkingDotfilesDir, \".no-such-file\"))\n\tassert.NoError(t, removePathManagement(modifiedFileList))\n\tfor _, dotFile := range filenames {\n\t\tverifyMgmtRemoved(t, dotFile)\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/delete_data_windows.go",
    "content": "package factoryreset\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/autostart\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl\"\n)\n\nfunc DeleteData(ctx context.Context, appPaths *paths.Paths, removeKubernetesCache bool) error {\n\tif err := autostart.EnsureAutostart(ctx, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to remove autostart configuration: %s\", err)\n\t}\n\tif err := deleteLimaVM(ctx); err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\tlogrus.Errorf(\"Failed to delete Lima VM: %s\", err)\n\t}\n\tw := wsl.WSLImpl{}\n\tif err := w.UnregisterDistros(ctx); err != nil {\n\t\tlogrus.Errorf(\"could not unregister WSL: %s\", err)\n\t\treturn err\n\t}\n\tif err := process.TerminateProcessInDirectory(appPaths.ExtensionRoot, false); err != nil {\n\t\tlogrus.Errorf(\"Failed to stop extension processes, ignoring: %s\", err)\n\t}\n\tif err := deleteWindowsData(!removeKubernetesCache, \"rancher-desktop\"); err != nil {\n\t\tlogrus.Errorf(\"could not delete data: %s\", err)\n\t\treturn err\n\t}\n\tif err := clearDockerContext(); err != nil {\n\t\tlogrus.Errorf(\"could not clear docker context: %s\", err)\n\t\treturn err\n\t}\n\tlogrus.Infoln(\"successfully cleared data.\")\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/factory_reset_unix.go",
    "content": "//go:build unix\n\n/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage factoryreset\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\nfunc CheckProcessWindows(ctx context.Context) (bool, error) {\n\treturn false, fmt.Errorf(\"internal error: CheckProcessWindows shouldn't be called\")\n}\n\nfunc KillRancherDesktop(ctx context.Context) error {\n\treturn fmt.Errorf(\"internal error: KillRancherDesktop shouldn't be called\")\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/factoryreset/factory_reset_windows.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage factoryreset\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/csv\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\n// CheckProcessWindows - returns true if Rancher Desktop is still running, false if it isn't\n// along with an error condition if there's a problem detecting that.\n//\n// It does this by calling `tasklist`, the Windows answer to ps(1)\n\nfunc CheckProcessWindows(ctx context.Context) (bool, error) {\n\tcmd := exec.CommandContext(ctx, \"tasklist\", \"/NH\", \"/FI\", \"IMAGENAME eq Rancher Desktop.exe\", \"/FO\", \"CSV\")\n\tcmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: windows.CREATE_NO_WINDOW}\n\tallOutput, err := cmd.CombinedOutput()\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to run %q: %w\", cmd, err)\n\t}\n\tr := csv.NewReader(bytes.NewReader(allOutput))\n\tfor {\n\t\trecord, err := r.Read()\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\treturn false, fmt.Errorf(\"failed to csv-read the output for tasklist: %w\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif len(record) > 0 && record[0] == \"Rancher Desktop.exe\" {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\treturn false, nil\n}\n\n// KillRancherDesktop terminates all processes where the executable is from the\n// Rancher Desktop application, excluding the current process.\nfunc KillRancherDesktop(ctx context.Context) error {\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find application directory: %w\", err)\n\t}\n\n\terr = process.TerminateProcessInDirectory(appDir, true)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc deleteWindowsData(keepSystemImages bool, appName string) error {\n\tdirs, err := getDirectoriesToDelete(keepSystemImages, appName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor _, dir := range dirs {\n\t\tlogrus.WithField(\"path\", dir).Trace(\"Removing directory\")\n\t\tif err := os.RemoveAll(dir); err != nil {\n\t\t\tlogrus.Errorf(\"Problem trying to delete %s: %s\\n\", dir, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc getDirectoriesToDelete(keepSystemImages bool, appName string) ([]string, error) {\n\t// Ordered from least important to most, so that if delete fails we\n\t// still keep some useful data.\n\tlocalAppData, err := directories.GetLocalAppDataDirectory()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not get LocalAppData folder: %w\", err)\n\t}\n\tdirs := []string{filepath.Join(localAppData, fmt.Sprintf(\"%s-updater\", appName))}\n\tlocalRDAppData := filepath.Join(localAppData, appName)\n\n\t// add files in %LOCALAPPDATA%\\rancher-desktop\n\tdeleteLocalRDAppData := true\n\tappDataFiles, err := os.ReadDir(localRDAppData)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\treturn dirs, nil\n\t} else if err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read directory %q: %w\", localRDAppData, err)\n\t}\n\tfor _, appDataFile := range appDataFiles {\n\t\tfileName := appDataFile.Name()\n\t\tif fileName == \"snapshots\" {\n\t\t\t// Only delete snapshots directory if it is empty\n\t\t\tsnapshotsDir := filepath.Join(localRDAppData, fileName)\n\t\t\tsnapshotsDirContents, err := os.ReadDir(snapshotsDir)\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to read directory %q: %w\", snapshotsDir, err)\n\t\t\t}\n\t\t\tif len(snapshotsDirContents) == 0 {\n\t\t\t\tdirs = append(dirs, snapshotsDir)\n\t\t\t} else {\n\t\t\t\tdeleteLocalRDAppData = false\n\t\t\t}\n\t\t} else if fileName == \"containerd-shims\" {\n\t\t\t// Only delete containerd-shims directory if it is empty\n\t\t\tshimsDir := filepath.Join(localRDAppData, fileName)\n\t\t\tshimsDirContents, err := os.ReadDir(shimsDir)\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to read directory %q: %w\", shimsDir, err)\n\t\t\t}\n\t\t\tif len(shimsDirContents) == 0 {\n\t\t\t\tdirs = append(dirs, shimsDir)\n\t\t\t} else {\n\t\t\t\tdeleteLocalRDAppData = false\n\t\t\t}\n\t\t} else if fileName == \"cache\" && keepSystemImages {\n\t\t\t// Don't delete cache\\k3s & cache\\k3s-versions.json if keeping system images\n\t\t\tcacheDir := filepath.Join(localRDAppData, fileName)\n\t\t\tcacheDirFiles, err := os.ReadDir(cacheDir)\n\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to read directory %q: %w\", cacheDir, err)\n\t\t\t}\n\t\t\tfor _, cacheDirFile := range cacheDirFiles {\n\t\t\t\tcacheFileName := cacheDirFile.Name()\n\t\t\t\tif cacheFileName != \"k3s\" && cacheFileName != \"k3s-versions.json\" {\n\t\t\t\t\tdirs = append(dirs, filepath.Join(cacheDir, cacheFileName))\n\t\t\t\t}\n\t\t\t}\n\t\t\tdeleteLocalRDAppData = false\n\t\t} else {\n\t\t\tdirs = append(dirs, filepath.Join(localRDAppData, fileName))\n\t\t}\n\t}\n\tif deleteLocalRDAppData {\n\t\tdirs = append(dirs, localRDAppData)\n\t}\n\troamingAppData, err := directories.GetRoamingAppDataDirectory()\n\tif err == nil {\n\t\tdirs = append(dirs,\n\t\t\tfilepath.Join(roamingAppData, appName),\n\t\t\t// Electron stores some files in AppData\\Roaming\\Rancher Desktop\n\t\t\tfilepath.Join(roamingAppData, \"Rancher Desktop\"))\n\t} else {\n\t\tlogrus.Errorf(\"Could not get AppData (roaming) folder: %s\\n\", err)\n\t}\n\t// The OldUserData directory is already deleted by deleting the Cache directory.\n\treturn dirs, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/info/ipaddress.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage info\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/shell\"\n)\n\ntype interfaceInfo struct {\n\tInterfaceName string `json:\"ifname\"`\n\tFlags         []string\n\tState         string `json:\"operstate\"`\n\tMACAddress    string `json:\"address\"`\n\tAddresses     []struct {\n\t\tFamily       string\n\t\tLocal        string\n\t\tPrefixLength uint `json:\"prefixlen\"`\n\t\tBroadcast    string\n\t\tScope        string\n\t} `json:\"addr_info\"`\n}\n\nfunc getIPAddress(ctx context.Context, result *Info, _ client.RDClient) error {\n\tcmd, err := shell.SpawnCommand(ctx, \"ip\", \"-json\", \"address\", \"show\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar buf bytes.Buffer\n\tcmd.Stdout = &buf\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn err\n\t}\n\n\tvar interfaces []interfaceInfo\n\tif err := json.Unmarshal(buf.Bytes(), &interfaces); err != nil {\n\t\treturn err\n\t}\n\n\t// The list of interface names to try, varying by OS.\n\tinterfaceNames := map[string][]string{\n\t\t\"darwin\":  {\"rd0\", \"vznat\", \"rd1\", \"eth0\"},\n\t\t\"linux\":   {\"eth0\"},\n\t\t\"windows\": {\"eth0\"},\n\t}[runtime.GOOS]\n\n\tfor _, ifaceName := range interfaceNames {\n\t\tfor _, iface := range interfaces {\n\t\t\tif iface.InterfaceName != ifaceName {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfor _, addr := range iface.Addresses {\n\t\t\t\tif addr.Family == \"inet\" && addr.Scope == \"global\" {\n\t\t\t\t\tresult.IPAddress = addr.Local\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"failed to find IP address\")\n}\n\nfunc init() {\n\tregister(\"ip-address\", getIPAddress)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/info/struct.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage info\n\nimport (\n\t\"context\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n)\n\n// Info describes the output `rdctl info` will generate when run with no\n// special options.\ntype Info struct {\n\tVersion   string `json:\"version\" help:\"Rancher Desktop application version\"`\n\tIPAddress string `json:\"ip-address\" help:\"IP address to use to contact the VM\"`\n}\n\n// HandlerFunc is the generic interface to populate the [Info] result structure.\n// The function is expected to fill in the fields it knows.\n// The given client may be `nil` if the configuration is invalid.\ntype HandlerFunc func(context.Context, *Info, client.RDClient) error\n\n// Handlers that have been registered; the key should be the same as the JSON\n// field tag (on struct [Info]).\nvar Handlers map[string]HandlerFunc\n\n// Register a handler for a given field.\nfunc register(name string, handler HandlerFunc) {\n\tif Handlers == nil {\n\t\tHandlers = make(map[string]HandlerFunc)\n\t}\n\tHandlers[name] = handler\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/info/version.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage info\n\nimport (\n\t\"context\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/version\"\n)\n\nfunc getVersion(ctx context.Context, result *Info, _ client.RDClient) error {\n\tresult.Version = version.Version\n\treturn nil\n}\n\nfunc init() {\n\tregister(\"version\", getVersion)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/lima/name.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package lima contains the constants related to running Rancher Desktop using\n// lima (i.e. darwin / Linux).\npackage lima\n\nconst (\n\t// The name of the lima instance, without the `lima-` prefix.\n\tInstanceName = \"0\"\n\t// The name of the lima instance, including the `lima-` prefix.\n\tInstanceFullName = \"lima-\" + InstanceName\n)\n"
  },
  {
    "path": "src/go/rdctl/pkg/lock/lock.go",
    "content": "package lock\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/client\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/config\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nconst backendLockName = \"backend.lock\"\n\ntype BackendLocker interface {\n\tLock(ctx context.Context, appPaths *paths.Paths, action string) error\n\tUnlock(ctx context.Context, appPaths *paths.Paths, restart bool) error\n}\n\ntype BackendLock struct {\n}\n\ntype LockData struct {\n\tAction string `json:\"action\"`\n}\n\n// Lock the backend by creating the lock file and shutting down the VM.\n// The lock file will be deleted if Lock returns an error (e.g. the backend couldn't be stopped).\nfunc (lock *BackendLock) Lock(ctx context.Context, appPaths *paths.Paths, action string) error {\n\tif err := os.MkdirAll(appPaths.AppHome, 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create backend lock parent directory %q: %w\", appPaths.AppHome, err)\n\t}\n\t// Create an empty file whose presence signifies that the backend is locked.\n\tlockPath := filepath.Join(appPaths.AppHome, backendLockName)\n\tfile, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0o644)\n\tif errors.Is(err, os.ErrExist) {\n\t\treturn errors.New(\"backend lock file already exists; if there is no snapshot operation in progress, you can remove this error with `rdctl snapshot unlock`\")\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"unexpected error acquiring backend lock: %w\", err)\n\t}\n\n\tlockData := LockData{\n\t\tAction: action,\n\t}\n\tencoder := json.NewEncoder(file)\n\tencoder.SetIndent(\"\", \"  \")\n\tif err := encoder.Encode(lockData); err != nil {\n\t\t_ = file.Close()\n\t\t_ = os.Remove(lockPath)\n\t\treturn fmt.Errorf(\"failed to write metadata file: %w\", err)\n\t}\n\n\tif err := file.Close(); err != nil {\n\t\t_, _ = fmt.Fprintf(os.Stderr, \"failed to close backend lock file descriptor: %s\", err)\n\t}\n\terr = ensureBackendStopped(ctx, action)\n\tif err != nil {\n\t\t_ = os.Remove(lockPath)\n\t}\n\treturn err\n}\n\n// Unlock the backend by removing the lock file. Restart the VM if the file was deleted and `restart` is true.\nfunc (lock *BackendLock) Unlock(ctx context.Context, appPaths *paths.Paths, restart bool) error {\n\tlockPath := filepath.Join(appPaths.AppHome, backendLockName)\n\terr := os.RemoveAll(lockPath)\n\tif err == nil && restart {\n\t\terr = ensureBackendStarted(ctx)\n\t}\n\treturn err\n}\n\nfunc ensureBackendStarted(ctx context.Context) error {\n\tconnectionInfo, err := config.GetConnectionInfo(true)\n\tif err != nil || connectionInfo == nil {\n\t\treturn err\n\t}\n\trdClient := client.NewRDClient(connectionInfo)\n\tdesiredState := client.BackendState{\n\t\tVMState: \"STARTED\",\n\t\tLocked:  false,\n\t}\n\terr = rdClient.UpdateBackendState(ctx, desiredState)\n\tif err != nil && !errors.Is(err, client.ErrConnectionRefused) {\n\t\treturn fmt.Errorf(\"failed to restart backend: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc ensureBackendStopped(ctx context.Context, action string) error {\n\tconnectionInfo, err := config.GetConnectionInfo(true)\n\tif err != nil || connectionInfo == nil {\n\t\treturn err\n\t}\n\n\t// Ensure backend is running if the main process is running at all\n\trdClient := client.NewRDClient(connectionInfo)\n\tstate, err := rdClient.GetBackendState(ctx)\n\tif errors.Is(err, client.ErrConnectionRefused) {\n\t\t// If we cannot connect to the server, assume that the main\n\t\t// process is not running.\n\t\treturn nil\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"failed to get backend state: %w\", err)\n\t}\n\tif state.VMState != \"STARTED\" && state.VMState != \"DISABLED\" {\n\t\treturn fmt.Errorf(\"Rancher Desktop state is %v. It must be fully running or fully shut down to perform the action: %s\", state.VMState, action)\n\t}\n\n\t// Stop and lock the backend\n\tdesiredState := client.BackendState{\n\t\tVMState: \"STOPPED\",\n\t\tLocked:  true,\n\t}\n\tif err := rdClient.UpdateBackendState(ctx, desiredState); err != nil {\n\t\treturn fmt.Errorf(\"failed to stop backend: %w\", err)\n\t}\n\tif err := waitForVMState(ctx, rdClient, []string{\"STOPPED\"}); err != nil {\n\t\treturn fmt.Errorf(\"error waiting for backend to stop: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// Normally snapshots can be created at state STARTED or DISABLED\nfunc waitForVMState(ctx context.Context, rdClient client.RDClient, desiredStates []string) error {\n\tinterval := 1 * time.Second\n\tnumIntervals := 120\n\tfor range numIntervals {\n\t\tstate, err := rdClient.GetBackendState(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to poll backend state: %w\", err)\n\t\t}\n\t\tif slices.Contains(desiredStates, state.VMState) {\n\t\t\treturn nil\n\t\t}\n\t\ttime.Sleep(interval)\n\t}\n\treturn fmt.Errorf(\"timed out waiting for backend state in %s\", desiredStates)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/lock/mock.go",
    "content": "package lock\n\nimport (\n\t\"context\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\ntype MockBackendLock struct {\n}\n\nfunc (lock *MockBackendLock) Lock(ctx context.Context, appPaths *paths.Paths, action string) error {\n\treturn nil\n}\n\nfunc (lock *MockBackendLock) Unlock(ctx context.Context, appPaths *paths.Paths, restart bool) error {\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths.go",
    "content": "package paths\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils\"\n)\n\nconst appName = \"rancher-desktop\"\n\ntype Paths struct {\n\t// Main location for application data.\n\tAppHome string `json:\"appHome\"`\n\t// Secondary location for application data.\n\tAltAppHome string `json:\"altAppHome\"`\n\t// Directory which holds configuration.\n\tConfig string `json:\"config\"`\n\t// Directory which holds logs.\n\tLogs string `json:\"logs\"`\n\t// Directory which holds caches that may be removed.\n\tCache string `json:\"cache\"`\n\t// Directory holding the WSL distribution (Windows-specific).\n\tWslDistro string `json:\"wslDistro,omitempty\"`\n\t// Directory holding the WSL data distribution (Windows-specific).\n\tWslDistroData string `json:\"wslDistroData,omitempty\"`\n\t// Directory holding Lima state (Unix-specific).\n\tLima string `json:\"lima,omitempty\"`\n\t// Directory holding provided binary resources.\n\tIntegration string `json:\"integration,omitempty\"`\n\t// Directory that holds resource files in the RD installation.\n\tResources string `json:\"resources\"`\n\t// Deployment Profile System-wide startup settings path.\n\tDeploymentProfileSystem string `json:\"deploymentProfileSystem,omitempty\"`\n\t// Secondary Deployment Profile System-wide startup settings path.\n\tAltDeploymentProfileSystem string `json:\"altDeploymentProfileSystem,omitempty\"`\n\t// Deployment Profile User startup settings path.\n\tDeploymentProfileUser string `json:\"deploymentProfileUser,omitempty\"`\n\t// Directory that holds extension data.\n\tExtensionRoot string `json:\"extensionRoot\"`\n\t// Directory that holds snapshots\n\tSnapshots string `json:\"snapshots,omitempty\"`\n\t// Directory containing user-managed containerd-shims\n\tContainerdShims string `json:\"containerdShims,omitempty\"`\n\t// Previous location of Electron user data (e.g. cookies) up to Rancher Desktop 1.16.\n\t// Current location is `$AppHome/electron` and does not need special treatment.\n\tOldUserData string `json:\"oldUserData,omitempty\"`\n}\n\nvar rdctlPathOverride string\n\n// Get the path to the resources directory (the parent directory of the\n// platform-specific directory); this is used to fill in [Paths.Resources].\nfunc GetResourcesPath() (string, error) {\n\tvar rdctlPath string\n\tif rdctlPathOverride != \"\" {\n\t\trdctlPath = rdctlPathOverride\n\t} else {\n\t\trdctlSymlinkPath, err := os.Executable()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get path to rdctl: %w\", err)\n\t\t}\n\t\trdctlPath, err = filepath.EvalSymlinks(rdctlSymlinkPath)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to resolve %q: %w\", rdctlSymlinkPath, err)\n\t\t}\n\t}\n\treturn utils.GetParentDir(rdctlPath, 3), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_darwin.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc GetPaths(getResourcesPathFuncs ...func() (string, error)) (*Paths, error) {\n\tvar getResourcesPathFunc func() (string, error)\n\tswitch len(getResourcesPathFuncs) {\n\tcase 0:\n\t\tgetResourcesPathFunc = GetResourcesPath\n\tcase 1:\n\t\tgetResourcesPathFunc = getResourcesPathFuncs[0]\n\tdefault:\n\t\treturn nil, errors.New(\"you can only pass one function in getResourcesPathFuncs arg\")\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get user home directory: %w\", err)\n\t}\n\tappHome := filepath.Join(homeDir, \"Library\", \"Application Support\", appName)\n\taltAppHome := filepath.Join(homeDir, \".rd\")\n\tpaths := Paths{\n\t\tAppHome:                    appHome,\n\t\tAltAppHome:                 altAppHome,\n\t\tConfig:                     filepath.Join(homeDir, \"Library\", \"Preferences\", appName),\n\t\tCache:                      filepath.Join(homeDir, \"Library\", \"Caches\", appName),\n\t\tLima:                       filepath.Join(appHome, \"lima\"),\n\t\tIntegration:                filepath.Join(altAppHome, \"bin\"),\n\t\tDeploymentProfileSystem:    \"/Library/Managed Preferences\",\n\t\tAltDeploymentProfileSystem: \"/Library/Preferences\",\n\t\tDeploymentProfileUser:      filepath.Join(homeDir, \"Library\", \"Preferences\"),\n\t\tExtensionRoot:              filepath.Join(appHome, \"extensions\"),\n\t\tSnapshots:                  filepath.Join(appHome, \"snapshots\"),\n\t\tContainerdShims:            filepath.Join(appHome, \"containerd-shims\"),\n\t\tOldUserData:                filepath.Join(homeDir, \"Library\", \"Application Support\", \"Rancher Desktop\"),\n\t}\n\tpaths.Logs = os.Getenv(\"RD_LOGS_DIR\")\n\tif paths.Logs == \"\" {\n\t\tpaths.Logs = filepath.Join(homeDir, \"Library\", \"Logs\", appName)\n\t}\n\tpaths.Resources, err = getResourcesPathFunc()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find resources directory: %w\", err)\n\t}\n\n\treturn &paths, nil\n}\n\n// Return the path used to launch Rancher Desktop.\nfunc GetRDLaunchPath(ctx context.Context) (string, error) {\n\terrs := multierror.Append(nil, errors.New(\"search location exhausted\"))\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\texecutablePath := []string{\"Contents\", \"MacOS\", \"Rancher Desktop\"}\n\n\tfor _, dir := range []string{appDir, \"/Applications/Rancher Desktop.app\"} {\n\t\tabsPathParts := append([]string{dir}, executablePath...)\n\t\tok, err := checkUsableApplication(filepath.Join(absPathParts...), true)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif ok {\n\t\t\treturn dir, nil\n\t\t}\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"%s is not suitable\", dir))\n\t}\n\treturn \"\", errs.ErrorOrNil()\n}\n\n// Return the path to the main Rancher Desktop executable.\n// In the case of `yarn dev`, this would be the electron executable.\nfunc GetMainExecutable(ctx context.Context) (string, error) {\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\treturn FindFirstExecutable(\n\t\tfilepath.Join(appDir, \"Contents\", \"MacOS\", \"Rancher Desktop\"),\n\t\tfilepath.Join(appDir, \"node_modules\", \"electron\", \"dist\",\n\t\t\t\"Electron.app\", \"Contents\", \"MacOS\", \"Electron\"),\n\t)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_darwin_test.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc TestGetPaths(t *testing.T) {\n\tt.Run(\"should return correct paths without environment variables set\", func(t *testing.T) {\n\t\tt.Setenv(\"RD_LOGS_DIR\", \"\")\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:                    filepath.Join(homeDir, \"Library\", \"Application Support\", appName),\n\t\t\tAltAppHome:                 filepath.Join(homeDir, \".rd\"),\n\t\t\tConfig:                     filepath.Join(homeDir, \"Library\", \"Preferences\", appName),\n\t\t\tLogs:                       filepath.Join(homeDir, \"Library\", \"Logs\", appName),\n\t\t\tCache:                      filepath.Join(homeDir, \"Library\", \"Caches\", appName),\n\t\t\tLima:                       filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"lima\"),\n\t\t\tIntegration:                filepath.Join(homeDir, \".rd\", \"bin\"),\n\t\t\tResources:                  fakeResourcesPath,\n\t\t\tDeploymentProfileSystem:    filepath.Join(\"/Library\", \"Managed Preferences\"),\n\t\t\tAltDeploymentProfileSystem: filepath.Join(\"/Library\", \"Preferences\"),\n\t\t\tDeploymentProfileUser:      filepath.Join(homeDir, \"Library\", \"Preferences\"),\n\t\t\tExtensionRoot:              filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"extensions\"),\n\t\t\tSnapshots:                  filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"snapshots\"),\n\t\t\tContainerdShims:            filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"containerd-shims\"),\n\t\t\tOldUserData:                filepath.Join(homeDir, \"Library\", \"Application Support\", \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n\n\tt.Run(\"should return correct paths with environment variables set\", func(t *testing.T) {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\trdLogsDir := filepath.Join(homeDir, \"anotherLogsDir\")\n\t\tt.Setenv(\"RD_LOGS_DIR\", rdLogsDir)\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:                    filepath.Join(homeDir, \"Library\", \"Application Support\", appName),\n\t\t\tAltAppHome:                 filepath.Join(homeDir, \".rd\"),\n\t\t\tConfig:                     filepath.Join(homeDir, \"Library\", \"Preferences\", appName),\n\t\t\tLogs:                       rdLogsDir,\n\t\t\tCache:                      filepath.Join(homeDir, \"Library\", \"Caches\", appName),\n\t\t\tLima:                       filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"lima\"),\n\t\t\tIntegration:                filepath.Join(homeDir, \".rd\", \"bin\"),\n\t\t\tResources:                  fakeResourcesPath,\n\t\t\tDeploymentProfileSystem:    filepath.Join(\"/Library\", \"Managed Preferences\"),\n\t\t\tAltDeploymentProfileSystem: filepath.Join(\"/Library\", \"Preferences\"),\n\t\t\tDeploymentProfileUser:      filepath.Join(homeDir, \"Library\", \"Preferences\"),\n\t\t\tExtensionRoot:              filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"extensions\"),\n\t\t\tSnapshots:                  filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"snapshots\"),\n\t\t\tContainerdShims:            filepath.Join(homeDir, \"Library\", \"Application Support\", appName, \"containerd-shims\"),\n\t\t\tOldUserData:                filepath.Join(homeDir, \"Library\", \"Application Support\", \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n}\n\n// Given an application directory, create the rdctl executable at the expected\n// path and return its path.\nfunc makeRdctl(t *testing.T, appDir string) string {\n\trdctlPath := filepath.Join(appDir, \"Contents/Resources/resources/darwin/bin/rdctl\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(rdctlPath), 0o755))\n\trdctl, err := os.OpenFile(rdctlPath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, rdctl.Close())\n\treturn rdctlPath\n}\n\n// Given an application directory, create the main executable at the expected\n// path and return its path.\nfunc makeExecutable(t *testing.T, appDir string) string {\n\texecutablePath := filepath.Join(appDir, \"Contents/MacOS/Rancher Desktop\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\texecutable, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, executable.Close())\n\treturn executablePath\n}\n\nfunc TestGetRDLaunchPath(t *testing.T) {\n\tt.Run(\"from bundled application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\t_ = makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, dir, actual)\n\t})\n\tt.Run(\"from application directory\", func(t *testing.T) {\n\t\tappDir := \"/Applications/Rancher Desktop.app\"\n\t\texecutablePath := filepath.Join(appDir, \"Contents/MacOS/Rancher Desktop\")\n\t\tif _, err := os.Stat(executablePath); errors.Is(err, os.ErrNotExist) {\n\t\t\tt.Skip(\"Application does not exist\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, appDir, actual)\n\t})\n\tt.Run(\"fail to find suitable application\", func(t *testing.T) {\n\t\tappDir := \"/Applications/Rancher Desktop.app\"\n\t\texecutablePath := filepath.Join(appDir, \"Contents/MacOS/Rancher Desktop\")\n\t\tif _, err := os.Stat(executablePath); err == nil {\n\t\t\tt.Skip(\"Application exists\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\t_, err = GetRDLaunchPath(ctx)\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestGetMainExecutable(t *testing.T) {\n\tt.Run(\"packaged application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\texecutablePath := makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"development build\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\texecutablePath := filepath.Join(dir, \"node_modules/electron/dist/Electron.app/Contents/MacOS/Electron\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\t\tf, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, f.Close())\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_linux.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc GetPaths(getResourcesPathFuncs ...func() (string, error)) (*Paths, error) {\n\tvar getResourcesPathFunc func() (string, error)\n\tswitch len(getResourcesPathFuncs) {\n\tcase 0:\n\t\tgetResourcesPathFunc = GetResourcesPath\n\tcase 1:\n\t\tgetResourcesPathFunc = getResourcesPathFuncs[0]\n\tdefault:\n\t\treturn nil, errors.New(\"you can only pass one function in getResourcesPathFuncs arg\")\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get user home directory: %w\", err)\n\t}\n\tdataHome := os.Getenv(\"XDG_DATA_HOME\")\n\tif dataHome == \"\" {\n\t\tdataHome = filepath.Join(homeDir, \".local\", \"share\")\n\t}\n\tconfigHome := os.Getenv(\"XDG_CONFIG_HOME\")\n\tif configHome == \"\" {\n\t\tconfigHome = filepath.Join(homeDir, \".config\")\n\t}\n\tcacheHome := os.Getenv(\"XDG_CACHE_HOME\")\n\tif cacheHome == \"\" {\n\t\tcacheHome = filepath.Join(homeDir, \".cache\")\n\t}\n\taltAppHome := filepath.Join(homeDir, \".rd\")\n\tpaths := Paths{\n\t\tAppHome:     filepath.Join(dataHome, appName),\n\t\tAltAppHome:  altAppHome,\n\t\tConfig:      filepath.Join(configHome, appName),\n\t\tCache:       filepath.Join(cacheHome, appName),\n\t\tLima:        filepath.Join(dataHome, appName, \"lima\"),\n\t\tIntegration: filepath.Join(altAppHome, \"bin\"),\n\t\t//nolint:gocritic // filepathJoin doesn't like absolute paths\n\t\tDeploymentProfileSystem: filepath.Join(\"/etc\", appName),\n\t\t//nolint:gocritic // filepathJoin doesn't like absolute paths\n\t\tAltDeploymentProfileSystem: filepath.Join(\"/usr/etc\", appName),\n\t\tDeploymentProfileUser:      configHome,\n\t\tExtensionRoot:              filepath.Join(dataHome, appName, \"extensions\"),\n\t\tSnapshots:                  filepath.Join(dataHome, appName, \"snapshots\"),\n\t\tContainerdShims:            filepath.Join(dataHome, appName, \"containerd-shims\"),\n\t\tOldUserData:                filepath.Join(configHome, \"Rancher Desktop\"),\n\t}\n\tpaths.Logs = os.Getenv(\"RD_LOGS_DIR\")\n\tif paths.Logs == \"\" {\n\t\tpaths.Logs = filepath.Join(dataHome, appName, \"logs\")\n\t}\n\tpaths.Resources, err = getResourcesPathFunc()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find resources directory: %w\", err)\n\t}\n\n\treturn &paths, nil\n}\n\n// Return the path used to launch Rancher Desktop.\nfunc GetRDLaunchPath(ctx context.Context) (string, error) {\n\terrs := multierror.Append(nil, errors.New(\"search location exhausted\"))\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\tcandidatePaths := []string{\n\t\tfilepath.Join(appDir, \"rancher-desktop\"),\n\t\t\"/opt/rancher-desktop/rancher-desktop\",\n\t}\n\tfor _, candidatePath := range candidatePaths {\n\t\tusable, err := checkUsableApplication(candidatePath, true)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to check usability of %q: %w\", candidatePath, err)\n\t\t}\n\t\tif usable {\n\t\t\treturn candidatePath, nil\n\t\t}\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"%s is not suitable\", candidatePath))\n\t}\n\treturn \"\", errs.ErrorOrNil()\n}\n\n// Return the path to the main Rancher Desktop executable.\n// In the case of `yarn dev`, this would be the electron executable.\nfunc GetMainExecutable(ctx context.Context) (string, error) {\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\treturn FindFirstExecutable(\n\t\tfilepath.Join(appDir, \"rancher-desktop\"),\n\t\tfilepath.Join(appDir, \"node_modules\", \"electron\", \"dist\", \"electron\"),\n\t)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_linux_test.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc TestGetPaths(t *testing.T) {\n\tt.Run(\"should return correct paths without environment variables set\", func(t *testing.T) {\n\t\t// Ensure that these variables are not set in the testing environment\n\t\tenvironment := map[string]string{\n\t\t\t\"RD_LOGS_DIR\":     \"\",\n\t\t\t\"XDG_DATA_HOME\":   \"\",\n\t\t\t\"XDG_CONFIG_HOME\": \"\",\n\t\t\t\"XDG_CACHE_HOME\":  \"\",\n\t\t}\n\t\tfor key, value := range environment {\n\t\t\tt.Setenv(key, value)\n\t\t}\n\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:                    filepath.Join(homeDir, \".local/share\", appName),\n\t\t\tAltAppHome:                 filepath.Join(homeDir, \".rd\"),\n\t\t\tConfig:                     filepath.Join(homeDir, \".config\", appName),\n\t\t\tLogs:                       filepath.Join(homeDir, \".local/share\", appName, \"logs\"),\n\t\t\tCache:                      filepath.Join(homeDir, \".cache\", appName),\n\t\t\tLima:                       filepath.Join(homeDir, \".local/share\", appName, \"lima\"),\n\t\t\tIntegration:                filepath.Join(homeDir, \".rd/bin\"),\n\t\t\tResources:                  fakeResourcesPath,\n\t\t\tDeploymentProfileSystem:    filepath.Join(\"/etc\", appName),\n\t\t\tAltDeploymentProfileSystem: filepath.Join(\"/usr/etc\", appName),\n\t\t\tDeploymentProfileUser:      filepath.Join(homeDir, \".config\"),\n\t\t\tExtensionRoot:              filepath.Join(homeDir, \".local/share\", appName, \"extensions\"),\n\t\t\tSnapshots:                  filepath.Join(homeDir, \".local/share\", appName, \"snapshots\"),\n\t\t\tContainerdShims:            filepath.Join(homeDir, \".local/share\", appName, \"containerd-shims\"),\n\t\t\tOldUserData:                filepath.Join(homeDir, \".config\", \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n\n\tt.Run(\"should return correct paths with environment variables set\", func(t *testing.T) {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\tenvironment := map[string]string{\n\t\t\t\"RD_LOGS_DIR\":     filepath.Join(homeDir, \"anotherLogsDir\"),\n\t\t\t\"XDG_DATA_HOME\":   filepath.Join(homeDir, \"anotherDataHome\"),\n\t\t\t\"XDG_CONFIG_HOME\": filepath.Join(homeDir, \"anotherConfigHome\"),\n\t\t\t\"XDG_CACHE_HOME\":  filepath.Join(homeDir, \"anotherCacheHome\"),\n\t\t}\n\t\tfor key, value := range environment {\n\t\t\tt.Setenv(key, value)\n\t\t}\n\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:                    filepath.Join(environment[\"XDG_DATA_HOME\"], appName),\n\t\t\tAltAppHome:                 filepath.Join(homeDir, \".rd\"),\n\t\t\tConfig:                     filepath.Join(environment[\"XDG_CONFIG_HOME\"], appName),\n\t\t\tLogs:                       environment[\"RD_LOGS_DIR\"],\n\t\t\tCache:                      filepath.Join(environment[\"XDG_CACHE_HOME\"], appName),\n\t\t\tLima:                       filepath.Join(environment[\"XDG_DATA_HOME\"], appName, \"lima\"),\n\t\t\tIntegration:                filepath.Join(homeDir, \".rd/bin\"),\n\t\t\tResources:                  fakeResourcesPath,\n\t\t\tDeploymentProfileSystem:    filepath.Join(\"/etc\", appName),\n\t\t\tAltDeploymentProfileSystem: filepath.Join(\"/usr/etc\", appName),\n\t\t\tDeploymentProfileUser:      environment[\"XDG_CONFIG_HOME\"],\n\t\t\tExtensionRoot:              filepath.Join(environment[\"XDG_DATA_HOME\"], appName, \"extensions\"),\n\t\t\tSnapshots:                  filepath.Join(environment[\"XDG_DATA_HOME\"], appName, \"snapshots\"),\n\t\t\tContainerdShims:            filepath.Join(environment[\"XDG_DATA_HOME\"], appName, \"containerd-shims\"),\n\t\t\tOldUserData:                filepath.Join(environment[\"XDG_CONFIG_HOME\"], \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n}\n\n// Given an application directory, create the rdctl executable at the expected\n// path and return its path.\nfunc makeRdctl(t *testing.T, appDir string) string {\n\trdctlPath := filepath.Join(appDir, \"resources/resources/linux/bin/rdctl\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(rdctlPath), 0o755))\n\trdctl, err := os.OpenFile(rdctlPath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, rdctl.Close())\n\treturn rdctlPath\n}\n\n// Given an application directory, create the main executable at the expected\n// path and return its path.\nfunc makeExecutable(t *testing.T, appDir string) string {\n\texecutablePath := filepath.Join(appDir, \"rancher-desktop\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\texecutable, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, executable.Close())\n\treturn executablePath\n}\n\nfunc TestGetRDLaunchPath(t *testing.T) {\n\tt.Run(\"from bundled application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\texecutablePath := makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"from application directory\", func(t *testing.T) {\n\t\tappDir := \"/opt/rancher-desktop\"\n\t\texecutablePath := filepath.Join(appDir, \"rancher-desktop\")\n\t\tif _, err := os.Stat(executablePath); errors.Is(err, os.ErrNotExist) {\n\t\t\tt.Skip(\"Application does not exist\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"fail to find suitable application\", func(t *testing.T) {\n\t\tappDir := \"/opt/rancher-desktop\"\n\t\texecutablePath := filepath.Join(appDir, \"rancher-desktop\")\n\t\tif _, err := os.Stat(executablePath); err == nil {\n\t\t\tt.Skip(\"Application exists\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\t_, err = GetRDLaunchPath(ctx)\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestGetMainExecutable(t *testing.T) {\n\tt.Run(\"packaged application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\texecutablePath := makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"development build\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\texecutablePath := filepath.Join(dir, \"node_modules/electron/dist/electron\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\t\tf, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, f.Close())\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_test.go",
    "content": "package paths\n\nimport (\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nconst fakeResourcesPath = \"fakePath\"\n\nfunc mockGetResourcesPath() (string, error) {\n\treturn fakeResourcesPath, nil\n}\n\nfunc TestGetResourcesPath(t *testing.T) {\n\tdir := t.TempDir()\n\trdctlPathOverride = filepath.Join(dir, \"resources\", runtime.GOOS, \"bin\", \"rdctl\")\n\tactual, err := GetResourcesPath()\n\tif assert.NoError(t, err) {\n\t\tassert.Equal(t, filepath.Join(dir, \"resources\"), actual)\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_unix.go",
    "content": "//go:build linux || darwin\n\npackage paths\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// Given a list of paths, return the first one that is a valid executable.\nfunc FindFirstExecutable(candidates ...string) (string, error) {\n\terrs := multierror.Append(nil, errors.New(\"search location exhausted\"))\n\tfor _, candidate := range candidates {\n\t\tusable, err := checkUsableApplication(candidate, true)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to check usability of %q: %w\", candidate, err)\n\t\t}\n\t\tif usable {\n\t\t\treturn candidate, nil\n\t\t}\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"%s is not suitable\", candidate))\n\t}\n\treturn \"\", errs.ErrorOrNil()\n}\n\n// Verify that the candidatePath is usable as a Rancher Desktop \"executable\". This means:\n//   - check that candidatePath exists\n//   - if checkExecutability is true, check that candidatePath is a regular file,\n//     and that it is executable\n//\n// Note that candidatePath may not always be a file; in macOS, it may be a\n// .app directory.\nfunc checkUsableApplication(candidatePath string, checkExecutability bool) (bool, error) {\n\tstatResult, err := os.Stat(candidatePath)\n\tif errors.Is(err, fs.ErrNotExist) {\n\t\treturn false, nil\n\t}\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"failed to get info on %q: %w\", candidatePath, err)\n\t}\n\n\tif !checkExecutability {\n\t\treturn true, nil\n\t}\n\n\tif !statResult.Mode().IsRegular() {\n\t\treturn false, nil\n\t}\n\n\terr = unix.Access(candidatePath, unix.X_OK)\n\treturn err == nil, nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_windows.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc GetPaths(getResourcesPathFuncs ...func() (string, error)) (*Paths, error) {\n\tvar getResourcesPathFunc func() (string, error)\n\tswitch len(getResourcesPathFuncs) {\n\tcase 0:\n\t\tgetResourcesPathFunc = GetResourcesPath\n\tcase 1:\n\t\tgetResourcesPathFunc = getResourcesPathFuncs[0]\n\tdefault:\n\t\treturn nil, errors.New(\"you can only pass one function in getResourcesPathFuncs arg\")\n\t}\n\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get user home directory: %w\", err)\n\t}\n\tlocalAppData := os.Getenv(\"LOCALAPPDATA\")\n\tif localAppData == \"\" {\n\t\tlocalAppData = filepath.Join(homeDir, \"AppData\", \"Local\")\n\t}\n\tappHome := filepath.Join(localAppData, appName)\n\tpaths := Paths{\n\t\tAppHome:         appHome,\n\t\tAltAppHome:      appHome,\n\t\tConfig:          appHome,\n\t\tCache:           filepath.Join(localAppData, appName, \"cache\"),\n\t\tWslDistro:       filepath.Join(localAppData, appName, \"distro\"),\n\t\tWslDistroData:   filepath.Join(localAppData, appName, \"distro-data\"),\n\t\tExtensionRoot:   filepath.Join(localAppData, appName, \"extensions\"),\n\t\tSnapshots:       filepath.Join(localAppData, appName, \"snapshots\"),\n\t\tContainerdShims: filepath.Join(localAppData, appName, \"containerd-shims\"),\n\t\tOldUserData:     filepath.Join(localAppData, appName, \"cache\", \"Rancher Desktop\"),\n\t}\n\tpaths.Logs = os.Getenv(\"RD_LOGS_DIR\")\n\tif paths.Logs == \"\" {\n\t\tpaths.Logs = filepath.Join(localAppData, appName, \"logs\")\n\t}\n\tpaths.Resources, err = getResourcesPathFunc()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find resources directory: %w\", err)\n\t}\n\n\treturn &paths, nil\n}\n\n// Given a list of paths, return the first one that is a valid executable.\nfunc FindFirstExecutable(candidates ...string) (string, error) {\n\terrs := multierror.Append(nil, errors.New(\"search location exhausted\"))\n\tfor _, candidate := range candidates {\n\t\t_, err := os.Stat(candidate)\n\t\tif err == nil {\n\t\t\treturn candidate, nil\n\t\t}\n\t\tif !errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn \"\", fmt.Errorf(\"failed to check existence of %q: %w\", candidate, err)\n\t\t}\n\t\terrs = multierror.Append(errs, fmt.Errorf(\"%s is not suitable\", candidate))\n\t}\n\treturn \"\", errs.ErrorOrNil()\n}\n\n// Return the path used to launch Rancher Desktop.\nfunc GetRDLaunchPath(ctx context.Context) (string, error) {\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\tdataDir, err := directories.GetLocalAppDataDirectory()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn FindFirstExecutable(\n\t\tfilepath.Join(appDir, \"Rancher Desktop.exe\"),\n\t\tfilepath.Join(dataDir, \"Programs\", \"Rancher Desktop\", \"Rancher Desktop.exe\"),\n\t)\n}\n\n// Return the path to the main Rancher Desktop executable.\n// In the case of `yarn dev`, this would be the electron executable.\nfunc GetMainExecutable(ctx context.Context) (string, error) {\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get application directory: %w\", err)\n\t}\n\treturn FindFirstExecutable(\n\t\tfilepath.Join(appDir, \"Rancher Desktop.exe\"),\n\t\tfilepath.Join(appDir, \"node_modules\", \"electron\", \"dist\", \"electron.exe\"),\n\t)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/paths/paths_windows_test.go",
    "content": "package paths\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\nfunc TestGetPaths(t *testing.T) {\n\tt.Run(\"should return correct paths without environment variables set\", func(t *testing.T) {\n\t\t// Ensure that these variables are not set in the testing environment\n\t\tenvironment := map[string]string{\n\t\t\t\"RD_LOGS_DIR\":  \"\",\n\t\t\t\"LOCALAPPDATA\": \"\",\n\t\t\t\"APPDATA\":      \"\",\n\t\t}\n\t\tfor key, value := range environment {\n\t\t\tt.Setenv(key, value)\n\t\t}\n\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:         filepath.Join(homeDir, \"AppData\", \"Local\", appName),\n\t\t\tAltAppHome:      filepath.Join(homeDir, \"AppData\", \"Local\", appName),\n\t\t\tConfig:          filepath.Join(homeDir, \"AppData\", \"Local\", appName),\n\t\t\tLogs:            filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"logs\"),\n\t\t\tCache:           filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"cache\"),\n\t\t\tWslDistro:       filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"distro\"),\n\t\t\tWslDistroData:   filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"distro-data\"),\n\t\t\tResources:       fakeResourcesPath,\n\t\t\tExtensionRoot:   filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"extensions\"),\n\t\t\tSnapshots:       filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"snapshots\"),\n\t\t\tContainerdShims: filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"containerd-shims\"),\n\t\t\tOldUserData:     filepath.Join(homeDir, \"AppData\", \"Local\", appName, \"cache\", \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n\n\tt.Run(\"should return correct paths with environment variables set\", func(t *testing.T) {\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting user home directory: %s\", err)\n\t\t}\n\t\tenvironment := map[string]string{\n\t\t\t\"RD_LOGS_DIR\":  filepath.Join(homeDir, \"mockRdLogsDir\"),\n\t\t\t\"LOCALAPPDATA\": filepath.Join(homeDir, \"mockLocalAppData\"),\n\t\t\t\"APPDATA\":      filepath.Join(homeDir, \"mockAppData\"),\n\t\t}\n\t\tfor key, value := range environment {\n\t\t\tt.Setenv(key, value)\n\t\t}\n\n\t\texpectedPaths := Paths{\n\t\t\tAppHome:         filepath.Join(environment[\"LOCALAPPDATA\"], appName),\n\t\t\tAltAppHome:      filepath.Join(environment[\"LOCALAPPDATA\"], appName),\n\t\t\tConfig:          filepath.Join(environment[\"LOCALAPPDATA\"], appName),\n\t\t\tLogs:            environment[\"RD_LOGS_DIR\"],\n\t\t\tCache:           filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"cache\"),\n\t\t\tWslDistro:       filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"distro\"),\n\t\t\tWslDistroData:   filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"distro-data\"),\n\t\t\tResources:       fakeResourcesPath,\n\t\t\tExtensionRoot:   filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"extensions\"),\n\t\t\tSnapshots:       filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"snapshots\"),\n\t\t\tContainerdShims: filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"containerd-shims\"),\n\t\t\tOldUserData:     filepath.Join(environment[\"LOCALAPPDATA\"], appName, \"cache\", \"Rancher Desktop\"),\n\t\t}\n\t\tactualPaths, err := GetPaths(mockGetResourcesPath)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Unexpected error getting actual paths: %s\", err)\n\t\t}\n\t\tif *actualPaths != expectedPaths {\n\t\t\tt.Errorf(\"Actual paths does not match expected paths\\nActual paths: %#v\\nExpected paths: %#v\", actualPaths, expectedPaths)\n\t\t}\n\t})\n}\n\n// Given an application directory, create the rdctl executable at the expected\n// path and return its path.\nfunc makeRdctl(t *testing.T, appDir string) string {\n\trdctlPath := filepath.Join(appDir, \"resources/resources/win32/bin/rdctl.exe\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(rdctlPath), 0o755))\n\trdctl, err := os.OpenFile(rdctlPath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, rdctl.Close())\n\treturn rdctlPath\n}\n\n// Given an application directory, create the main executable at the expected\n// path and return its path.\nfunc makeExecutable(t *testing.T, appDir string) string {\n\texecutablePath := filepath.Join(appDir, \"Rancher Desktop.exe\")\n\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\texecutable, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\trequire.NoError(t, err)\n\tassert.NoError(t, executable.Close())\n\treturn executablePath\n}\n\nfunc TestGetRDLaunchPath(t *testing.T) {\n\tt.Run(\"from bundled application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\texecutablePath := makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"from application directory\", func(t *testing.T) {\n\t\tappDataDir, err := directories.GetLocalAppDataDirectory()\n\t\trequire.NoError(t, err)\n\t\texecutablePath := filepath.Join(appDataDir, \"Programs/Rancher Desktop/Rancher Desktop.exe\")\n\t\tif _, err := os.Stat(executablePath); errors.Is(err, os.ErrNotExist) {\n\t\t\tt.Skip(\"Application does not exist\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetRDLaunchPath(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"fail to find suitable application\", func(t *testing.T) {\n\t\tappDataDir, err := directories.GetLocalAppDataDirectory()\n\t\trequire.NoError(t, err)\n\t\texecutablePath := filepath.Join(appDataDir, \"Programs/Rancher Desktop/Rancher Desktop.exe\")\n\t\tif _, err := os.Stat(executablePath); err == nil {\n\t\t\tt.Skip(\"Application exists\")\n\t\t}\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\t_, err = GetRDLaunchPath(ctx)\n\t\tassert.Error(t, err)\n\t})\n}\n\nfunc TestGetMainExecutable(t *testing.T) {\n\tt.Run(\"packaged application\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\texecutablePath := makeExecutable(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n\tt.Run(\"development build\", func(t *testing.T) {\n\t\tdir, err := filepath.EvalSymlinks(t.TempDir())\n\t\trequire.NoError(t, err)\n\t\trdctlPath := makeRdctl(t, dir)\n\t\tctx := directories.OverrideRdctlPath(context.Background(), rdctlPath)\n\t\texecutablePath := filepath.Join(dir, \"node_modules/electron/dist/electron.exe\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(executablePath), 0o755))\n\t\tf, err := os.OpenFile(executablePath, os.O_CREATE|os.O_WRONLY, 0o755)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, f.Close())\n\t\tactual, err := GetMainExecutable(ctx)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, executablePath, actual)\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/plist/plist.go",
    "content": "// This package exists because I looked at three commonly used JSON->PLIST transformers/encoders.\n// One of them (vinzenz/go-plist) was too low-level, and the other two\n// (distatus/go-plist and howett.net/plist) ignored the `omitempty` directive in structure tags,\n// producing a large number of '<dict></dict>' sequences in the generated output.\n//\n// We already had a module that used reflection to convert objects into REGISTRY files, so it wasn't\n// very hard to take the same code and repurpose it to generate minimal plist files. One could make a\n// case that using a hardened XML library will avoid encoding problems, but given that we deal with\n// Hence this package.\n\npackage plist\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils\"\n)\n\nconst indentChange = \"  \"\n\n// convertToPListLines recursively reflects the supplied value into lines for a plist\n\n// structType: type information on the current field for the `value` parameter\n// value: the reflected value of the current field, based on a simple map[string]interface{} JSON-parse\n// indent: the leading whitespace for each line so the generated XML is more readable\n// path: a dotted representation of the fully-qualified name of the field\n//\n// Returns two values:\n//\n//\tan array of lines representing the generated XML\n//\tan error: the only non-nil error this function can return is when it encounters an unhandled data type\n\nfunc convertToPListLines(structType reflect.Type, value reflect.Value, indent, path string) ([]string, error) {\n\tkind := structType.Kind()\n\tif value.Kind() == reflect.Interface && kind != reflect.Interface {\n\t\tif value.IsNil() {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn convertToPListLines(structType, value.Elem(), indent, path)\n\t}\n\tif value.Kind() == reflect.Ptr {\n\t\treturn nil, fmt.Errorf(\"plist generation: got an unexpected pointer for %s value %v, expecting type %v\", path, value, structType)\n\t}\n\tswitch kind {\n\tcase reflect.Struct:\n\t\tif value.Kind() != reflect.Map {\n\t\t\treturn nil, fmt.Errorf(\"expecting actual kind for a typed struct %s to be a map, got %v\", path, value.Kind())\n\t\t}\n\t\tnumTypedFields := structType.NumField()\n\t\treturnedLines := []string{indent + \"<dict>\"}\n\t\t// Typed fields are ordered according to options.ServerSettingsForJSON\n\t\t// By walking the list of fields in the structure type, and expanding only those fields\n\t\t// that are specified, we get a consistent order in the output\n\t\t// (e.g. `updater` always appears before `autoStart` in `application`)\n\t\tfor i := 0; i < numTypedFields; i++ {\n\t\t\tfield := structType.Field(i)\n\t\t\tfieldName, _, _ := strings.Cut(field.Tag.Get(\"json\"), \",\")\n\t\t\tvalueElement := value.MapIndex(reflect.ValueOf(fieldName))\n\t\t\tif valueElement.IsValid() {\n\t\t\t\tnewRetLines, err := convertToPListLines(field.Type, valueElement, indent+indentChange, path+\".\"+fieldName)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturnedLines = append(returnedLines, fmt.Sprintf(`%s<key>%s</key>`, indent+indentChange, fieldName))\n\t\t\t\treturnedLines = append(returnedLines, newRetLines...)\n\t\t\t}\n\t\t}\n\t\tif len(returnedLines) == 1 {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturnedLines = append(returnedLines, indent+\"</dict>\")\n\t\treturn returnedLines, nil\n\tcase reflect.Ptr:\n\t\treturn convertToPListLines(structType.Elem(), value, indent, path)\n\tcase reflect.Slice, reflect.Array:\n\t\tif value.Kind() != reflect.Slice && value.Kind() != reflect.Array {\n\t\t\treturn nil, fmt.Errorf(\"expected slice or array at %s, got %v\", path, value.Kind())\n\t\t}\n\t\t// Currently, all arrays in the options are arrays of strings\n\t\tnumValues := value.Len()\n\t\tretLines := make([]string, numValues+2)\n\t\tretLines[0] = indent + \"<array>\"\n\t\tfor i := 0; i < numValues; i++ {\n\t\t\titem := value.Index(i)\n\t\t\tfor item.Kind() == reflect.Interface || item.Kind() == reflect.Pointer {\n\t\t\t\titem = item.Elem()\n\t\t\t}\n\t\t\tescapedString, err := xmlEscapeText(item.String())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tretLines[i+1] = fmt.Sprintf(\"%s<string>%s</string>\", indent+indentChange, escapedString)\n\t\t}\n\t\tretLines[numValues+1] = indent + \"</array>\"\n\t\treturn retLines, nil\n\tcase reflect.Map:\n\t\treturnedLines := []string{indent + \"<dict>\"}\n\t\tmapKeys := utils.SortKeys(value.MapKeys())\n\t\tfor _, mapKey := range mapKeys {\n\t\t\tkeyAsString := mapKey.StringKey\n\t\t\tinnerLines, err := convertToPListLines(structType.Elem(), value.MapIndex(mapKey.MapKey), indent+indentChange, path+\".\"+keyAsString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else if len(innerLines) > 0 {\n\t\t\t\tescapedString, err := xmlEscapeText(keyAsString)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\treturnedLines = append(returnedLines, fmt.Sprintf(`%s<key>%s</key>`, indent+indentChange, escapedString))\n\t\t\t\treturnedLines = append(returnedLines, innerLines...)\n\t\t\t}\n\t\t}\n\t\treturnedLines = append(returnedLines, indent+\"</dict>\")\n\t\treturn returnedLines, nil\n\tcase reflect.Interface:\n\t\t// Since we allow whatever here, just use the actual type of the value.\n\t\t// But if it's an interface{} we'll need to dereference it first to avoid\n\t\t// an infinite loop.\n\t\tfor value.Kind() == reflect.Interface {\n\t\t\tvalue = value.Elem()\n\t\t}\n\t\treturn convertToPListLines(value.Type(), value, indent, path)\n\tcase reflect.Bool:\n\t\tboolValue := map[bool]string{true: \"true\", false: \"false\"}[value.Bool()]\n\t\treturn []string{fmt.Sprintf(\"%s<%s/>\", indent, boolValue)}, nil\n\tcase reflect.Int, reflect.Int8, reflect.Int16,\n\t\treflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16,\n\t\treflect.Uint32, reflect.Int64, reflect.Uint64:\n\t\tif value.CanConvert(reflect.TypeOf(int64(0))) {\n\t\t\tvalue = value.Convert(reflect.TypeOf(int64(0)))\n\t\t}\n\t\treturn []string{fmt.Sprintf(\"%s<integer>%d</integer>\", indent, value.Int())}, nil\n\tcase reflect.Float32:\n\t\treturn []string{fmt.Sprintf(\"%s<float>%f</float>\", indent, value.Float())}, nil\n\tcase reflect.String:\n\t\tescapedString, err := xmlEscapeText(value.String())\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []string{fmt.Sprintf(\"%s<string>%s</string>\", indent, escapedString)}, nil\n\t}\n\treturn nil, fmt.Errorf(\"convertToPListLines: don't know how to process %s kind: %q, (%T), value: %v\", path, kind, structType, value)\n}\n\nfunc xmlEscapeText(s string) (string, error) {\n\trecvBuffer := &bytes.Buffer{}\n\terr := xml.EscapeText(recvBuffer, []byte(s))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn recvBuffer.String(), nil\n}\n\n// JSONToPlist converts the json settings to plist-compatible xml text.\nfunc JSONToPlist(settingsBodyAsJSON string) (string, error) {\n\tvar actualSettingsJSON map[string]interface{}\n\n\tif err := json.Unmarshal([]byte(settingsBodyAsJSON), &actualSettingsJSON); err != nil {\n\t\treturn \"\", fmt.Errorf(\"error in json: %s\", err)\n\t}\n\t_, ok := actualSettingsJSON[\"version\"]\n\tif !ok {\n\t\tactualSettingsJSON[\"version\"] = options.CURRENT_SETTINGS_VERSION\n\t}\n\t// We use the type as a schema, mainly to distinguish the absence of an array or map from an empty instance\n\t// - see https://github.com/golang/go/issues/27589\n\t// And the reason why the type-free parse isn't sufficient is that it doesn't distinguish\n\t// hashes (like `diagnostics.mutedChecks`) from subtrees.\n\t// By walking the two data structures in parallel the converter can figure out exactly which fields were specified,\n\t// and how to interpret their values.\n\tlines, err := convertToPListLines(reflect.TypeOf(options.ServerSettingsForJSON{}), reflect.ValueOf(actualSettingsJSON), indentChange, \"\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\theaderLines := []string{`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`,\n\t\t`<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">`,\n\t\t`<plist version=\"1.0\">`,\n\t}\n\ttrailerLines := []string{\"</plist>\", \"\"}\n\tif len(lines) == 0 {\n\t\tlines = []string{\"  <dict/>\"}\n\t}\n\theaderLines = append(headerLines, lines...)\n\theaderLines = append(headerLines, trailerLines...)\n\treturn strings.Join(headerLines, \"\\n\"), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/plist/plist_test.go",
    "content": "package plist\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n)\n\nfunc TestJsonToPlistFormat(t *testing.T) {\n\tt.Run(\"handles empty bodies\", func(t *testing.T) {\n\t\ts, err := JSONToPlist(\"{}\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fmt.Sprintf(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>%d</integer>\n  </dict>\n</plist>\n`, options.CURRENT_SETTINGS_VERSION), s)\n\t})\n\n\tt.Run(\"Handles arrays\", func(t *testing.T) {\n\t\tjsonBody := `{\"application\": { \"extensions\": { \"allowed\": {\n        \"enabled\": false,\n        \"list\": [\"wink\", \"blink\", \"drink\"]\n     } } }, \"containerEngine\": { \"name\": \"beatrice\" }}`\n\t\ts, err := JSONToPlist(jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fmt.Sprintf(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>%d</integer>\n    <key>application</key>\n    <dict>\n      <key>extensions</key>\n      <dict>\n        <key>allowed</key>\n        <dict>\n          <key>enabled</key>\n          <false/>\n          <key>list</key>\n          <array>\n            <string>wink</string>\n            <string>blink</string>\n            <string>drink</string>\n          </array>\n        </dict>\n      </dict>\n    </dict>\n    <key>containerEngine</key>\n    <dict>\n      <key>name</key>\n      <string>beatrice</string>\n    </dict>\n  </dict>\n</plist>\n`, options.CURRENT_SETTINGS_VERSION), s)\n\t})\n\n\tt.Run(\"Handles everything\", func(t *testing.T) {\n\t\tjsonBody := `{\n  \"version\": 9,\n  \"application\": {\n    \"adminAccess\": false,\n    \"debug\": false,\n    \"extensions\": {\n      \"allowed\": {\n        \"enabled\": false,\n        \"list\": [\n          \"<wi & nk>\",\n          \"blink\",\n          \"ok\"\n        ]\n      },\n      \"installed\": {}\n    },\n    \"pathManagementStrategy\": \"rcfiles\",\n    \"telemetry\": {\n      \"enabled\": true\n    },\n    \"updater\": {\n      \"enabled\": true\n    },\n    \"autoStart\": false,\n    \"startInBackground\": false,\n    \"hideNotificationIcon\": false,\n    \"window\": {\n      \"quitOnClose\": false\n    }\n  },\n  \"containerEngine\": {\n    \"allowedImages\": {\n      \"enabled\": false,\n      \"patterns\": []\n    },\n    \"name\": \"moby\"\n  },\n  \"virtualMachine\": {\n    \"memoryInGB\": 4,\n    \"mount\": {\n      \"type\": \"reverse-sshfs\"\n    },\n    \"numberCPUs\": 2,\n    \"type\": \"qemu\",\n    \"useRosetta\": false\n  },\n  \"WSL\": {\n    \"integrations\": {\n      \"first\": true,\n      \"second\": false,\n      \"third\": true\n    }\n  },\n  \"kubernetes\": {\n    \"version\": \"1.27.3\",\n    \"port\": 6443,\n    \"enabled\": true,\n    \"options\": {\n      \"traefik\": true,\n      \"flannel\": true\n    },\n    \"ingress\": {\n      \"localhostOnly\": false\n    }\n  },\n  \"portForwarding\": {\n    \"includeKubernetesServices\": false\n  },\n  \"images\": {\n    \"showAll\": true,\n    \"namespace\": \"k8s.io\"\n  },\n  \"diagnostics\": {\n    \"showMuted\": false,\n    \"mutedChecks\": {\n      \"moss\": true,\n      \"dial\": false\n    }\n  },\n  \"experimental\": {\n    \"virtualMachine\": {\n      \"mount\": {\n        \"9p\": {\n          \"securityModel\": \"none\",\n          \"protocolVersion\": \"9p2000.L\",\n          \"msizeInKib\": 128,\n          \"cacheMode\": \"mmap\"\n        }\n      },\n      \"proxy\": {\n        \"enabled\": false,\n        \"address\": \"\",\n        \"password\": \"\",\n        \"port\": 3128,\n        \"username\": \"\"\n      }\n    }\n  }\n}\n`\n\t\ts, err := JSONToPlist(jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>9</integer>\n    <key>application</key>\n    <dict>\n      <key>adminAccess</key>\n      <false/>\n      <key>debug</key>\n      <false/>\n      <key>extensions</key>\n      <dict>\n        <key>allowed</key>\n        <dict>\n          <key>enabled</key>\n          <false/>\n          <key>list</key>\n          <array>\n            <string>&lt;wi &amp; nk&gt;</string>\n            <string>blink</string>\n            <string>ok</string>\n          </array>\n        </dict>\n        <key>installed</key>\n        <dict>\n        </dict>\n      </dict>\n      <key>pathManagementStrategy</key>\n      <string>rcfiles</string>\n      <key>telemetry</key>\n      <dict>\n        <key>enabled</key>\n        <true/>\n      </dict>\n      <key>updater</key>\n      <dict>\n        <key>enabled</key>\n        <true/>\n      </dict>\n      <key>autoStart</key>\n      <false/>\n      <key>startInBackground</key>\n      <false/>\n      <key>hideNotificationIcon</key>\n      <false/>\n      <key>window</key>\n      <dict>\n        <key>quitOnClose</key>\n        <false/>\n      </dict>\n    </dict>\n    <key>containerEngine</key>\n    <dict>\n      <key>name</key>\n      <string>moby</string>\n      <key>allowedImages</key>\n      <dict>\n        <key>enabled</key>\n        <false/>\n        <key>patterns</key>\n        <array>\n        </array>\n      </dict>\n    </dict>\n    <key>virtualMachine</key>\n    <dict>\n      <key>memoryInGB</key>\n      <integer>4</integer>\n      <key>numberCPUs</key>\n      <integer>2</integer>\n      <key>type</key>\n      <string>qemu</string>\n      <key>useRosetta</key>\n      <false/>\n      <key>mount</key>\n      <dict>\n        <key>type</key>\n        <string>reverse-sshfs</string>\n      </dict>\n    </dict>\n    <key>kubernetes</key>\n    <dict>\n      <key>version</key>\n      <string>1.27.3</string>\n      <key>port</key>\n      <integer>6443</integer>\n      <key>enabled</key>\n      <true/>\n      <key>options</key>\n      <dict>\n        <key>traefik</key>\n        <true/>\n        <key>flannel</key>\n        <true/>\n      </dict>\n      <key>ingress</key>\n      <dict>\n        <key>localhostOnly</key>\n        <false/>\n      </dict>\n    </dict>\n    <key>experimental</key>\n    <dict>\n      <key>virtualMachine</key>\n      <dict>\n        <key>mount</key>\n        <dict>\n          <key>9p</key>\n          <dict>\n            <key>securityModel</key>\n            <string>none</string>\n            <key>protocolVersion</key>\n            <string>9p2000.L</string>\n            <key>msizeInKib</key>\n            <integer>128</integer>\n            <key>cacheMode</key>\n            <string>mmap</string>\n          </dict>\n        </dict>\n        <key>proxy</key>\n        <dict>\n          <key>enabled</key>\n          <false/>\n          <key>address</key>\n          <string></string>\n          <key>password</key>\n          <string></string>\n          <key>port</key>\n          <integer>3128</integer>\n          <key>username</key>\n          <string></string>\n        </dict>\n      </dict>\n    </dict>\n    <key>WSL</key>\n    <dict>\n      <key>integrations</key>\n      <dict>\n        <key>first</key>\n        <true/>\n        <key>second</key>\n        <false/>\n        <key>third</key>\n        <true/>\n      </dict>\n    </dict>\n    <key>portForwarding</key>\n    <dict>\n      <key>includeKubernetesServices</key>\n      <false/>\n    </dict>\n    <key>images</key>\n    <dict>\n      <key>showAll</key>\n      <true/>\n      <key>namespace</key>\n      <string>k8s.io</string>\n    </dict>\n    <key>diagnostics</key>\n    <dict>\n      <key>showMuted</key>\n      <false/>\n      <key>mutedChecks</key>\n      <dict>\n        <key>dial</key>\n        <false/>\n        <key>moss</key>\n        <true/>\n      </dict>\n    </dict>\n  </dict>\n</plist>\n`, s)\n\t})\n\n\tt.Run(\"Escapes problematic strings\", func(t *testing.T) {\n\t\tjsonBody := `{ \"application\": {\n\t\t\t\t\t\t\t\t\t\t\"extensions\": {\n\t\t\t\t\t\t\t\t\t\t\t\"allowed\": {\n\t\t\t\t\t\t\t\t\t\t\t  \"enabled\": false,\n\t\t\t\t\t\t\t\t\t\t\t  \"list\": [\"less-than:<\", \"greater:>\", \"and:&\", \"d-quote:\\\"\", \"emoji:😀\"]\n\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\"installed\": {\n\t\t\t\t\t\t\t\t\t\t\t\t\"key-with-less-than: <\": true,\n\t\t\t\t\t\t\t\t\t\t\t\t\"key-with-ampersand: &\": true,\n\t\t\t\t\t\t\t\t\t\t\t\t\"key-with-greater-than: >\": true,\n\t\t\t\t\t\t\t\t\t\t\t\t\"key-with-emoji: 🐤\": false\n\t\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\"containerEngine\": {\n\t\t\t\t\t\t\t\t\t  \"name\": \"name-less-<-than\"\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n`\n\t\ts, err := JSONToPlist(jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fmt.Sprintf(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>version</key>\n    <integer>%d</integer>\n    <key>application</key>\n    <dict>\n      <key>extensions</key>\n      <dict>\n        <key>allowed</key>\n        <dict>\n          <key>enabled</key>\n          <false/>\n          <key>list</key>\n          <array>\n            <string>less-than:&lt;</string>\n            <string>greater:&gt;</string>\n            <string>and:&amp;</string>\n            <string>d-quote:&#34;</string>\n            <string>emoji:😀</string>\n          </array>\n        </dict>\n        <key>installed</key>\n        <dict>\n          <key>key-with-ampersand: &amp;</key>\n          <true/>\n          <key>key-with-emoji: 🐤</key>\n          <false/>\n          <key>key-with-greater-than: &gt;</key>\n          <true/>\n          <key>key-with-less-than: &lt;</key>\n          <true/>\n        </dict>\n      </dict>\n    </dict>\n    <key>containerEngine</key>\n    <dict>\n      <key>name</key>\n      <string>name-less-&lt;-than</string>\n    </dict>\n  </dict>\n</plist>\n`, options.CURRENT_SETTINGS_VERSION), s)\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_darwin.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n)\n\nconst (\n\tCTL_KERN      = \"kern\"\n\tKERN_PROCARGS = 38\n)\n\n// Iterate over all processes, calling a callback function for each process\n// found with the pid and the path to the executable.  If the callback function\n// returns an error, iteration is immediately stopped.\nfunc iterProcesses(callback func(pid int, executable string) error) error {\n\tprocs, err := unix.SysctlKinfoProcSlice(\"kern.proc.all\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list processes: %w\", err)\n\t}\n\tfor procIndex := range procs {\n\t\tproc := &procs[procIndex]\n\t\tpid := int(proc.Proc.P_pid)\n\t\tbuf, err := unix.SysctlRaw(CTL_KERN, KERN_PROCARGS, pid)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, unix.EINVAL) {\n\t\t\t\tlogrus.Debugf(\"Failed to get command line of pid %d: %s\", pid, err)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\t// The buffer starts with a null-terminated executable path, plus\n\t\t// command line arguments and things.\n\t\tindex := slices.Index(buf, 0)\n\t\tif index < 0 {\n\t\t\t// If we have unexpected data, don't fall over.\n\t\t\tcontinue\n\t\t}\n\t\tif err := callback(pid, string(buf[:index])); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Block and wait for the given process to exit.\nfunc WaitForProcess(pid int) error {\n\tqueue, err := unix.Kqueue()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize process monitoring: %w\", err)\n\t}\n\tdefer func() {\n\t\tif err := unix.Close(queue); err != nil {\n\t\t\tlogrus.Warnf(\"Ignoring failure to close kqueue: %s\", err)\n\t\t}\n\t}()\n\tchange := unix.Kevent_t{\n\t\tIdent:  uint64(pid),\n\t\tFilter: unix.EVFILT_PROC,\n\t\tFlags:  unix.EV_ADD | unix.EV_ENABLE | unix.EV_ONESHOT,\n\t\tFflags: unix.NOTE_EXIT,\n\t}\n\tevents := make([]unix.Kevent_t, 1)\n\tn, err := unix.Kevent(queue, []unix.Kevent_t{change}, events, nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to wait for process %d to exit: %w\", pid, err)\n\t}\n\tlogrus.Tracef(\"got %d kqueue events: %+v\", n, events[:n])\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_linux.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\n// Iterate over all processes, calling a callback function for each process\n// found with the pid and the path to the executable.  If the callback function\n// returns an error, iteration is immediately stopped.\nfunc iterProcesses(callback func(pid int, executable string) error) error {\n\tpidfds, err := os.ReadDir(\"/proc\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error listing processes: %w\", err)\n\t}\n\tfor _, pidfd := range pidfds {\n\t\tif !pidfd.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\tpid, err := strconv.Atoi(pidfd.Name())\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\t//nolint:gocritic // filepathJoin doesn't like absolute paths\n\t\tprocPath, err := os.Readlink(filepath.Join(\"/proc\", pidfd.Name(), \"exe\"))\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif err := callback(pid, procPath); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Block and wait for the given process to exit.\nfunc WaitForProcess(pid int) error {\n\tpidfd, err := unix.PidfdOpen(pid, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open process %d: %w\", pid, err)\n\t}\n\tdefer func() {\n\t\t_ = os.NewFile(uintptr(pidfd), fmt.Sprintf(\"/proc/%d\", pid)).Close()\n\t}()\n\n\tpollFd := unix.PollFd{\n\t\tFd:     int32(pidfd),\n\t\tEvents: unix.POLLIN,\n\t}\n\t_, err = unix.Poll([]unix.PollFd{pollFd}, -1)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to wait for process %d: %w\", pid, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_test.go",
    "content": "package process_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\nfunc TestFindPidOfProcess(t *testing.T) {\n\texe, err := os.Executable()\n\trequire.NoError(t, err)\n\tpid, err := process.FindPidOfProcess(exe)\n\trequire.NoError(t, err)\n\tassert.Equal(t, os.Getpid(), pid)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_unix.go",
    "content": "//go:build unix\n\n/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// TerminateProcessInDirectory terminates all processes where the executable\n// resides within the given directory, as gracefully as possible.  If `force` is\n// set, SIGKILL is used instead.\nfunc TerminateProcessInDirectory(directory string, force bool) error {\n\treturn iterProcesses(func(pid int, procPath string) error {\n\t\t// Don't kill the current process\n\t\tif pid == os.Getpid() {\n\t\t\treturn nil\n\t\t}\n\t\trelPath, err := filepath.Rel(directory, procPath)\n\t\tif err != nil || strings.HasPrefix(relPath, \"../\") {\n\t\t\treturn nil\n\t\t}\n\t\tproc, err := os.FindProcess(pid)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\tif force {\n\t\t\terr = proc.Signal(unix.SIGKILL)\n\t\t} else {\n\t\t\terr = proc.Signal(unix.SIGTERM)\n\t\t}\n\t\tif err == nil {\n\t\t\tlogrus.Infof(\"Terminated process %d (%s)\", pid, procPath)\n\t\t} else if !errors.Is(err, unix.EINVAL) {\n\t\t\tlogrus.Infof(\"Ignoring failure to terminate pid %d (%s): %s\", pid, procPath, err)\n\t\t}\n\t\treturn nil\n\t})\n}\n\n// Find some pid running the given executable.  If not found, return 0.\nfunc FindPidOfProcess(executable string) (int, error) {\n\ttargetInfo, err := os.Stat(executable)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to determine %s info: %w\", executable, err)\n\t}\n\n\tvar mainPid int\n\t// errFound is a sentinel error so we can break out of the loop early.\n\terrFound := fmt.Errorf(\"found executable process\")\n\terr = iterProcesses(func(pid int, executable string) error {\n\t\tinfo, err := os.Stat(executable)\n\t\tif err != nil {\n\t\t\t// Maybe the executable has been deleted since.\n\t\t\tlogrus.Debugf(\"failed to look up executable for pid %d: %s\", pid, err)\n\t\t\treturn nil\n\t\t}\n\t\tif os.SameFile(targetInfo, info) {\n\t\t\tmainPid = pid\n\t\t\treturn errFound\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil && !errors.Is(err, errFound) {\n\t\treturn 0, err\n\t}\n\treturn mainPid, nil\n}\n\n// Kill the process group the given process belongs to.  If wait is set, block\n// until the target process exits first before doing so.  On Linux, the process\n// group is only killed if the given pid is its own process group leader.\nfunc KillProcessGroup(pid int, wait bool) error {\n\tif pid == 0 {\n\t\treturn nil\n\t}\n\tpgid, err := unix.Getpgid(pid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get process group id for %d: %w\", pid, err)\n\t}\n\tif wait {\n\t\tif err = WaitForProcess(pid); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to wait for process: %w\", err)\n\t\t}\n\t}\n\tif runtime.GOOS == \"linux\" && pid != pgid {\n\t\t// On Linux, do not kill the process group if the pid is not the same as\n\t\t// the process group id; this can happen when running from rpm/deb\n\t\t// packaged builds (in which case killing the process group ends up\n\t\t// killing the whole X11 session).\n\t\treturn nil\n\t}\n\terr = unix.Kill(-pgid, unix.SIGTERM)\n\tif err != nil && !errors.Is(err, unix.ESRCH) {\n\t\treturn fmt.Errorf(\"failed to send SIGTERM: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_windows.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n)\n\ntype JOBOBJECT_BASIC_LIMIT_INFORMATION struct {\n\tPerProcessUserTimeLimit int64\n\tPerJobUserTimeLimit     int64\n\tLimitFlags              uint32\n\tMinimumWorkingSetSize   uintptr\n\tMaximumWorkingSetSize   uintptr\n\tActiveProcessLimit      uint32\n\tAffinity                uintptr\n\tPriorityClass           uint32\n\tSchedulingClass         uint32\n}\ntype IO_COUNTERS struct {\n\tReadOperationCount  uint64\n\tWriteOperationCount uint64\n\tOtherOperationCount uint64\n\tReadTransferCount   uint64\n\tWriteTransferCount  uint64\n\tOtherTransferCount  uint64\n}\ntype JOBOBJECT_EXTENDED_LIMIT_INFORMATION struct {\n\tBasicLimitInformation JOBOBJECT_BASIC_LIMIT_INFORMATION\n\tIoInfo                IO_COUNTERS\n\tProcessMemoryLimit    uintptr\n\tJobMemoryLimit        uintptr\n\tPeakProcessMemoryUsed uintptr\n\tPeakJobMemoryUsed     uintptr\n}\n\nconst (\n\tjobName                              = \"RancherDesktopJob\"\n\tJobObjectExtendedLimitInformation    = 9\n\tJOB_OBJECT_LIMIT_BREAKAWAY_OK        = uint32(0x00000800)\n\tJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE   = uint32(0x00002000)\n\tJOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = uint32(0x00001000)\n\tPROC_THREAD_ATTRIBUTE_JOB_LIST       = 0x0002000D // 13 + input\n)\n\nvar (\n\thKernel32 = windows.NewLazySystemDLL(\"kernel32\")\n\n\tcreateJobObject           = hKernel32.NewProc(\"CreateJobObjectW\")\n\tqueryInformationJobObject = hKernel32.NewProc(\"QueryInformationJobObject\")\n\tsetInformationJobObject   = hKernel32.NewProc(\"SetInformationJobObject\")\n\tgetProcessHeap            = hKernel32.NewProc(\"GetProcessHeap\")\n\theapAlloc                 = hKernel32.NewProc(\"HeapAlloc\")\n\theapFree                  = hKernel32.NewProc(\"HeapFree\")\n)\n\n// buildCommandLine convert a slice of arguments into a properly formatted\n// command line string suitable for use with [windows.CreateProcess].  This\n// function is the reverse of [windows.DecomposeCommandLine], which parses a\n// command line string into individual arguments.\n//\n// The function follows the parsing rules for command-line arguments as outlined in\n// https://learn.microsoft.com/en-us/cpp/c-language/parsing-c-command-line-arguments\n//\n// Key behaviors include:\n//\n//  1. The first argument (typically the executable name) is treated specially and\n//     enclosed in double quotes without applying backslash escape rules,\n//     including for embedded quotes.\n//\n//  2. Each subsequent argument is wrapped in double quotes, and any internal\n//     quotes or backslashes are escaped appropriately according to the rules for\n//     Windows command-line parsing:\n//\n//     - Backslashes preceding a quote are doubled (e.g., \\ becomes \\\\), and the\n//     quote itself is escaped.\n//\n//     - Backslashes followed by non-quote characters are preserved as-is.\nfunc buildCommandLine(args []string) string {\n\tvar builder strings.Builder\n\n\t// argv[0], i.e. the executable name, must be treated specially.  It is quoted\n\t// without any of the backslash escape rules.  This includes not being able to\n\t// escape quotes.\n\tif len(args) > 0 {\n\t\t_, _ = builder.WriteString(\"\\\"\")\n\t\t_, _ = builder.WriteString(args[0])\n\t\t_, _ = builder.WriteString(\"\\\"\")\n\t}\n\n\tfor _, word := range args[1:] {\n\t\tslashes := 0\n\t\t_, _ = builder.WriteString(\" \\\"\")\n\t\tfor _, ch := range []byte(word) {\n\t\t\tswitch ch {\n\t\t\tcase '\\\\':\n\t\t\t\tslashes += 1\n\t\t\tcase '\"':\n\t\t\t\t// If a run of backslashes is followed by a quote, each backslash needs\n\t\t\t\t// to be escaped by another backslash, and then the quote must be\n\t\t\t\t// itself escaped.\n\t\t\t\tfor i := 0; i < slashes; i++ {\n\t\t\t\t\t_, _ = builder.WriteString(\"\\\\\\\\\")\n\t\t\t\t}\n\t\t\t\t_, _ = builder.WriteString(\"\\\\\\\"\")\n\t\t\t\tslashes = 0\n\t\t\tdefault:\n\t\t\t\t// If a run of backslashes is followed by a non-quote character, all of\n\t\t\t\t// the backslashes are treated literally.\n\t\t\t\tfor i := 0; i < slashes; i++ {\n\t\t\t\t\t_, _ = builder.WriteString(\"\\\\\")\n\t\t\t\t}\n\t\t\t\t_ = builder.WriteByte(ch)\n\t\t\t\tslashes = 0\n\t\t\t}\n\t\t}\n\t\t// If the word ends in slashes, because we're adding a quote we must escape\n\t\t// all of the slashes.\n\t\tfor i := 0; i < slashes; i++ {\n\t\t\t_, _ = builder.WriteString(\"\\\\\\\\\")\n\t\t}\n\t\t_, _ = builder.WriteString(\"\\\"\")\n\t}\n\n\treturn builder.String()\n}\n\n// Given a job handle, spawn a process in the given job.  The function does not\n// return until the process exits.\nfunc spawnProcessInJob(job windows.Handle, commandLine *uint16) (*os.ProcessState, error) {\n\tlogrus.Tracef(\"Spawning in job %x: %s\", job, windows.UTF16PtrToString(commandLine))\n\t// We need the handle to have a stable address for the jobs list; we\n\t// do this by allocating memory in C to avoid the golang GC moving\n\t// things around.\n\theap, _, err := getProcessHeap.Call()\n\tif heap == 0 {\n\t\treturn nil, fmt.Errorf(\"failed to get process heap: %w\", err)\n\t}\n\n\tjobList, _, err := heapAlloc.Call(heap, 0, unsafe.Sizeof(job))\n\tif jobList == 0 {\n\t\treturn nil, fmt.Errorf(\"failed to allocate memory: %w\", err)\n\t}\n\tdefer func() {\n\t\tif ok, _, err := heapFree.Call(heap, 0, jobList); ok == 0 {\n\t\t\tlogrus.Tracef(\"Ignoring error %s freeing job list\", err)\n\t\t}\n\t}()\n\t*(*windows.Handle)(unsafe.Pointer(jobList)) = job\n\n\tmaxAttrCount := uint32(1) // We only have PROC_THREAD_ATTRIBUTE_JOB_LIST\n\tattrList, err := windows.NewProcThreadAttributeList(maxAttrCount)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to allocate process attributes: %w\", err)\n\t}\n\terr = attrList.Update(PROC_THREAD_ATTRIBUTE_JOB_LIST, unsafe.Pointer(jobList), unsafe.Sizeof(job))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to update process attributes: %w\", err)\n\t}\n\tstartupInfo := windows.StartupInfoEx{\n\t\tStartupInfo: windows.StartupInfo{\n\t\t\tCb: uint32(unsafe.Sizeof(windows.StartupInfoEx{})),\n\t\t},\n\t\tProcThreadAttributeList: attrList.List(),\n\t}\n\tvar procInfo windows.ProcessInformation\n\terr = windows.CreateProcess(\n\t\tnil, commandLine, nil, nil, true, windows.EXTENDED_STARTUPINFO_PRESENT, nil, nil,\n\t\t&startupInfo.StartupInfo, &procInfo)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create process: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = windows.CloseHandle(procInfo.Process)\n\t\t_ = windows.CloseHandle(procInfo.Thread)\n\t}()\n\tproc, err := os.FindProcess(int(procInfo.ProcessId))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to find process %d: %w\", procInfo.ProcessId, err)\n\t}\n\tstate, err := proc.Wait()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn state, nil\n}\n\n// configureJobLimits configures the given job to prevent processes in the job\n// from breaking away, and flags the job to terminate when the last handle to\n// the job is closed.\nfunc configureJobLimits(job windows.Handle) error {\n\tvar limits JOBOBJECT_EXTENDED_LIMIT_INFORMATION\n\tok, _, err := queryInformationJobObject.Call(\n\t\tuintptr(job),\n\t\tJobObjectExtendedLimitInformation,\n\t\tuintptr(unsafe.Pointer(&limits)),\n\t\tunsafe.Sizeof(limits),\n\t\tuintptr(unsafe.Pointer(nil)))\n\tif ok == 0 {\n\t\treturn fmt.Errorf(\"error looking up job limits: %w\", err)\n\t}\n\n\t// Prevent processes from breaking away from the job.\n\tbreakAwayFlags := JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK\n\tlimits.BasicLimitInformation.LimitFlags &= ^breakAwayFlags\n\t// Flag the job to terminate when the last handle to the job is closed.\n\tlimits.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE\n\n\tok, _, err = setInformationJobObject.Call(\n\t\tuintptr(job),\n\t\tJobObjectExtendedLimitInformation,\n\t\tuintptr(unsafe.Pointer(&limits)),\n\t\tunsafe.Sizeof(limits))\n\tif ok == 0 {\n\t\treturn fmt.Errorf(\"error setting job limits: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// injectHandleInProcess creates a copy of the provided handle into the\n// specified process.  As nothing refers to the handle otherwise, the duplicated\n// handle will only be closed when the specified process exits.\nfunc injectHandleInProcess(pid uint32, handle windows.Handle) error {\n\tprocess, err := windows.OpenProcess(windows.PROCESS_DUP_HANDLE, false, pid)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open parent process %d: %w\", pid, err)\n\t}\n\terr = windows.DuplicateHandle(windows.CurrentProcess(), handle, process, nil, 0, false, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to inject job into parent process %d: %w\", pid, err)\n\t}\n\treturn nil\n}\n\n// Spawn a process in the Rancher Desktop job.  If the job doesn't exist, ensure\n// that the given process has a handle to the new job.  Returns the resulting\n// process state after the process exits; the caller may get the process exit\n// code that way.\nfunc SpawnProcessInRDJob(pid uint32, command []string) (*os.ProcessState, error) {\n\tjobNameBytes, err := windows.UTF16PtrFromString(jobName)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to convert job name: %w\", err)\n\t}\n\n\t// Creating a job that already exists will return the job, with\n\t// ERROR_ALREADY_EXISTS as the error.  We can use that to determine if we need\n\t// to do the initial setup.\n\tjobUintptr, _, err := createJobObject.Call(\n\t\tuintptr(unsafe.Pointer(nil)),\n\t\tuintptr(unsafe.Pointer(jobNameBytes)))\n\tif jobUintptr == 0 {\n\t\treturn nil, fmt.Errorf(\"failed to create job: %w\", err)\n\t}\n\tjob := windows.Handle(jobUintptr)\n\tdefer func() {\n\t\t_ = windows.CloseHandle(job)\n\t}()\n\t// Check whether a new job was created, or an existing one was found.\n\tif !errors.Is(err, os.ErrExist) {\n\t\t// The job was newly created.\n\n\t\tif err := configureJobLimits(job); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// Duplicate the job into the given process (but leaking it).  This way when\n\t\t// the target process exits, it will shut down the job.\n\t\tif err := injectHandleInProcess(pid, job); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tcommandLine, err := windows.UTF16PtrFromString(buildCommandLine(command))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to build command line: %w\", err)\n\t}\n\tstate, err := spawnProcessInJob(job, commandLine)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to spawn process: %w\", err)\n\t}\n\n\treturn state, nil\n}\n\n// Iterate over all processes, calling a callback function for each process\n// found with the process handle and the path to the executable.  If the\n// callback function returns an error, iteration is immediately stopped.\nfunc iterProcesses(callback func(proc windows.Handle, executable string) error) error {\n\tvar pids []uint32\n\t// Try EnumProcesses until the number of pids returned is less than the\n\t// buffer size.\n\terr := directories.InvokeWin32WithBuffer(func(size uint32) error {\n\t\tpids = make([]uint32, size)\n\t\tvar bytesReturned uint32\n\t\terr := windows.EnumProcesses(pids, &bytesReturned)\n\t\tif err != nil || len(pids) < 1 {\n\t\t\treturn fmt.Errorf(\"failed to enumerate processes: %w\", err)\n\t\t}\n\t\tpidsReturned := uintptr(bytesReturned) / unsafe.Sizeof(pids[0])\n\t\tif pidsReturned < uintptr(len(pids)) {\n\t\t\t// Remember to truncate the pids to only the valid set.\n\t\t\tpids = pids[:pidsReturned]\n\t\t\treturn nil\n\t\t}\n\t\treturn windows.ERROR_INSUFFICIENT_BUFFER\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not get process list: %w\", err)\n\t}\n\n\tfor _, pid := range pids {\n\t\t// Do each iteration in a function so defer statements run faster.\n\t\terr = (func() error {\n\t\t\thProc, err := windows.OpenProcess(\n\t\t\t\twindows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_TERMINATE,\n\t\t\t\tfalse,\n\t\t\t\tpid)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Debugf(\"Ignoring error opening process %d: %s\", pid, err)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = windows.CloseHandle(hProc)\n\t\t\t}()\n\n\t\t\tvar executablePath string\n\t\t\terr = directories.InvokeWin32WithBuffer(func(size uint32) error {\n\t\t\t\tnameBuf := make([]uint16, size)\n\t\t\t\tcharsWritten := size\n\t\t\t\terr := windows.QueryFullProcessImageName(hProc, 0, &nameBuf[0], &charsWritten)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Tracef(\"failed to get image name for pid %d: %s\", pid, err)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tif charsWritten >= size-1 {\n\t\t\t\t\treturn windows.ERROR_INSUFFICIENT_BUFFER\n\t\t\t\t}\n\t\t\t\texecutablePath = windows.UTF16ToString(nameBuf)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif err != nil {\n\t\t\t\tlogrus.Debugf(\"failed to get process name of pid %d: %s (skipping)\", pid, err)\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\t\tif err := callback(hProc, executablePath); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t})()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Find some pid running the given executable.  If not found, return 0.\nfunc FindPidOfProcess(executable string) (int, error) {\n\ttargetInfo, err := os.Stat(executable)\n\tif err != nil {\n\t\treturn 0, fmt.Errorf(\"failed to determine %s info: %w\", executable, err)\n\t}\n\n\tvar mainPid int\n\t// errFound is a sentinel error so we can break out of the loop early.\n\terrFound := fmt.Errorf(\"found Rancher Desktop process\")\n\terr = iterProcesses(func(proc windows.Handle, executable string) error {\n\t\tpid, err := windows.GetProcessId(proc)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to get pid of process %s\", executable)\n\t\t}\n\t\tinfo, err := os.Stat(executable)\n\t\tif err != nil {\n\t\t\t// Maybe the executable has been deleted since.\n\t\t\tlogrus.Debugf(\"failed to look up executable for pid %d: %s\", pid, err)\n\t\t\treturn nil\n\t\t}\n\t\tif os.SameFile(targetInfo, info) {\n\t\t\tmainPid = int(pid)\n\t\t\treturn errFound\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil && !errors.Is(err, errFound) {\n\t\treturn 0, err\n\t}\n\treturn mainPid, nil\n}\n\n// Kill the process group the given process belongs to.  If wait is set, block\n// until the target process exits first before doing so.  On Linux, the process\n// group is only killed if the given pid is its own process group leader.\nfunc KillProcessGroup(pid int, wait bool) error {\n\treturn errors.New(\"KillProcessGroup is not implemented on Windows\")\n}\n\n// TerminateProcessInDirectory terminates all processes where the executable\n// resides within the given directory, as gracefully as possible.  The force\n// parameter is unused on Windows.\nfunc TerminateProcessInDirectory(directory string, force bool) error {\n\treturn iterProcesses(func(proc windows.Handle, executablePath string) error {\n\t\tpid, err := windows.GetProcessId(proc)\n\t\tif err != nil {\n\t\t\tpid = 0\n\t\t}\n\t\tif pid == uint32(os.Getpid()) {\n\t\t\t// Skip terminating the current process.\n\t\t\treturn nil\n\t\t}\n\t\trelPath, err := filepath.Rel(directory, executablePath)\n\t\tif err != nil {\n\t\t\t// This may be because they're on different drives, network shares, etc.\n\t\t\tlogrus.Tracef(\"failed to make pid %d image %s relative to %s: %s\", pid, executablePath, directory, err)\n\t\t\treturn nil\n\t\t}\n\t\tif strings.HasPrefix(relPath, \"..\") {\n\t\t\t// Relative path includes \"../\" prefix, not a child of given directory.\n\t\t\tlogrus.Tracef(\"skipping pid %d (%s), not in %s\", pid, executablePath, directory)\n\t\t\treturn nil\n\t\t}\n\n\t\tlogrus.Tracef(\"will terminate pid %d image %s\", pid, executablePath)\n\t\tif err = windows.TerminateProcess(proc, 0); err != nil {\n\t\t\tlogrus.Errorf(\"failed to terminate pid %d (%s): %s\", pid, executablePath, err)\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/process/process_windows_test.go",
    "content": "package process\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc TestBuildCommandLine(t *testing.T) {\n\tt.Parallel()\n\tcases := [][]string{\n\t\t{\"arg0\", \"a b c\", \"d\", \"e\"},\n\t\t{\"C:\\\\Program Files\\\\arg0\\\\\\\\\", \"ab\\\"c\", \"\\\\\", \"d\"},\n\t\t{\"\\\\\\\\\", \"a\\\\\\\\\\\\b\", \"de fg\", \"h\"},\n\t\t{\"arg0\", \"a\\\\\\\"b\", \"c\", \"d\"},\n\t\t{\"arg0\", \"a\\\\\\\\b c\", \"d\", \"e\"},\n\t\t{\"arg0\", \"ab\\\" c d\"},\n\t\t{\"C:/Path\\\\with/mixed slashes\"},\n\t\t{\"arg0\", \" leading\", \" and \", \"trailing \", \"space\"},\n\t\t{\"special characters\", \"&\", \"|\", \">\", \"<\", \"*\", \"\\\"\", \" \"},\n\t}\n\tfor _, testcase := range cases {\n\t\tt.Run(strings.Join(testcase, \" \"), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresult := buildCommandLine(testcase)\n\t\t\targv, err := windows.DecomposeCommandLine(result)\n\t\t\trequire.NoError(t, err, \"failed to parse result %s\", result)\n\t\t\tassert.Equal(t, testcase, argv, \"failed to round trip arguments via [%s]\", result)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/reg/reg.go",
    "content": "// Package reg is responsible for converting ServerSettingsForJSON structures into\n// importable Windows registry files by running `reg import FILE`.\n//\n// Note that the `reg` command must be run with administrator privileges because it\n// modifies either a section of `HKEY_LOCAL_MACHINE` or `HKEY_CURRENT_USER\\SOFTWARE\\Policies`,\n// both of which require escalated privileges to be modified.\n\npackage reg\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"strings\"\n\t\"unicode/utf16\"\n\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils\"\n)\n\nconst HkcuRegistryHive = \"hkcu\"\nconst HklmRegistryHive = \"hklm\"\n\nfunc escape(s string) string {\n\ts1 := strings.ReplaceAll(s, \"\\\\\", \"\\\\\\\\\")\n\treturn strings.ReplaceAll(s1, `\"`, `\\\\\"`)\n}\n\n// convertToRegFormat recursively reflects the supplied value into lines for a reg file\n//\n// Params:\n// pathParts: represents the registry path to the current item\n// structType: type information on the current field for the `value` parameter\n// value: the reflected value of the current field, based on a simple map[string]interface{} JSON-parse\n// jsonTag: the name of the field, used in json (and the registry)\n// path: a dotted representation of the fully-qualified name of the field\n//\n// Returns two values:\n//\n//\tan array of lines representing the current value\n//\tan error: the only non-nil error this function can return is when it encounters an unhandled value type\nfunc convertToRegFormat(pathParts []string, structType reflect.Type, value reflect.Value, jsonTag, path string) ([]string, error) {\n\tkind := structType.Kind()\n\tif value.Kind() == reflect.Interface && kind != reflect.Interface {\n\t\tif value.IsNil() {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn convertToRegFormat(pathParts, structType, value.Elem(), jsonTag, path)\n\t}\n\tif value.Kind() == reflect.Ptr {\n\t\treturn nil, fmt.Errorf(\"reg-file generation: got an unexpected pointer for %s value %v, expecting type %v\", path, value, structType)\n\t}\n\tswitch kind {\n\tcase reflect.Struct:\n\t\t// Processing here is similar to struct fields in plist.go\n\t\t// In the plist world we want to order the fields according to their\n\t\t// position in the defined ServerSettingsForJSON struct.\n\t\t// In the registry world the fields are ordered alphabetically ignoring case.\n\t\t//\n\t\tif value.Kind() != reflect.Map {\n\t\t\treturn nil, fmt.Errorf(\"expecting actual kind for a typed struct to be a map, got %v\", value.Kind())\n\t\t}\n\t\tnumTypedFields := structType.NumField()\n\t\tsortedStructFields := utils.SortStructFields(structType)\n\t\tscalarReturnedLines := make([]string, 0, numTypedFields)\n\t\tnestedReturnedLines := make([]string, 0)\n\t\tfor i := range sortedStructFields {\n\t\t\tcompoundStructField := &sortedStructFields[i]\n\t\t\tfieldName := compoundStructField.FieldName\n\t\t\tvalueElement := value.MapIndex(reflect.ValueOf(fieldName))\n\t\t\tif valueElement.IsValid() {\n\t\t\t\tnewRetLines, err := convertToRegFormat(append(pathParts, fieldName),\n\t\t\t\t\tcompoundStructField.StructField.Type,\n\t\t\t\t\tvalueElement,\n\t\t\t\t\tfieldName,\n\t\t\t\t\tpath+\".\"+fieldName)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tif len(newRetLines) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// If the first character of the first line is a '[' it's a struct. Otherwise, it's a scalar.\n\t\t\t\t// ']' placed here to appease my IDE's linter.\n\t\t\t\tif newRetLines[0][0] == '[' {\n\t\t\t\t\tnestedReturnedLines = append(nestedReturnedLines, newRetLines...)\n\t\t\t\t} else {\n\t\t\t\t\tscalarReturnedLines = append(scalarReturnedLines, newRetLines...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(scalarReturnedLines) == 0 && len(nestedReturnedLines) == 0 {\n\t\t\treturn nil, nil\n\t\t}\n\t\tretLines := []string{fmt.Sprintf(\"[%s]\", strings.Join(pathParts, \"\\\\\"))}\n\t\tretLines = append(retLines, scalarReturnedLines...)\n\t\tretLines = append(retLines, nestedReturnedLines...)\n\t\treturn retLines, nil\n\tcase reflect.Ptr:\n\t\treturn convertToRegFormat(pathParts, structType.Elem(), value, jsonTag, path)\n\tcase reflect.Slice, reflect.Array:\n\t\tif value.Kind() != reflect.Slice && value.Kind() != reflect.Array {\n\t\t\treturn nil, fmt.Errorf(\"expected slice or array at %s, got %v\", path, value.Kind())\n\t\t}\n\t\t// Currently, all arrays in the options are arrays of strings\n\t\tnumValues := value.Len()\n\t\tarrayValues := make([]string, numValues)\n\t\tfor i := 0; i < numValues; i++ {\n\t\t\titem := value.Index(i)\n\t\t\tfor item.Kind() == reflect.Interface || item.Kind() == reflect.Pointer {\n\t\t\t\titem = item.Elem()\n\t\t\t}\n\t\t\tarrayValues[i] = item.String()\n\t\t}\n\t\treturn []string{fmt.Sprintf(`\"%s\"=hex(7):%s`, jsonTag, stringToMultiStringHexBytes(arrayValues))}, nil\n\tcase reflect.Map:\n\t\treturnedLines := []string{fmt.Sprintf(\"[%s]\", strings.Join(pathParts, \"\\\\\"))}\n\t\tmapKeys := utils.SortKeys(value.MapKeys())\n\t\tfor _, mapKey := range mapKeys {\n\t\t\tkeyAsString := mapKey.StringKey\n\t\t\tinnerLines, err := convertToRegFormat(append(pathParts, keyAsString), structType.Elem(), value.MapIndex(mapKey.MapKey), keyAsString, path+\".\"+keyAsString)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else if len(innerLines) > 0 {\n\t\t\t\treturnedLines = append(returnedLines, innerLines...)\n\t\t\t}\n\t\t}\n\t\treturn returnedLines, nil\n\tcase reflect.Interface:\n\t\t// Since we allow whatever here, just use the actual type of the value.\n\t\t// But if it's an interface{} we'll need to dereference it first to avoid\n\t\t// an infinite loop.\n\t\tfor value.Kind() == reflect.Interface {\n\t\t\tvalue = value.Elem()\n\t\t}\n\t\treturn convertToRegFormat(pathParts, value.Type(), value, jsonTag, path)\n\tcase reflect.Bool:\n\t\tboolValue := map[bool]int{true: 1, false: 0}[value.Bool()]\n\t\treturn []string{fmt.Sprintf(`\"%s\"=dword:%d`, jsonTag, boolValue)}, nil\n\tcase reflect.Int, reflect.Int8, reflect.Int16,\n\t\treflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16,\n\t\treflect.Uint32:\n\t\tif value.CanConvert(reflect.TypeOf(int64(0))) {\n\t\t\tvalue = value.Convert(reflect.TypeOf(int64(0)))\n\t\t}\n\t\treturn []string{fmt.Sprintf(`\"%s\"=dword:%x`, jsonTag, value.Int())}, nil\n\tcase reflect.Int64, reflect.Uint64:\n\t\tif value.CanConvert(reflect.TypeOf(int64(0))) {\n\t\t\tvalue = value.Convert(reflect.TypeOf(int64(0)))\n\t\t}\n\t\treturn []string{fmt.Sprintf(`\"%s\"=qword:%x`, jsonTag, value.Int())}, nil\n\tcase reflect.Float32, reflect.Float64:\n\t\treturn []string{fmt.Sprintf(`\"%s\"=dword:%x`, jsonTag, int(value.Float()))}, nil\n\tcase reflect.String:\n\t\treturn []string{fmt.Sprintf(`\"%s\"=\"%s\"`, jsonTag, escape(value.String()))}, nil\n\t}\n\treturn nil, fmt.Errorf(\"convertToRegFormat: don't know how to process %s kind: %q, (%T), value: %v for var %q\", path, kind, structType, value, jsonTag)\n}\n\n// Encode multi-stringSZ settings in comma-separated ucs2 little-endian bytes\n// e.g.=> [\"abc\", \"def\"] would be ucs-2-encoded as '61,00,62,00,63,00,00,00,64,00,65,00,66,00,00,00,00,00'\n// where a null 16-bit word (so two 00 bytes) separate each pair of words and\n// two null 16-bit words (\"00 00 00 00\") indicate the end of the list\nfunc stringToMultiStringHexBytes(values []string) string {\n\tvalueString := strings.Join(values, \"\\x00\")\n\thex := utf16.Encode([]rune(valueString))\n\thexChars := make([]string, len(hex)*2)\n\tfor i, h := range hex {\n\t\ts := fmt.Sprintf(\"%04x\", h)\n\t\thexChars[2*i] = s[2:4]\n\t\thexChars[2*i+1] = s[0:2]\n\t}\n\tif len(hexChars) == 0 {\n\t\treturn \"00,00\"\n\t}\n\treturn strings.Join(hexChars, \",\") + \",00,00,00,00\"\n}\n\n// JSONToReg - convert the json settings to a reg file\n// @param hiveType: \"hklm\" or \"hkcu\"\n// @param profileType: \"defaults\" or \"locked\"\n// @param settingsBodyAsJSON - options marshaled as JSON\n// @returns: array of strings, intended for writing to a reg file\nfunc JSONToReg(hiveType, profileType, settingsBodyAsJSON string) ([]string, error) {\n\tvar actualSettingsJSON map[string]interface{}\n\n\tfullHiveType, ok := map[string]string{\"hklm\": \"HKEY_LOCAL_MACHINE\", \"hkcu\": \"HKEY_CURRENT_USER\"}[hiveType]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(`unrecognized hiveType of %q, must be \"hklm\" or \"hkcu\"`, hiveType)\n\t}\n\t_, ok = map[string]bool{\"defaults\": true, \"locked\": true}[profileType]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(`unrecognized profileType of %q, must be \"defaults\" or \"locked\"`, profileType)\n\t}\n\tif err := json.Unmarshal([]byte(settingsBodyAsJSON), &actualSettingsJSON); err != nil {\n\t\treturn nil, fmt.Errorf(\"error in json: %s\", err)\n\t}\n\t_, ok = actualSettingsJSON[\"version\"]\n\tif !ok {\n\t\tactualSettingsJSON[\"version\"] = options.CURRENT_SETTINGS_VERSION\n\t}\n\theaderLines := []string{\"Windows Registry Editor Version 5.00\"}\n\tbodyLines, err := convertToRegFormat([]string{fullHiveType, \"SOFTWARE\", \"Policies\", \"Rancher Desktop\", profileType}, reflect.TypeOf(options.ServerSettingsForJSON{}), reflect.ValueOf(actualSettingsJSON), \"\", \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(bodyLines) > 0 {\n\t\theaderLines = append(\n\t\t\theaderLines,\n\t\t\tfmt.Sprintf(\"[%s\\\\%s\\\\%s]\", fullHiveType, \"SOFTWARE\", \"Policies\"),\n\t\t\tfmt.Sprintf(\"[%s\\\\%s\\\\%s\\\\%s]\", fullHiveType, \"SOFTWARE\", \"Policies\", \"Rancher Desktop\"))\n\t}\n\treturn append(headerLines, bodyLines...), nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/reg/reg_test.go",
    "content": "package reg\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\toptions \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/options/generated\"\n)\n\nconst (\n\tDefaultsHeader = \"HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\defaults\"\n)\n\nfunc TestJsonToRegFormat(t *testing.T) {\n\tt.Run(\"complains about bad arguments\", func(t *testing.T) {\n\t\ttype errorTestCases struct {\n\t\t\thiveType      string\n\t\t\tprofileType   string\n\t\t\texpectedError string\n\t\t}\n\t\ttestCases := []errorTestCases{\n\t\t\t{\n\t\t\t\thiveType:      \"bad-hive\",\n\t\t\t\tprofileType:   \"defaults\",\n\t\t\t\texpectedError: `unrecognized hiveType of \"bad-hive\", must be \"hklm\" or \"hkcu\"`,\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:      \"bad-hive\",\n\t\t\t\tprofileType:   \"locked\",\n\t\t\t\texpectedError: `unrecognized hiveType of \"bad-hive\", must be \"hklm\" or \"hkcu\"`,\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:      \"hkcu\",\n\t\t\t\tprofileType:   \"bad-profile\",\n\t\t\t\texpectedError: `unrecognized profileType of \"bad-profile\", must be \"defaults\" or \"locked\"`,\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:      \"hklm\",\n\t\t\t\tprofileType:   \"bad-profile\",\n\t\t\t\texpectedError: `unrecognized profileType of \"bad-profile\", must be \"defaults\" or \"locked\"`,\n\t\t\t},\n\t\t}\n\t\tfor _, testCase := range testCases {\n\t\t\tt.Run(fmt.Sprintf(\"%s:%s\", testCase.hiveType, testCase.profileType), func(t *testing.T) {\n\t\t\t\t_, err := JSONToReg(testCase.hiveType, testCase.profileType, \"\")\n\t\t\t\tassert.ErrorContains(t, err, testCase.expectedError)\n\t\t\t})\n\t\t}\n\t})\n\tt.Run(\"handles empty bodies\", func(t *testing.T) {\n\t\tlines, err := JSONToReg(\"hkcu\", \"defaults\", \"{}\")\n\t\tassert.NoError(t, err)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s]\", DefaultsHeader), lines[3])\n\t\tassert.Equal(t, 5, len(lines))\n\t\tassert.Equal(t, \"Windows Registry Editor Version 5.00\", lines[0])\n\t\tassert.Equal(t, \"[HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies]\", lines[1])\n\t\tassert.Equal(t, \"[HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop]\", lines[2])\n\t\tassert.Equal(t, \"[HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\defaults]\", lines[3])\n\t\tassert.Equal(t, fmt.Sprintf(`\"version\"=dword:%02x`, options.CURRENT_SETTINGS_VERSION), lines[4])\n\t})\n\tt.Run(\"converts the registry-type arguments into reg headers\", func(t *testing.T) {\n\t\ttype testCaseType struct {\n\t\t\thiveType       string\n\t\t\tprofileType    string\n\t\t\texpectedHeader string\n\t\t}\n\t\ttestCases := []testCaseType{\n\t\t\t{\n\t\t\t\thiveType:       \"hkcu\",\n\t\t\t\tprofileType:    \"defaults\",\n\t\t\t\texpectedHeader: DefaultsHeader,\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:       \"hklm\",\n\t\t\t\tprofileType:    \"defaults\",\n\t\t\t\texpectedHeader: \"HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\defaults\",\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:       \"hkcu\",\n\t\t\t\tprofileType:    \"locked\",\n\t\t\t\texpectedHeader: \"HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\locked\",\n\t\t\t},\n\t\t\t{\n\t\t\t\thiveType:       \"hklm\",\n\t\t\t\tprofileType:    \"locked\",\n\t\t\t\texpectedHeader: \"HKEY_LOCAL_MACHINE\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\locked\",\n\t\t\t},\n\t\t}\n\t\tjsonBody := `{\"version\": 19, \"application\": { \"pathManagementStrategy\": \"manual\" } }`\n\t\tfor _, testCase := range testCases {\n\t\t\tt.Run(fmt.Sprintf(\"%s:%s\", testCase.hiveType, testCase.profileType), func(t *testing.T) {\n\t\t\t\tlines, err := JSONToReg(testCase.hiveType, testCase.profileType, jsonBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, 7, len(lines))\n\t\t\t\tassert.Equal(t, fmt.Sprintf(\"[%s]\", testCase.expectedHeader), lines[3])\n\t\t\t\tassert.Equal(t, `\"version\"=dword:13`, lines[4])\n\t\t\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\application]\", testCase.expectedHeader), lines[5])\n\t\t\t\tassert.Equal(t, `\"pathManagementStrategy\"=\"manual\"`, lines[6])\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"Handles arrays\", func(t *testing.T) {\n\t\tjsonBody := `{\"application\": { \"extensions\": { \"allowed\": {\n        \"enabled\": false,\n        \"list\": [\"wink\", \"blink\", \"drink\"]\n     } } }, \"containerEngine\": { \"name\": \"beatrice\" }}`\n\t\theader := \"HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\defaults\"\n\t\tlines, err := JSONToReg(\"hkcu\", \"defaults\", jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 12, len(lines))\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s]\", header), lines[3])\n\t\tassert.Equal(t, fmt.Sprintf(`\"version\"=dword:%02x`, options.CURRENT_SETTINGS_VERSION), lines[4])\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\application]\", header), lines[5])\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\application\\\\extensions]\", header), lines[6])\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\application\\\\extensions\\\\allowed]\", header), lines[7])\n\t\tassert.Equal(t, `\"enabled\"=dword:0`, lines[8])\n\t\tassert.Equal(t, `\"list\"=hex(7):77,00,69,00,6e,00,6b,00,00,00,62,00,6c,00,69,00,6e,00,6b,00,00,00,64,00,72,00,69,00,6e,00,6b,00,00,00,00,00`, lines[9])\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\containerEngine]\", header), lines[10])\n\t\tassert.Equal(t, `\"name\"=\"beatrice\"`, lines[11])\n\t})\n\n\tt.Run(\"Handles maps\", func(t *testing.T) {\n\t\tjsonBody := `{\n \"WSL\": {\n   \"integrations\": {\n\t\t\t\"fish\": true,\n\t\t\t\"sheep\": false,\n\t\t\t\"cows\": 17,\n\t\t\t\"owls\": \"stuff\"\n\t\t}\n  }\n}`\n\t\theader := \"HKEY_CURRENT_USER\\\\SOFTWARE\\\\Policies\\\\Rancher Desktop\\\\defaults\"\n\t\tlines, err := JSONToReg(\"hkcu\", \"defaults\", jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 11, len(lines))\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\WSL]\", header), lines[5])\n\t\tassert.Equal(t, fmt.Sprintf(\"[%s\\\\WSL\\\\integrations]\", header), lines[6])\n\n\t\t// maps aren't processed in json-order, so allow any order\n\t\texpectedMapValues := []string{`\"fish\"=dword:1`, `\"sheep\"=dword:0`, `\"owls\"=\"stuff\"`, `\"cows\"=dword:11`}\n\t\treceivedMapValues := lines[7:11]\n\t\tsort.Strings(expectedMapValues)\n\t\tsort.Strings(receivedMapValues)\n\t\tassert.Equal(t, expectedMapValues, receivedMapValues)\n\t})\n\tt.Run(\"In each node, it first writes out scalar values before writing out sub-objects\", func(t *testing.T) {\n\t\tjsonBody := `{\n  \"version\": 8,\n  \"application\": {\n    \"adminAccess\": false,\n    \"extensions\": {\n      \"allowed\": {\n        \"enabled\": false,\n        \"list\": []\n      }\n    },\n    \"pathManagementStrategy\": \"manual\",\n    \"updater\": {\n      \"enabled\": false\n    },\n    \"autoStart\": false\n  },\n  \"containerEngine\": {\n    \"allowedImages\": {\n      \"patterns\": [\"fable\", \"there\", \"crazy\", \"whine\"],\n      \"enabled\": false\n    },\n    \"name\": \"moby\"\n  }\n}`\n\t\tlines, err := JSONToReg(\"hkcu\", \"defaults\", jsonBody)\n\t\tassert.NoError(t, err)\n\t\texpectedLines := []string{\n\t\t\t`Windows Registry Editor Version 5.00`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies]`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop]`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults]`,\n\t\t\t`\"version\"=dword:8`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application]`,\n\t\t\t`\"adminAccess\"=dword:0`,\n\t\t\t`\"autoStart\"=dword:0`,\n\t\t\t`\"pathManagementStrategy\"=\"manual\"`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\extensions]`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\extensions\\allowed]`,\n\t\t\t`\"enabled\"=dword:0`,\n\t\t\t`\"list\"=hex(7):00,00`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\application\\updater]`,\n\t\t\t`\"enabled\"=dword:0`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\containerEngine]`,\n\t\t\t`\"name\"=\"moby\"`,\n\t\t\t`[HKEY_CURRENT_USER\\SOFTWARE\\Policies\\Rancher Desktop\\defaults\\containerEngine\\allowedImages]`,\n\t\t\t`\"enabled\"=dword:0`,\n\t\t\t`\"patterns\"=hex(7):66,00,61,00,62,00,6c,00,65,00,00,00,74,00,68,00,65,00,72,00,65,00,00,00,63,00,72,00,61,00,7a,00,79,00,00,00,77,00,68,00,69,00,6e,00,65,00,00,00,00,00`,\n\t\t}\n\t\tassert.Equal(t, 20, len(lines))\n\t\tassert.Equal(t, expectedLines, lines)\n\t})\n\tt.Run(\"It handles a full settings file\", func(t *testing.T) {\n\t\tjsonBody := `{\n  \"version\": 8,\n  \"application\": {\n    \"adminAccess\": false,\n    \"debug\": true,\n    \"extensions\": {\n      \"allowed\": {\n        \"enabled\": false,\n        \"list\": [\"found\", \"fully\", \"bawdy\", \"tarot\"]\n      },\n\t\t\t\"installed\": {\n\t\t\t\t\t \"timeCheck1\": \"a\",\n\t\t\t\t\t \"timeCheck2\": \"b\"\n\t\t\t }\n    },\n    \"pathManagementStrategy\": \"manual\",\n    \"telemetry\": {\n      \"enabled\": true\n    },\n    \"updater\": {\n      \"enabled\": false\n    },\n    \"autoStart\": false,\n    \"startInBackground\": false,\n    \"hideNotificationIcon\": false,\n    \"window\": {\n      \"quitOnClose\": false\n    }\n  },\n  \"containerEngine\": {\n    \"allowedImages\": {\n      \"enabled\": false,\n      \"patterns\": [\"fable\", \"there\", \"crazy\", \"whine\"]\n    },\n    \"name\": \"moby\"\n  },\n  \"virtualMachine\": {\n    \"memoryInGB\": 4,\n    \"mount\": {\n      \"type\": \"reverse-sshfs\"\n    },\n    \"numberCPUs\": 2,\n    \"type\": \"qemu\",\n    \"useRosetta\": false\n  },\n  \"WSL\": {\n    \"integrations\": {\n\t\t  \"butte\" : true, \"assay\": false, \"moron\": 55, \"hovel\":\"stuff\"\n\t\t}\n  },\n  \"kubernetes\": {\n    \"version\": \"1.25.9\",\n    \"port\": 6443,\n    \"enabled\": true,\n    \"options\": {\n      \"traefik\": true,\n      \"flannel\": true\n    },\n    \"ingress\": {\n      \"localhostOnly\": false\n    }\n  },\n  \"portForwarding\": {\n    \"includeKubernetesServices\": false\n  },\n  \"images\": {\n    \"showAll\": true,\n    \"namespace\": \"k8s.io\"\n  },\n  \"diagnostics\": {\n    \"showMuted\": false,\n    \"mutedChecks\": {\n       \"check1\": true,\n       \"check2\": false\n    }\n  },\n  \"experimental\": {\n    \"virtualMachine\": {\n      \"mount\": {\n        \"9p\": {\n          \"securityModel\": \"none\",\n          \"protocolVersion\": \"9p2000.L\",\n          \"msizeInKib\": 128,\n          \"cacheMode\": \"mmap\"\n        }\n      },\n      \"proxy\": {\n        \"enabled\": false,\n        \"address\": \"\",\n        \"password\": \"\",\n        \"port\": 3128,\n        \"username\": \"\"\n      }\n    }\n  }\n}\n`\n\t\tlines, err := JSONToReg(\"hkcu\", \"defaults\", jsonBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, 76, len(lines))\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/runner/runner.go",
    "content": "package runner\n\nimport (\n\t\"context\"\n\t\"errors\"\n)\n\nvar ErrContextDone error = errors.New(\"context marked done\")\n\n// TaskRunner accepts functions and asynchronously calls them in the\n// order they were received. Before each function is called, TaskRunner\n// checks whether its context is marked done; if so, it stops calling\n// functions.\ntype TaskRunner struct {\n\tcontext  context.Context\n\tfuncChan chan func() error\n\terrChan  chan error\n}\n\nfunc NewTaskRunner(ctx context.Context) *TaskRunner {\n\tfuncChan := make(chan func() error, 10)\n\terrChan := make(chan error)\n\tgo checkContextBetween(ctx, funcChan, errChan)\n\treturn &TaskRunner{\n\t\tcontext:  ctx,\n\t\tfuncChan: funcChan,\n\t\terrChan:  errChan,\n\t}\n}\n\n// Appends a function to the queue of functions to be called.\nfunc (tr *TaskRunner) Add(function func() error) {\n\ttr.funcChan <- function\n}\n\n// Waits until the last function has completed, returning the first\n// (if any) error returned by a passed function.\nfunc (tr *TaskRunner) Wait() error {\n\tclose(tr.funcChan)\n\terr, ok := <-tr.errChan\n\tif ok {\n\t\treturn err\n\t}\n\treturn nil\n}\n\n// checkContextBetween is the main loop of the TaskRunner type.\nfunc checkContextBetween(ctx context.Context, funcChan <-chan func() error, errChan chan<- error) {\n\tdefer close(errChan)\n\tfor function := range funcChan {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\terrChan <- ErrContextDone\n\t\t\treturn\n\t\tdefault:\n\t\t\tif err := function(); err != nil {\n\t\t\t\terrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/runner/runner_test.go",
    "content": "package runner\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTaskRunner(t *testing.T) {\n\tt.Run(\"should run all functions in order they were added if context not cancelled and no errors\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\ttaskRunner := NewTaskRunner(ctx)\n\t\trunOrder := make([]int, 0, 3)\n\t\tfor i := 1; i < 4; i++ {\n\t\t\ttaskRunner.Add(func() error {\n\t\t\t\trunOrder = append(runOrder, i)\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t\tassert.NoError(t, taskRunner.Wait())\n\t\tassert.Equal(t, []int{1, 2, 3}, runOrder)\n\t})\n\n\tt.Run(\"should stop execution after current function when context is cancelled\", func(t *testing.T) {\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\ttaskRunner := NewTaskRunner(ctx)\n\n\t\t// Used to delay until func1 has started running\n\t\treadyForCancelChan := make(chan struct{})\n\t\t// Gets closed when we want func1 to return\n\t\tfunc1Chan := make(chan struct{})\n\t\tfunc1Ran := false\n\t\tfunc1 := func() error {\n\t\t\tfunc1Ran = true\n\t\t\tt.Log(\"func1 ran\")\n\t\t\tclose(readyForCancelChan)\n\t\t\t<-func1Chan\n\t\t\treturn nil\n\t\t}\n\n\t\tfunc2Ran := false\n\t\tfunc2 := func() error {\n\t\t\tfunc2Ran = true\n\t\t\tt.Log(\"func2 ran\")\n\t\t\treturn nil\n\t\t}\n\n\t\ttaskRunner.Add(func1)\n\t\ttaskRunner.Add(func2)\n\t\t<-readyForCancelChan\n\t\tcancel()\n\t\tclose(func1Chan)\n\n\t\tassert.ErrorIs(t, taskRunner.Wait(), ErrContextDone)\n\t\tassert.True(t, func1Ran)\n\t\tassert.False(t, func2Ran)\n\t})\n\n\tt.Run(\"should return error from first function that errors out and not run subsequent functions\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\ttaskRunner := NewTaskRunner(ctx)\n\n\t\texpectedError := \"func1 error\"\n\t\tranSlice := make([]bool, 2)\n\t\tfor i := range ranSlice {\n\t\t\ttaskRunner.Add(func() error {\n\t\t\t\tranSlice[i] = true\n\t\t\t\tt.Logf(\"func%d ran\", i+1)\n\t\t\t\treturn fmt.Errorf(\"func%d error\", i+1)\n\t\t\t})\n\t\t}\n\t\tif err := taskRunner.Wait(); err != nil {\n\t\t\tassert.Equal(t, expectedError, err.Error())\n\t\t}\n\t\tassert.True(t, ranSlice[0])\n\t\tassert.False(t, ranSlice[1])\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/shell/shell.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package shell contains a function to help with spawning commands in the\n// Rancher Desktop VM.\npackage shell\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/text/encoding/unicode\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/command\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lima\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl\"\n)\n\n// Spawn a command that, when run, will be executed in the VM with the given\n// arguments.\nfunc SpawnCommand(ctx context.Context, args ...string) (*exec.Cmd, error) {\n\tcommandName, err := directories.GetLimactlPath()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif runtime.GOOS == \"windows\" {\n\t\tdistroNames := []string{wsl.DistributionName}\n\t\tfound := false\n\n\t\tif _, err = os.Stat(commandName); err == nil {\n\t\t\t// If limactl is available, try the lima distribution first.\n\t\t\tdistroNames = append([]string{lima.InstanceFullName}, distroNames...)\n\t\t}\n\n\t\tfor _, distroName := range distroNames {\n\t\t\terr = assertWSLIsRunning(ctx, distroName)\n\t\t\tif err == nil {\n\t\t\t\tcommandName = \"wsl\"\n\t\t\t\targs = append([]string{\n\t\t\t\t\t\"--distribution\", distroName,\n\t\t\t\t\t\"--exec\", \"/usr/local/bin/wsl-exec\",\n\t\t\t\t}, args...)\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tif !found {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\tp, err := paths.GetPaths()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := directories.SetupLimaHome(p.AppHome); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := setupPathEnvVar(p); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err := checkLimaIsRunning(ctx, commandName); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\targs = append([]string{\"shell\", lima.InstanceName}, args...)\n\t}\n\treturn exec.CommandContext(ctx, commandName, args...), nil\n}\n\n// Set up the PATH environment variable for limactl.\nfunc setupPathEnvVar(p *paths.Paths) error {\n\tif runtime.GOOS != \"windows\" {\n\t\t// This is only needed on Windows.\n\t\treturn nil\n\t}\n\tmsysDir := filepath.Join(utils.GetParentDir(p.Resources, 2), \"msys\")\n\tpathList := filepath.SplitList(os.Getenv(\"PATH\"))\n\tif slices.Contains(pathList, msysDir) {\n\t\treturn nil\n\t}\n\tpathList = append([]string{msysDir}, pathList...)\n\treturn os.Setenv(\"PATH\", strings.Join(pathList, string(os.PathListSeparator)))\n}\n\nfunc checkLimaIsRunning(ctx context.Context, commandName string) error {\n\tvar stdout bytes.Buffer\n\tvar stderr bytes.Buffer\n\n\tconst desiredState = \"Running\"\n\n\t//nolint:gosec // The command name is auto-detected, and the instance name is constant.\n\tcmd := exec.CommandContext(ctx, commandName, \"ls\", lima.InstanceName, \"--format\", \"{{.Status}}\")\n\tcmd.Stdout = &stdout\n\tcmd.Stderr = &stderr\n\tif err := cmd.Run(); err != nil {\n\t\tlogrus.Errorf(\"Failed to run %q: %s\\n\", cmd, err)\n\t\treturn command.NewFatalError(\"\", 1)\n\t}\n\tlimaState := strings.TrimRight(stdout.String(), \"\\n\")\n\t// We can do an equals check here because we should only have received the status for VM 0\n\tif limaState == desiredState {\n\t\treturn nil\n\t}\n\tif limaState != \"\" {\n\t\treturn command.NewVMStateError(ctx, desiredState, limaState)\n\t}\n\terrorMsg := stderr.String()\n\tif strings.Contains(errorMsg, fmt.Sprintf(\"No instance matching %s found.\", lima.InstanceName)) {\n\t\treturn command.NewVMStateError(ctx, desiredState, \"\")\n\t} else if errorMsg != \"\" {\n\t\treturn command.NewFatalError(errorMsg, 1)\n\t}\n\treturn command.NewFatalError(\"Underlying limactl check failed with no output.\", 1)\n}\n\n// Check that WSL is running the given distribution; if not, an error will be\n// returned with a message suitable for printing to the user.\nfunc assertWSLIsRunning(ctx context.Context, distroName string) error {\n\t// Ignore error messages; none are expected here\n\trawOutput, err := exec.CommandContext(ctx, \"wsl\", \"--list\", \"--verbose\").CombinedOutput()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to run 'wsl --list --verbose': %w\", err)\n\t}\n\tdecoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()\n\toutput, err := decoder.Bytes(rawOutput)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read WSL output ([% q]...); error: %w\", rawOutput[:12], err)\n\t}\n\tactualState := \"\"\n\tfor _, line := range regexp.MustCompile(`\\r?\\n`).Split(string(output), -1) {\n\t\tfields := regexp.MustCompile(`\\s+`).Split(strings.TrimLeft(line, \" \\t\"), -1)\n\t\tif fields[0] == \"*\" {\n\t\t\tfields = fields[1:]\n\t\t}\n\t\tif len(fields) >= 2 && fields[0] == distroName {\n\t\t\tactualState = fields[1]\n\t\t\tbreak\n\t\t}\n\t}\n\tconst desiredState = \"Running\"\n\tif actualState == desiredState {\n\t\treturn nil\n\t}\n\n\treturn command.NewVMStateError(ctx, desiredState, actualState)\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/shutdown/shutdown.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\t\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage shutdown\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/hashicorp/go-multierror\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/factoryreset\"\n\tp \"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils\"\n)\n\ntype shutdownData struct {\n\twaitForShutdown bool\n}\n\ntype InitiatingCommand string\n\nconst (\n\tShutdown     InitiatingCommand = \"shutdown\"\n\tFactoryReset InitiatingCommand = \"factory-reset\"\n\t// When killing an application, the number of times to retry.\n\tappKillRetryCount = 15\n\t// When killing an application, time interval between retries.\n\tappKillWaitInterval = 2 * time.Second\n)\n\nvar limaCtlPath string\n\nfunc newShutdownData(waitForShutdown bool) *shutdownData {\n\treturn &shutdownData{waitForShutdown: waitForShutdown}\n}\n\n// FinishShutdown - ensures that none of the Rancher Desktop related processes are around\n// after a graceful shutdown command has been sent as part of either `rdctl shutdown` or\n// `rdctl factory-reset`.\nfunc FinishShutdown(ctx context.Context, waitForShutdown bool, initiatingCommand InitiatingCommand) error {\n\ts := newShutdownData(waitForShutdown)\n\tif runtime.GOOS == \"windows\" {\n\t\treturn s.waitForAppToDieOrKillIt(ctx, factoryreset.CheckProcessWindows, factoryreset.KillRancherDesktop, false, \"the app\")\n\t}\n\tpaths, err := p.GetPaths()\n\tif err != nil {\n\t\tlogrus.Errorf(\"Ignoring error trying to get application paths: %s\", err)\n\t} else if err = directories.SetupLimaHome(paths.AppHome); err != nil {\n\t\tlogrus.Errorf(\"Ignoring error trying to get lima directory: %s\", err)\n\t} else {\n\t\tlimaCtlPath, err = directories.GetLimactlPath()\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"Ignoring error trying to get path to limactl: %s\", err)\n\t\t} else {\n\t\t\tswitch initiatingCommand {\n\t\t\tcase Shutdown:\n\t\t\t\terr = s.waitForAppToDieOrKillIt(ctx, checkLima, stopLima, false, \"lima\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Errorf(\"Ignoring error trying to stop lima: %s\", err)\n\t\t\t\t}\n\t\t\t\t// Check once more to see if lima is still running, and if so, run `limactl stop --force 0`\n\t\t\t\terr = s.waitForAppToDieOrKillIt(ctx, checkLima, stopLimaWithForce, true, \"lima\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Errorf(\"Ignoring error trying to force-stop lima: %s\", err)\n\t\t\t\t}\n\t\t\tcase FactoryReset:\n\t\t\t\terr = s.waitForAppToDieOrKillIt(ctx, checkLima, deleteLima, false, \"lima\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tlogrus.Errorf(\"Ignoring error trying to delete lima subtree: %s\", err)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"internal error: unknown shutdown initiating command of %q\", initiatingCommand)\n\t\t\t}\n\t\t}\n\t}\n\tqemuExecutable, err := getQemuExecutable()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find qemu executable: %w\", err)\n\t}\n\terr = s.waitForAppToDieOrKillIt(\n\t\tctx,\n\t\tisExecutableRunningFunc(qemuExecutable),\n\t\tterminateExecutableFunc(qemuExecutable),\n\t\tfalse,\n\t\t\"qemu\")\n\tif err != nil {\n\t\tlogrus.Errorf(\"Ignoring error trying to kill qemu: %s\", err)\n\t}\n\tappDir, err := directories.GetApplicationDirectory(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to find application directory: %w\", err)\n\t}\n\tmainExecutablePath, err := p.GetMainExecutable(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get Rancher Desktop executable: %w\", err)\n\t}\n\treturn s.waitForAppToDieOrKillIt(\n\t\tctx,\n\t\tisExecutableRunningFunc(mainExecutablePath),\n\t\tterminateRancherDesktopFunc(appDir),\n\t\tfalse,\n\t\t\"the app\")\n}\n\n// Run the given check function to detect if an application has exited, every\n// appKillWaitInterval for appKillRetryCount times.  After all the checks have\n// expired, run killFunc to terminate the application forcefully.  If skipRetry\n// is true, do not wait at all and just kill immediately.\nfunc (s *shutdownData) waitForAppToDieOrKillIt(ctx context.Context, checkFunc func(context.Context) (bool, error), killFunc func(context.Context) error, skipRetry bool, description string) error {\n\tfor iter := 0; s.waitForShutdown && iter < appKillRetryCount; iter++ {\n\t\tif iter > 0 {\n\t\t\tlogrus.Debugf(\"checking %s showed it's still running; sleeping for %s\\n\", description, appKillWaitInterval)\n\t\t\ttime.Sleep(appKillWaitInterval)\n\t\t}\n\t\tstatus, err := checkFunc(ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"while checking %s, found error: %w\", description, err)\n\t\t}\n\t\tif !status {\n\t\t\tlogrus.Debugf(\"%s is no longer running\\n\", description)\n\t\t\treturn nil\n\t\t}\n\t\tif skipRetry {\n\t\t\tbreak\n\t\t}\n\t}\n\tlogrus.Debugf(\"About to force-kill %s\\n\", description)\n\treturn killFunc(ctx)\n}\n\nfunc getQemuExecutable() (string, error) {\n\tif runtime.GOOS == \"windows\" {\n\t\treturn \"\", fmt.Errorf(\"qemu not installed on Windows\")\n\t}\n\tresourcesDir, err := p.GetResourcesPath()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get resources directory: %w\", err)\n\t}\n\tvar arch string\n\tswitch runtime.GOARCH {\n\tcase \"amd64\":\n\t\tarch = \"x86_64\"\n\tcase \"arm64\":\n\t\tarch = \"aarch64\"\n\tdefault:\n\t\tarch = runtime.GOARCH\n\t}\n\tqemuName := fmt.Sprintf(\"qemu-system-%s\", arch)\n\tcandidates := []string{\n\t\tfilepath.Join(resourcesDir, runtime.GOOS, \"lima\", \"bin\", qemuName),\n\t}\n\tif runtime.GOOS == \"linux\" {\n\t\t// On Linux, we may be running in AppImage; in that case, we need to check\n\t\t// the bundled qemu.\n\t\tcandidates = append(\n\t\t\tcandidates,\n\t\t\tfilepath.Join(utils.GetParentDir(resourcesDir, 4), \"usr\", \"bin\", qemuName),\n\t\t)\n\t}\n\treturn p.FindFirstExecutable(candidates...)\n}\n\nfunc isExecutableRunningFunc(executablePath string) func(context.Context) (bool, error) {\n\treturn func(ctx context.Context) (bool, error) {\n\t\tpid, err := process.FindPidOfProcess(executablePath)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\treturn pid != 0, nil\n\t}\n}\n\nfunc terminateExecutableFunc(executablePath string) func(context.Context) error {\n\treturn func(ctx context.Context) error {\n\t\tpid, err := process.FindPidOfProcess(executablePath)\n\t\tif err != nil || pid == 0 {\n\t\t\treturn err\n\t\t}\n\t\tproc, err := os.FindProcess(pid)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to find process for pid %d: %w\", pid, err)\n\t\t}\n\t\t// The pid might not exist even if we did not receive an error.\n\t\terr = proc.Signal(syscall.SIGTERM)\n\t\tif err != nil && !errors.Is(err, os.ErrProcessDone) {\n\t\t\treturn fmt.Errorf(\"failed to terminate process %d: %w\", pid, err)\n\t\t}\n\t\treturn nil\n\t}\n}\n\nfunc checkLima(ctx context.Context) (bool, error) {\n\tcmd := exec.CommandContext(ctx, limaCtlPath, \"ls\", \"--format\", \"{{.Status}}\", \"0\")\n\tcmd.Stderr = os.Stderr\n\tresult, err := cmd.Output()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn strings.HasPrefix(string(result), \"Running\"), nil\n}\n\nfunc runCommandIgnoreOutput(cmd *exec.Cmd) error {\n\tcmd.Stdin = os.Stdin\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n}\n\nfunc stopLima(ctx context.Context) error {\n\treturn runCommandIgnoreOutput(exec.CommandContext(ctx, limaCtlPath, \"stop\", \"0\"))\n}\n\nfunc stopLimaWithForce(ctx context.Context) error {\n\treturn runCommandIgnoreOutput(exec.CommandContext(ctx, limaCtlPath, \"stop\", \"--force\", \"0\"))\n}\n\nfunc deleteLima(ctx context.Context) error {\n\treturn runCommandIgnoreOutput(exec.CommandContext(ctx, limaCtlPath, \"delete\", \"--force\", \"0\"))\n}\n\nfunc terminateRancherDesktopFunc(appDir string) func(context.Context) error {\n\treturn func(ctx context.Context) error {\n\t\tvar errs *multierror.Error\n\n\t\terrs = multierror.Append(errs, (func() error {\n\t\t\tmainExe, err := p.GetMainExecutable(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tpid, err := process.FindPidOfProcess(mainExe)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn process.KillProcessGroup(pid, false)\n\t\t})())\n\n\t\terrs = multierror.Append(errs, process.TerminateProcessInDirectory(appDir, true))\n\n\t\treturn errs.ErrorOrNil()\n\t}\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/copyFile_darwin.go",
    "content": "package snapshot\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\n// Copies a file from src to dst. If copyOnWrite is true, attempts to\n// use clonefile syscall to do the copy. If clonefile is not supported\n// by the underlying filesystem, or src and dst are on different\n// drives, falls back to a plain copy. If copyOnWrite is false, does a\n// plain copy.\nfunc copyFile(dst, src string, copyOnWrite bool, fileMode os.FileMode) error {\n\tif err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination parent dir: %w\", err)\n\t}\n\tif copyOnWrite {\n\t\tif err := os.RemoveAll(dst); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove existing destination file: %w\", err)\n\t\t}\n\t\tif err := unix.Clonefile(src, dst, 0); err == nil {\n\t\t\treturn nil\n\t\t} else if !errors.Is(err, unix.ENOTSUP) && !errors.Is(err, unix.EXDEV) {\n\t\t\treturn fmt.Errorf(\"failed to clone src to dest: %w\", err)\n\t\t}\n\t}\n\tsrcFd, err := os.Open(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open source file: %w\", err)\n\t}\n\tdefer srcFd.Close()\n\tdstFd, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open destination file: %w\", err)\n\t}\n\tdefer dstFd.Close()\n\tif _, err := io.Copy(dstFd, srcFd); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy contents of src to dst: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/copyFile_linux.go",
    "content": "package snapshot\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/unix\"\n)\n\n// Copies a file from src to dst. If copyOnWrite is true, attempts to\n// use ioctl FICLONE to do the copy. If ioctl FICLONE is not supported\n// by the underlying filesystem, falls back to a plain copy. If\n// copyOnWrite is false, does a plain copy. fileMode specifies the\n// permissions that are applied to the destination file.\nfunc copyFile(dst, src string, copyOnWrite bool, fileMode os.FileMode) error {\n\tsrcFd, err := os.Open(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open source file: %w\", err)\n\t}\n\tdefer srcFd.Close()\n\tif err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination parent dir: %w\", err)\n\t}\n\tdstFd, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fileMode)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open destination file: %w\", err)\n\t}\n\tdefer dstFd.Close()\n\tif copyOnWrite {\n\t\tif err := unix.IoctlFileClone(int(dstFd.Fd()), int(srcFd.Fd())); err == nil {\n\t\t\treturn nil\n\t\t} else if !errors.Is(err, unix.ENOTSUP) {\n\t\t\treturn fmt.Errorf(\"failed to ioctl_ficlone file: %w\", err)\n\t\t}\n\t}\n\tif _, err := io.Copy(dstFd, srcFd); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy contents of src to dst: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/manager.go",
    "content": "package snapshot\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\t\"unicode\"\n\n\t\"github.com/google/uuid\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lock\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n)\n\nconst completeFileName = \"complete.txt\"\nconst completeFileContents = \"The presence of this file indicates that this snapshot is complete and valid.\"\nconst maxNameLength = 250\nconst nameDisplayCutoffSize = 30\n\n// Manager handles all snapshot-related functionality.\ntype Manager struct {\n\tSnapshotter\n\t*paths.Paths\n\tlock.BackendLocker\n}\n\nfunc NewManager() (*Manager, error) {\n\tappPaths, err := paths.GetPaths()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmanager := &Manager{\n\t\tPaths:         appPaths,\n\t\tSnapshotter:   NewSnapshotterImpl(),\n\t\tBackendLocker: &lock.BackendLock{},\n\t}\n\treturn manager, nil\n}\n\n// Snapshot returns a Snapshot object for an existing and complete snapshot with the given name.\n// It will return an error if no snapshot is found, or if the snapshot is not complete.\nfunc (manager *Manager) Snapshot(name string) (Snapshot, error) {\n\tsnapshots, err := manager.List(false)\n\tif err != nil {\n\t\treturn Snapshot{}, fmt.Errorf(\"failed to list snapshots: %w\", err)\n\t}\n\tfor _, candidate := range snapshots {\n\t\tif name == candidate.Name {\n\t\t\treturn candidate, nil\n\t\t}\n\t}\n\treturn Snapshot{}, fmt.Errorf(`can't find snapshot %q`, name)\n}\n\nfunc (manager *Manager) SnapshotDirectory(snapshot Snapshot) string {\n\treturn filepath.Join(manager.Snapshots, snapshot.ID)\n}\n\n// ValidateName checks that name is a valid snapshot name and that\n// it is not used by an existing snapshot.\nfunc (manager *Manager) ValidateName(name string) error {\n\tif name == \"\" {\n\t\treturn fmt.Errorf(\"snapshot name must not be the empty string\")\n\t}\n\truneName := []rune(name)\n\tif len(runeName) > maxNameLength {\n\t\terrMsgName := truncate(name, nameDisplayCutoffSize)\n\t\treturn fmt.Errorf(`invalid name %q: max length is %d, %d were specified`, errMsgName, maxNameLength, len(runeName))\n\t}\n\tif err := checkForInvalidCharacter(name); err != nil {\n\t\treturn err\n\t}\n\tif unicode.IsSpace(rune(name[0])) {\n\t\terrMsgName := truncate(name, nameDisplayCutoffSize)\n\t\treturn fmt.Errorf(`invalid name %q: must not start with a white-space character`, errMsgName)\n\t}\n\tif unicode.IsSpace(runeName[len(runeName)-1]) {\n\t\terrMsgName := name\n\t\tif len(runeName) > nameDisplayCutoffSize {\n\t\t\terrMsgName = \"…\" + string(runeName[len(runeName)-nameDisplayCutoffSize:])\n\t\t}\n\t\treturn fmt.Errorf(`invalid name %q: must not end with a white-space character`, errMsgName)\n\t}\n\tcurrentSnapshots, err := manager.List(false)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to list snapshots: %w\", err)\n\t}\n\tfor _, currentSnapshot := range currentSnapshots {\n\t\tif currentSnapshot.Name == name {\n\t\t\terrMsgName := truncate(name, nameDisplayCutoffSize)\n\t\t\treturn fmt.Errorf(\"name %q already exists\", errMsgName)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (manager *Manager) writeMetadataFile(snapshot Snapshot) error {\n\tsnapshotDir := manager.SnapshotDirectory(snapshot)\n\tif err := os.MkdirAll(snapshotDir, 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create snapshot directory: %w\", err)\n\t}\n\tmetadataPath := filepath.Join(snapshotDir, \"metadata.json\")\n\tmetadataFile, err := os.Create(metadataPath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create metadata file: %w\", err)\n\t}\n\tdefer metadataFile.Close()\n\tencoder := json.NewEncoder(metadataFile)\n\tencoder.SetIndent(\"\", \"  \")\n\tif err := encoder.Encode(snapshot); err != nil {\n\t\treturn fmt.Errorf(\"failed to write metadata file: %w\", err)\n\t}\n\treturn nil\n}\n\n// Create a new snapshot.\nfunc (manager *Manager) Create(ctx context.Context, name, description string) (Snapshot, error) {\n\tid, err := uuid.NewRandom()\n\tif err != nil {\n\t\treturn Snapshot{}, fmt.Errorf(\"failed to generate ID for snapshot: %w\", err)\n\t}\n\tsnapshot := Snapshot{\n\t\tCreated:     time.Now(),\n\t\tName:        name,\n\t\tID:          id.String(),\n\t\tDescription: description,\n\t}\n\taction := fmt.Sprintf(\"Creating snapshot %q\", name)\n\tif err := manager.Lock(ctx, manager.Paths, action); err != nil {\n\t\treturn snapshot, err\n\t}\n\tdefer func() {\n\t\tif err != nil {\n\t\t\tos.RemoveAll(manager.SnapshotDirectory(snapshot))\n\t\t}\n\t\tunlockErr := manager.Unlock(ctx, manager.Paths, true)\n\t\tif err == nil {\n\t\t\terr = unlockErr\n\t\t}\n\t}()\n\t// (Re)validate the name after acquiring the lock in case another process created a snapshot with the same name\n\tif err := manager.ValidateName(name); err != nil {\n\t\treturn snapshot, err\n\t}\n\tif err = manager.writeMetadataFile(snapshot); err == nil {\n\t\terr = manager.CreateFiles(ctx, manager.Paths, manager.SnapshotDirectory(snapshot))\n\t}\n\treturn snapshot, err\n}\n\n// List snapshots that are present on the system. If includeIncomplete is\n// true, includes snapshots that are currently being created, are currently\n// being deleted, or are otherwise incomplete and cannot be restored from.\nfunc (manager *Manager) List(includeIncomplete bool) ([]Snapshot, error) {\n\tdirEntries, err := os.ReadDir(manager.Snapshots)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\treturn []Snapshot{}, fmt.Errorf(\"failed to read snapshots directory: %w\", err)\n\t}\n\tsnapshots := make([]Snapshot, 0, len(dirEntries))\n\tfor _, dirEntry := range dirEntries {\n\t\tif _, err := uuid.Parse(dirEntry.Name()); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tsnapshot := Snapshot{}\n\t\tmetadataPath := filepath.Join(manager.Snapshots, dirEntry.Name(), \"metadata.json\")\n\t\tcontents, err := os.ReadFile(metadataPath)\n\t\tif err != nil {\n\t\t\treturn []Snapshot{}, fmt.Errorf(\"failed to read %q: %w\", metadataPath, err)\n\t\t}\n\t\tif err := json.Unmarshal(contents, &snapshot); err != nil {\n\t\t\treturn []Snapshot{}, fmt.Errorf(\"failed to unmarshal contents of %q: %w\", metadataPath, err)\n\t\t}\n\t\t// TODO this should be done by the caller\n\t\tsnapshot.Created = snapshot.Created.Local()\n\n\t\tcompleteFilePath := filepath.Join(manager.Snapshots, snapshot.ID, completeFileName)\n\t\t_, err = os.Stat(completeFilePath)\n\t\tcompleteFileExists := err == nil\n\n\t\tif !includeIncomplete && !completeFileExists {\n\t\t\tcontinue\n\t\t}\n\n\t\tsnapshots = append(snapshots, snapshot)\n\t}\n\treturn snapshots, nil\n}\n\n// Delete a snapshot.\nfunc (manager *Manager) Delete(name string) error {\n\tsnapshot, err := manager.Snapshot(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsnapshotDir := manager.SnapshotDirectory(snapshot)\n\t// Remove complete.txt file. This must be done first because restoring\n\t// from a partially-deleted snapshot could result in errors.\n\terr = os.RemoveAll(filepath.Join(snapshotDir, completeFileName))\n\treturn errors.Join(err, os.RemoveAll(snapshotDir))\n}\n\n// Restore Rancher Desktop to the state saved in a snapshot.\nfunc (manager *Manager) Restore(ctx context.Context, name string) (err error) {\n\tsnapshot, err := manager.Snapshot(name)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\taction := fmt.Sprintf(\"Restoring snapshot %q\", name)\n\tif err := manager.Lock(ctx, manager.Paths, action); err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\t// Restart the backend only if a data reset occurred\n\t\tunlockErr := manager.Unlock(ctx, manager.Paths, !errors.Is(err, ErrDataReset))\n\t\tif err == nil {\n\t\t\terr = unlockErr\n\t\t}\n\t}()\n\t// If the context is marked done (i.e. the user cancelled the\n\t// operation) we can avoid running RestoreFiles() and thus avoid\n\t// an unnecessary data reset.\n\tif contextIsDone(ctx) {\n\t\treturn runner.ErrContextDone\n\t}\n\tif err = manager.RestoreFiles(ctx, manager.Paths, manager.SnapshotDirectory(snapshot)); err != nil {\n\t\treturn fmt.Errorf(\"failed to restore files: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc checkForInvalidCharacter(name string) error {\n\tfor idx, c := range name {\n\t\tif !unicode.IsPrint(c) {\n\t\t\treturn fmt.Errorf(\"invalid character %q at position %d in name: all characters must be printable or a space\", c, idx)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc contextIsDone(ctx context.Context) bool {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Does a utf8-aware truncation of input to maximum maxChars\n// unicode code points. Adds an ellipsis if truncation occurred.\nfunc truncate(input string, maxChars int) string {\n\truneInput := []rune(input)\n\tif len(runeInput) > maxChars {\n\t\treturn string(runeInput[0:maxChars-1]) + \"…\"\n\t}\n\treturn input\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/manager_test.go",
    "content": "package snapshot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n)\n\ntype TestFile struct {\n\tPath     string\n\tContents string\n}\n\nfunc TestManager(t *testing.T) {\n\tt.Run(\"ValidateName should disallow two snapshots with the same name, but only when the first is complete\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tsnapshotName := \"test-snapshot\"\n\t\tif err := manager.ValidateName(snapshotName); err != nil {\n\t\t\tt.Fatalf(\"failed to validate first snapshot: %s\", err)\n\t\t}\n\t\tsnapshot, err := manager.Create(context.Background(), snapshotName, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create first snapshot: %s\", err)\n\t\t}\n\t\tif err := manager.ValidateName(snapshotName); err == nil {\n\t\t\tt.Fatalf(\"name validation failed to return error when complete snapshot with name %q exists\", snapshotName)\n\t\t}\n\t\tcompleteFilePath := filepath.Join(manager.Snapshots, snapshot.ID, completeFileName)\n\t\tif err := os.Remove(completeFilePath); err != nil {\n\t\t\tt.Fatalf(\"failed to remove %q from first snapshot: %s\", completeFileName, err)\n\t\t}\n\t\tif err := manager.ValidateName(snapshotName); err != nil {\n\t\t\tt.Fatalf(\"name validation returned error when complete snapshot with name %q does not exist: %s\", snapshotName, err)\n\t\t}\n\t})\n\n\ttestCases := []struct {\n\t\tName          string\n\t\tExpectedError string\n\t\t// When the name we are validating is above a certain length, it should\n\t\t// be truncated to a certain length in the error message. Otherwise, it\n\t\t// should be left as is.\n\t\tExpectedErrMsgName string\n\t}{\n\t\t{\n\t\t\tName:          \"\", // empty string not allowed\n\t\t\tExpectedError: \"snapshot name must not be the empty string\",\n\t\t},\n\t\t{\n\t\t\tName:               \" can't start with a space\",\n\t\t\tExpectedError:      \"must not start with a white-space character\",\n\t\t\tExpectedErrMsgName: \" can't start with a space\",\n\t\t},\n\t\t{\n\t\t\tName:               \" 12345678911234567892123456我喜欢鸡肉\",\n\t\t\tExpectedError:      \"must not start with a white-space character\",\n\t\t\tExpectedErrMsgName: \" 12345678911234567892123456我喜…\",\n\t\t},\n\t\t{\n\t\t\tName:               `can't end with a \"space\" `,\n\t\t\tExpectedError:      \"must not end with a white-space character\",\n\t\t\tExpectedErrMsgName: `can't end with a \"space\" `,\n\t\t},\n\t\t{\n\t\t\tName:               `我喜欢鸡肉1234567891123456789212345 `,\n\t\t\tExpectedError:      \"must not end with a white-space character\",\n\t\t\tExpectedErrMsgName: `…喜欢鸡肉1234567891123456789212345 `,\n\t\t},\n\t\t{\n\t\t\tName:               \"filename_too_long_workaround\",\n\t\t\tExpectedError:      \"max length is\",\n\t\t\tExpectedErrMsgName: \"12345678911234567892123456789…\",\n\t\t},\n\t\t{\n\t\t\tName:          \"can't contain a \\t tab\",\n\t\t\tExpectedError: `invalid character '\\t' at position 16 in name: all characters must be printable or a space`,\n\t\t},\n\t\t{\n\t\t\tName:          \"can't contain a \\n newline\",\n\t\t\tExpectedError: `invalid character '\\n' at position 16 in name: all characters must be printable or a space`,\n\t\t},\n\t\t{\n\t\t\tName:          \"can't contain a \\r carriage-return\",\n\t\t\tExpectedError: `invalid character '\\r' at position 16 in name: all characters must be printable or a space`,\n\t\t},\n\t\t{\n\t\t\tName:          \"can't contain a \\x00 null-byte\",\n\t\t\tExpectedError: `invalid character '\\x00' at position 16 in name: all characters must be printable or a space`,\n\t\t},\n\t\t{\n\t\t\tName:          \"can't contain a \\a control character\",\n\t\t\tExpectedError: `invalid character '\\a' at position 16 in name: all characters must be printable or a space`,\n\t\t},\n\t}\n\tfor _, testCase := range testCases {\n\t\tdescription := fmt.Sprintf(\"ValidateName should disallow invalid names (case %+v)\", testCase)\n\t\tt.Run(description, func(t *testing.T) {\n\t\t\tpaths, _ := populateFiles(t, true)\n\t\t\tmanager := newTestManager(paths)\n\t\t\t// The test case name is used in the name of a file inside the temporary\n\t\t\t// directory, which means it is limited in length. Work around this.\n\t\t\tif testCase.Name == \"filename_too_long_workaround\" {\n\t\t\t\t// 251 characters is too long (and the indentation here is what our linter demands)\n\t\t\t\ttestCase.Name = \"12345678911234567892123456789312345678941234567895123456789612345678971234567898\" +\n\t\t\t\t\t\"12345678991234567890123456789112345678921234567893123456789412345678951234567896\" +\n\t\t\t\t\t\"12345678971234567898123456789912345678901234567891123456789212345678931234567894\" +\n\t\t\t\t\t\"12345678951\"\n\t\t\t}\n\t\t\terr := manager.ValidateName(testCase.Name)\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected error but got err == nil\")\n\t\t\t} else if !strings.Contains(err.Error(), testCase.ExpectedError) {\n\t\t\t\tt.Errorf(\"unexpected error %q\", err)\n\t\t\t}\n\t\t\t// check that we are truncating the name properly in the error message\n\t\t\tif len(testCase.ExpectedErrMsgName) > 0 {\n\t\t\t\tif !strings.Contains(err.Error(), strconv.Quote(testCase.ExpectedErrMsgName)) {\n\t\t\t\t\tt.Errorf(\"error %q does not contain name %q\", err, strconv.Quote(testCase.ExpectedErrMsgName))\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Should create these valid names\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tvalidNames := []string{\n\t\t\t`no \"spaces\" at either end`,\n\t\t\t// 250 characters is ok\n\t\t\t\"12345678911234567892123456789312345678941234567895123456789612345678971234567898\" +\n\t\t\t\t\"12345678991234567890123456789112345678921234567893123456789412345678951234567896\" +\n\t\t\t\t\"12345678971234567898123456789912345678901234567891123456789212345678931234567894\" +\n\t\t\t\t\"1234567895\",\n\t\t\t\"french student: élève\",\n\t\t\t\"我喜欢鸡肉\",\n\t\t}\n\t\tfor _, validName := range validNames {\n\t\t\tif err := manager.ValidateName(validName); err != nil {\n\t\t\t\tt.Errorf(\"Name %s should be valid\", validName)\n\t\t\t}\n\t\t}\n\t})\n\n\tfor _, includeIncomplete := range []bool{true, false} {\n\t\tt.Run(fmt.Sprintf(\"List with includeIncomplete %t\", includeIncomplete), func(t *testing.T) {\n\t\t\tpaths, _ := populateFiles(t, true)\n\t\t\tmanager := newTestManager(paths)\n\t\t\tvar lastSnapshot Snapshot\n\t\t\tfor i := range []int{1, 2, 3} {\n\t\t\t\tsnapshotName := fmt.Sprintf(\"test-snapshot-%d\", i)\n\t\t\t\tsnapshot, err := manager.Create(context.Background(), snapshotName, \"\")\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"failed to create snapshot %q: %s\", snapshotName, err)\n\t\t\t\t}\n\t\t\t\tlastSnapshot = snapshot\n\t\t\t}\n\t\t\tlastSnapshotCompleteFilePath := filepath.Join(manager.SnapshotDirectory(lastSnapshot), completeFileName)\n\t\t\tif err := os.Remove(lastSnapshotCompleteFilePath); err != nil {\n\t\t\t\tt.Fatalf(\"failed to delete %q from snapshot %q: %s\", completeFileName, lastSnapshot.ID, err)\n\t\t\t}\n\t\t\tsnapshots, err := manager.List(includeIncomplete)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to list snapshots: %s\", err)\n\t\t\t}\n\n\t\t\texpectedLength := 0\n\t\t\tif includeIncomplete {\n\t\t\t\texpectedLength = 3\n\t\t\t} else {\n\t\t\t\texpectedLength = 2\n\t\t\t}\n\t\t\tif len(snapshots) != expectedLength {\n\t\t\t\tt.Errorf(\"unexpected length of snapshots slice %d (expected %d)\", len(snapshots), expectedLength)\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Delete\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot-delete\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tsnapshots, err := manager.List(false)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to list snapshots before delete: %s\", err)\n\t\t}\n\t\tif len(snapshots) != 1 {\n\t\t\tt.Fatalf(\"unexpected length of snapshots slice before delete %d\", len(snapshots))\n\t\t}\n\t\tif err := manager.Delete(snapshot.Name); err != nil {\n\t\t\tt.Fatalf(\"failed to delete snapshot: %s\", err)\n\t\t}\n\t\tsnapshots, err = manager.List(false)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to list snapshots after delete: %s\", err)\n\t\t}\n\t\tif len(snapshots) != 0 {\n\t\t\tt.Fatalf(\"unexpected length of snapshots slice after delete %d\", len(snapshots))\n\t\t}\n\t})\n\n\tt.Run(\"Restore should return an error if asked to restore a nonexistent snapshot\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tif err := manager.Restore(context.Background(), \"no-such-snapshot-id\"); err == nil {\n\t\t\tt.Errorf(\"Failed to complain when asked to restore a nonexistent snapshot\")\n\t\t}\n\t})\n\n\tt.Run(\"Restore should return the proper error if asked to restore from an incomplete snapshot\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot-restore-incomplete\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tcompleteFilePath := filepath.Join(manager.Snapshots, snapshot.ID, completeFileName)\n\t\tif err := os.Remove(completeFilePath); err != nil {\n\t\t\tt.Fatalf(\"failed to remove %q: %s\", completeFileName, err)\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshot.Name); err == nil {\n\t\t\tt.Errorf(\"Failed to complain when asked to restore an incomplete snapshot\")\n\t\t}\n\t})\n\n\tt.Run(\"Restore should return proper error and not run RestoreFiles when context is already cancelled\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tsnapshotName := \"test-snapshot-restore-cancelled\"\n\t\t_, err := manager.Create(context.Background(), snapshotName, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel()\n\t\tif err := manager.Restore(ctx, snapshotName); !errors.Is(err, runner.ErrContextDone) {\n\t\t\tt.Errorf(\"Error is of unexpected type: %q\", err)\n\t\t}\n\t})\n\n\tt.Run(\"Restore should return data reset error when RestoreFiles encounters an error and resets data\", func(t *testing.T) {\n\t\tpaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(paths)\n\t\tsnapshotName := \"test-snapshot-error\"\n\t\tsnapshot, err := manager.Create(context.Background(), snapshotName, \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tsnapshotSettingsPath := filepath.Join(paths.Snapshots, snapshot.ID, \"settings.json\")\n\t\tif err := os.RemoveAll(snapshotSettingsPath); err != nil {\n\t\t\tt.Fatalf(\"failed to remove settings.json: %s\", err)\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshotName); !errors.Is(err, ErrDataReset) {\n\t\t\tt.Errorf(\"Error is of unexpected type: %q\", err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/manager_unix_test.go",
    "content": "//go:build unix\n\npackage snapshot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lock\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\nfunc populateFiles(t *testing.T, includeOverrideYaml bool) (*paths.Paths, map[string]TestFile) {\n\tbaseDir := t.TempDir()\n\tappPaths := paths.Paths{\n\t\tAppHome:   baseDir,\n\t\tConfig:    filepath.Join(baseDir, \"config\"),\n\t\tLima:      filepath.Join(baseDir, \"lima\"),\n\t\tSnapshots: filepath.Join(baseDir, \"snapshots\"),\n\t}\n\ttestFiles := map[string]TestFile{\n\t\t\"settings.json\": {\n\t\t\tPath:     filepath.Join(appPaths.Config, \"settings.json\"),\n\t\t\tContents: `{\"test\": \"settings.json\"}`,\n\t\t},\n\t\t\"basedisk\": {\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"0\", \"basedisk\"),\n\t\t\tContents: \"basedisk contents\",\n\t\t},\n\t\t\"diffdisk\": {\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"0\", \"diffdisk\"),\n\t\t\tContents: \"diffdisk contents\",\n\t\t},\n\t\t\"user\": {\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"_config\", \"user\"),\n\t\t\tContents: \"user SSH key\",\n\t\t},\n\t\t\"user.pub\": {\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"_config\", \"user.pub\"),\n\t\t\tContents: \"user public SSH key\",\n\t\t},\n\t\t\"lima.yaml\": {\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"0\", \"lima.yaml\"),\n\t\t\tContents: \"this is yaml\",\n\t\t},\n\t}\n\tif includeOverrideYaml {\n\t\ttestFiles[\"override.yaml\"] = TestFile{\n\t\t\tPath:     filepath.Join(appPaths.Lima, \"_config\", \"override.yaml\"),\n\t\t\tContents: \"test: override.yaml\",\n\t\t}\n\t}\n\tfor _, file := range testFiles {\n\t\ttestDirectory := filepath.Dir(file.Path)\n\t\tif err := os.MkdirAll(testDirectory, 0o755); err != nil {\n\t\t\tt.Fatalf(\"failed to create dir %q: %s\", testDirectory, err)\n\t\t}\n\t\tif err := os.WriteFile(file.Path, []byte(file.Contents), 0o644); err != nil {\n\t\t\tt.Fatalf(\"failed to create test file %q: %s\", file.Path, err)\n\t\t}\n\t}\n\treturn &appPaths, testFiles\n}\n\nfunc newTestManager(appPaths *paths.Paths) *Manager {\n\tmanager := &Manager{\n\t\tPaths:         appPaths,\n\t\tSnapshotter:   NewSnapshotterImpl(),\n\t\tBackendLocker: &lock.MockBackendLock{},\n\t}\n\treturn manager\n}\n\nfunc TestManagerUnix(t *testing.T) {\n\tfor _, includeOverrideYaml := range []bool{true, false} {\n\t\tt.Run(fmt.Sprintf(\"Create with includeOverrideYaml %t\", includeOverrideYaml), func(t *testing.T) {\n\t\t\tappPaths, _ := populateFiles(t, includeOverrideYaml)\n\n\t\t\t// create snapshot\n\t\t\ttestManager := newTestManager(appPaths)\n\t\t\tsnapshot, err := testManager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"unexpected error creating snapshot: %s\", err)\n\t\t\t}\n\n\t\t\t// ensure desired files are present\n\t\t\tsnapshotDir := testManager.SnapshotDirectory(snapshot)\n\t\t\tsnapshotFiles := []string{\n\t\t\t\tfilepath.Join(snapshotDir, \"settings.json\"),\n\t\t\t\tfilepath.Join(snapshotDir, \"basedisk\"),\n\t\t\t\tfilepath.Join(snapshotDir, \"diffdisk\"),\n\t\t\t\tfilepath.Join(snapshotDir, \"metadata.json\"),\n\t\t\t}\n\t\t\tif includeOverrideYaml {\n\t\t\t\tsnapshotFiles = append(snapshotFiles, filepath.Join(snapshotDir, \"override.yaml\"))\n\t\t\t}\n\t\t\tfor _, file := range snapshotFiles {\n\t\t\t\tif _, err := os.ReadFile(file); err != nil {\n\t\t\t\t\tt.Errorf(\"file %q does not exist in snapshot: %s\", file, err)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tfor _, includeOverrideYaml := range []bool{true, false} {\n\t\tt.Run(fmt.Sprintf(\"Restore with includeOverrideYaml %t\", includeOverrideYaml), func(t *testing.T) {\n\t\t\tappPaths, testFiles := populateFiles(t, includeOverrideYaml)\n\t\t\tmanager := newTestManager(appPaths)\n\t\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t\t}\n\t\t\tfor testFileName, testFile := range testFiles {\n\t\t\t\tif err := os.WriteFile(testFile.Path, []byte(`{\"something\": \"different\"}`), 0o644); err != nil {\n\t\t\t\t\tt.Fatalf(\"failed to modify %s: %s\", testFileName, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err := manager.Restore(context.Background(), snapshot.Name); err != nil {\n\t\t\t\tt.Fatalf(\"failed to restore snapshot: %s\", err)\n\t\t\t}\n\t\t\tfor testFileName, testFile := range testFiles {\n\t\t\t\tcontents, err := os.ReadFile(testFile.Path)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Fatalf(\"failed to read contents of %s: %s\", testFileName, err)\n\t\t\t\t}\n\t\t\t\tif string(contents) != testFile.Contents {\n\t\t\t\t\tt.Errorf(\"contents of %s appear to have not been restored\", testFileName)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n\n\tt.Run(\"Restore should delete override.yaml if restoring to a snapshot without it\", func(t *testing.T) {\n\t\tappPaths, testFiles := populateFiles(t, true)\n\t\tmanager := newTestManager(appPaths)\n\t\tif err := os.Remove(testFiles[\"override.yaml\"].Path); err != nil {\n\t\t\tt.Fatalf(\"failed to delete override.yaml: %s\", err)\n\t\t}\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tfor testFileName, testFile := range testFiles {\n\t\t\tif err := os.WriteFile(testFile.Path, []byte(`{\"something\": \"different\"}`), 0o644); err != nil {\n\t\t\t\tt.Fatalf(\"failed to modify %s: %s\", testFileName, err)\n\t\t\t}\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshot.Name); err != nil {\n\t\t\tt.Fatalf(\"failed to restore snapshot: %s\", err)\n\t\t}\n\t\toverrideYamlPath := testFiles[\"override.yaml\"].Path\n\t\tdelete(testFiles, \"override.yaml\")\n\t\tfor testFileName, testFile := range testFiles {\n\t\t\tcontents, err := os.ReadFile(testFile.Path)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to read contents of %s: %s\", testFileName, err)\n\t\t\t}\n\t\t\tif string(contents) != testFile.Contents {\n\t\t\t\tt.Errorf(\"contents of %s appear to have not been restored\", testFileName)\n\t\t\t}\n\t\t}\n\t\tif _, err := os.Stat(overrideYamlPath); !errors.Is(err, os.ErrNotExist) {\n\t\t\tt.Errorf(\"override.yaml appears to not have been removed in restore\")\n\t\t}\n\t})\n\n\tt.Run(\"Restore should create any needed parent directories\", func(t *testing.T) {\n\t\tappPaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(appPaths)\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tfor _, dir := range []string{appPaths.Config, appPaths.Lima} {\n\t\t\tif err := os.RemoveAll(dir); err != nil {\n\t\t\t\tt.Fatalf(\"failed to remove directory: %s\", err)\n\t\t\t}\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshot.Name); err != nil {\n\t\t\tt.Fatalf(\"failed to restore snapshot: %s\", err)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/manager_windows_test.go",
    "content": "package snapshot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lock\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl\"\n)\n\nfunc populateFiles(t *testing.T, _ bool) (*paths.Paths, map[string]TestFile) {\n\tbaseDir := t.TempDir()\n\tappPaths := paths.Paths{\n\t\tConfig:        filepath.Join(baseDir, \"config\"),\n\t\tSnapshots:     filepath.Join(baseDir, \"snapshots\"),\n\t\tWslDistro:     filepath.Join(baseDir, \"wslDistro\"),\n\t\tWslDistroData: filepath.Join(baseDir, \"wslDistroData\"),\n\t}\n\ttestFiles := map[string]TestFile{\n\t\t\"settings.json\": {\n\t\t\tPath:     filepath.Join(appPaths.Config, \"settings.json\"),\n\t\t\tContents: `{\"test\": \"settings.json\"}`,\n\t\t},\n\t}\n\tfor _, file := range testFiles {\n\t\ttestDirectory := filepath.Dir(file.Path)\n\t\tif err := os.MkdirAll(testDirectory, 0o755); err != nil {\n\t\t\tt.Fatalf(\"failed to create dir %q: %s\", testDirectory, err)\n\t\t}\n\t\tif err := os.WriteFile(file.Path, []byte(file.Contents), 0o644); err != nil {\n\t\t\tt.Fatalf(\"failed to create test file %q: %s\", file.Path, err)\n\t\t}\n\t}\n\treturn &appPaths, testFiles\n}\n\nfunc newTestManager(appPaths *paths.Paths) *Manager {\n\tsnapshotter := NewSnapshotterImpl()\n\tsnapshotter.WSL = wsl.MockWSL{}\n\tmanager := &Manager{\n\t\tPaths:         appPaths,\n\t\tSnapshotter:   snapshotter,\n\t\tBackendLocker: &lock.MockBackendLock{},\n\t}\n\treturn manager\n}\n\nfunc TestManagerWindows(t *testing.T) {\n\tt.Run(\"Create should create the necessary files\", func(t *testing.T) {\n\t\tappPaths, _ := populateFiles(t, false)\n\n\t\t// create snapshot\n\t\ttestManager := newTestManager(appPaths)\n\t\tsnapshot, err := testManager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"unexpected error creating snapshot: %s\", err)\n\t\t}\n\n\t\t// ensure desired files are present\n\t\tsnapshotFiles := []string{\n\t\t\tfilepath.Join(appPaths.Snapshots, snapshot.ID, \"settings.json\"),\n\t\t\tfilepath.Join(appPaths.Snapshots, snapshot.ID, \"metadata.json\"),\n\t\t}\n\t\tfor _, file := range snapshotFiles {\n\t\t\tif _, err := os.ReadFile(file); err != nil {\n\t\t\t\tt.Errorf(\"file %q does not exist in snapshot: %s\", file, err)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"Restore should work properly\", func(t *testing.T) {\n\t\tappPaths, testFiles := populateFiles(t, false)\n\t\tmanager := newTestManager(appPaths)\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\tfor testFileName, testFile := range testFiles {\n\t\t\tif err := os.WriteFile(testFile.Path, []byte(`{\"something\": \"different\"}`), 0o644); err != nil {\n\t\t\t\tt.Fatalf(\"failed to modify %s: %s\", testFileName, err)\n\t\t\t}\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshot.Name); err != nil {\n\t\t\tt.Fatalf(\"failed to restore snapshot: %s\", err)\n\t\t}\n\t\tfor testFileName, testFile := range testFiles {\n\t\t\tcontents, err := os.ReadFile(testFile.Path)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"failed to read contents of %s: %s\", testFileName, err)\n\t\t\t}\n\t\t\tif string(contents) != testFile.Contents {\n\t\t\t\tt.Errorf(\"contents of %s appear to have not been restored\", testFileName)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"Restore should create any needed parent directories\", func(t *testing.T) {\n\t\tappPaths, _ := populateFiles(t, true)\n\t\tmanager := newTestManager(appPaths)\n\t\tsnapshot, err := manager.Create(context.Background(), \"test-snapshot\", \"\")\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"failed to create snapshot: %s\", err)\n\t\t}\n\t\ttestDirs := []string{\n\t\t\tappPaths.Config,\n\t\t\tappPaths.WslDistro,\n\t\t\tappPaths.WslDistroData,\n\t\t}\n\t\tfor _, testDir := range testDirs {\n\t\t\tif err := os.RemoveAll(testDir); err != nil {\n\t\t\t\tt.Fatalf(\"failed to remove test directory %q: %s\", testDir, err)\n\t\t\t}\n\t\t}\n\t\tif err := manager.Restore(context.Background(), snapshot.Name); err != nil {\n\t\t\tt.Fatalf(\"failed to restore snapshot: %s\", err)\n\t\t}\n\t\tfor _, testDir := range testDirs {\n\t\t\tif _, err := os.Stat(testDir); errors.Is(err, os.ErrNotExist) {\n\t\t\t\tt.Errorf(\"directory %q was not created\", testDir)\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/snapshot.go",
    "content": "package snapshot\n\nimport (\n\t\"encoding/json\"\n\t\"time\"\n)\n\ntype Snapshot struct {\n\tCreated     time.Time `json:\"created\"`\n\tName        string    `json:\"name\"`\n\tID          string    `json:\"id,omitempty\"`\n\tDescription string    `json:\"description\"`\n}\n\nfunc (s *Snapshot) getTimeString() string {\n\treturn s.Created.Format(time.RFC3339)\n}\n\nfunc (s *Snapshot) MarshalJSON() ([]byte, error) {\n\ttype Alias Snapshot\n\treturn json.Marshal(&struct {\n\t\t*Alias\n\t\tCreated string `json:\"created\"`\n\t}{\n\t\tAlias:   (*Alias)(s),\n\t\tCreated: s.getTimeString(),\n\t})\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/snapshotter.go",
    "content": "package snapshot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n)\n\n// Types that implement Snapshotter are responsible for copying/creating\n// files that need to be copied/created for the creation and restoration of\n// snapshots.\ntype Snapshotter interface {\n\t// Does all of the things that can fail when creating a snapshot,\n\t// so that the snapshot creation can easily be rolled back upon\n\t// a failure.\n\tCreateFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error\n\t// Like CreateFiles, but for restoring: does all of the things\n\t// that can fail when restoring a snapshot so that restoration can\n\t// easily be rolled back in the event of a failure. Returns ErrDataReset\n\t// when data has been reset due to an error in this process.\n\tRestoreFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error\n}\n\n// Returned by Snapshotter.RestoreFiles when data has been reset\n// due to an error restoring the files.\nvar ErrDataReset = errors.New(\"data reset\")\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/snapshotter_unix.go",
    "content": "//go:build unix\n\npackage snapshot\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n)\n\n// Represents a file that is included in a snapshot.\ntype snapshotFile struct {\n\t// The path that Rancher Desktop uses.\n\tWorkingPath string\n\t// The path that the file is put at in a snapshot.\n\tSnapshotPath string\n\t// Whether clonefile (macOS) or ioctl_ficlone (Linux) should be used\n\t// when copying the file around.\n\tCopyOnWrite bool\n\t// Whether it is ok for the file to not be present.\n\tMissingOk bool\n\t// The permissions the file should have.\n\tFileMode os.FileMode\n}\n\n// SnapshotterImpl also works as a *Manager receiver\ntype SnapshotterImpl struct {\n}\n\nfunc NewSnapshotterImpl() Snapshotter {\n\treturn SnapshotterImpl{}\n}\n\nfunc (snapshotter SnapshotterImpl) Files(appPaths *paths.Paths, snapshotDir string) []snapshotFile {\n\tfiles := []snapshotFile{\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Config, \"settings.json\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"settings.json\"),\n\t\t\tCopyOnWrite:  false,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"_config\", \"override.yaml\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"override.yaml\"),\n\t\t\tCopyOnWrite:  false,\n\t\t\tMissingOk:    true,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"0\", \"basedisk\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"basedisk\"),\n\t\t\tCopyOnWrite:  true,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"0\", \"diffdisk\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"diffdisk\"),\n\t\t\tCopyOnWrite:  true,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"_config\", \"user\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"user\"),\n\t\t\tCopyOnWrite:  false,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o600,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"_config\", \"user.pub\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"user.pub\"),\n\t\t\tCopyOnWrite:  false,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t\t{\n\t\t\tWorkingPath:  filepath.Join(appPaths.Lima, \"0\", \"lima.yaml\"),\n\t\t\tSnapshotPath: filepath.Join(snapshotDir, \"lima.yaml\"),\n\t\t\tCopyOnWrite:  false,\n\t\t\tMissingOk:    false,\n\t\t\tFileMode:     0o644,\n\t\t},\n\t}\n\treturn files\n}\n\nfunc (snapshotter SnapshotterImpl) CreateFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error {\n\ttaskRunner := runner.NewTaskRunner(ctx)\n\tfiles := snapshotter.Files(appPaths, snapshotDir)\n\tfor _, file := range files {\n\t\ttaskRunner.Add(func() error {\n\t\t\terr := copyFile(file.SnapshotPath, file.WorkingPath, file.CopyOnWrite, file.FileMode)\n\t\t\tif errors.Is(err, os.ErrNotExist) && file.MissingOk {\n\t\t\t\treturn nil\n\t\t\t} else if err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to copy %s: %w\", filepath.Base(file.WorkingPath), err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// Create complete.txt file. This is done last because its presence\n\t// signifies a complete and valid snapshot.\n\ttaskRunner.Add(func() error {\n\t\tcompleteFilePath := filepath.Join(snapshotDir, completeFileName)\n\t\tif err := os.WriteFile(completeFilePath, []byte(completeFileContents), 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write %q: %w\", completeFileName, err)\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn taskRunner.Wait()\n}\n\n// Restores the files from their location in a snapshot directory\n// to their working location.\nfunc (snapshotter SnapshotterImpl) RestoreFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error {\n\ttaskRunner := runner.NewTaskRunner(ctx)\n\tfiles := snapshotter.Files(appPaths, snapshotDir)\n\tfor _, file := range files {\n\t\ttaskRunner.Add(func() error {\n\t\t\tfilename := filepath.Base(file.WorkingPath)\n\t\t\terr := copyFile(file.WorkingPath, file.SnapshotPath, file.CopyOnWrite, file.FileMode)\n\t\t\tif errors.Is(err, os.ErrNotExist) && file.MissingOk {\n\t\t\t\tif err := os.RemoveAll(file.WorkingPath); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to remove %q: %w\", filename, err)\n\t\t\t\t}\n\t\t\t} else if err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to restore %q: %w\", filename, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\tif err := taskRunner.Wait(); err != nil {\n\t\tfor _, file := range files {\n\t\t\t_ = os.Remove(file.WorkingPath)\n\t\t}\n\t\t_ = os.RemoveAll(appPaths.Lima)\n\t\treturn fmt.Errorf(\"%w: %w\", ErrDataReset, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/snapshot/snapshotter_windows.go",
    "content": "package snapshot\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/paths\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/runner\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/wsl\"\n)\n\ntype wslDistro struct {\n\t// The name of the WSL distro.\n\tName string\n\t// The path to the directory that is used to store the\n\t// copy of the distro that is actually used by WSL.\n\tWorkingDirPath string\n}\n\n// SnapshotterImpl also works as a *Manager receiver\ntype SnapshotterImpl struct {\n\twsl.WSL\n}\n\nfunc (snapshotter SnapshotterImpl) WSLDistros(appPaths *paths.Paths) []wslDistro {\n\treturn []wslDistro{\n\t\t{\n\t\t\tName:           \"rancher-desktop\",\n\t\t\tWorkingDirPath: appPaths.WslDistro,\n\t\t},\n\t\t{\n\t\t\tName:           \"rancher-desktop-data\",\n\t\t\tWorkingDirPath: appPaths.WslDistroData,\n\t\t},\n\t}\n}\n\n// Note: on Windows, there are system calls such as CopyFile and CopyFileEx\n// that may speed up the process of copying a file, but they appear to require\n// loading DLL's. This approach works fine for copying smaller files, but if\n// we need to copy big files it may be worth the complexity to use the syscall.\nfunc copyFile(dst, src string) error {\n\tsrcFd, err := os.Open(src)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open source file: %w\", err)\n\t}\n\tdefer srcFd.Close()\n\tif err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to create destination parent dir: %w\", err)\n\t}\n\tdstFd, err := os.Create(dst)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open destination file: %w\", err)\n\t}\n\tdefer dstFd.Close()\n\tif _, err := io.Copy(dstFd, srcFd); err != nil {\n\t\treturn fmt.Errorf(\"failed to copy contents of src to dst: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc NewSnapshotterImpl() SnapshotterImpl {\n\treturn SnapshotterImpl{\n\t\tWSL: wsl.WSLImpl{},\n\t}\n}\n\nfunc (snapshotter SnapshotterImpl) CreateFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error {\n\ttaskRunner := runner.NewTaskRunner(ctx)\n\n\t// export WSL distros to snapshot directory\n\tfor _, distro := range snapshotter.WSLDistros(appPaths) {\n\t\ttaskRunner.Add(func() error {\n\t\t\tsnapshotDistroPath := filepath.Join(snapshotDir, distro.Name+\".tar\")\n\t\t\tif err := snapshotter.ExportDistro(ctx, distro.Name, snapshotDistroPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to export WSL distro %q: %w\", distro.Name, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// copy settings.json to snapshot directory\n\ttaskRunner.Add(func() error {\n\t\tworkingSettingsPath := filepath.Join(appPaths.Config, \"settings.json\")\n\t\tsnapshotSettingsPath := filepath.Join(snapshotDir, \"settings.json\")\n\t\tif err := copyFile(snapshotSettingsPath, workingSettingsPath); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to copy %q to snapshot directory: %w\", workingSettingsPath, err)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// Create complete.txt file. This is done last because its presence\n\t// signifies a complete and valid snapshot.\n\ttaskRunner.Add(func() error {\n\t\tcompleteFilePath := filepath.Join(snapshotDir, completeFileName)\n\t\tif err := os.WriteFile(completeFilePath, []byte(completeFileContents), 0o644); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write %q: %w\", completeFileName, err)\n\t\t}\n\t\treturn nil\n\t})\n\n\treturn taskRunner.Wait()\n}\n\nfunc (snapshotter SnapshotterImpl) RestoreFiles(ctx context.Context, appPaths *paths.Paths, snapshotDir string) error {\n\ttr := runner.NewTaskRunner(ctx)\n\n\t// unregister WSL distros\n\ttr.Add(func() error {\n\t\tif err := snapshotter.UnregisterDistros(ctx); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to unregister WSL distros: %w\", err)\n\t\t}\n\t\treturn nil\n\t})\n\n\t// restore WSL distros\n\tfor _, distro := range snapshotter.WSLDistros(appPaths) {\n\t\ttr.Add(func() error {\n\t\t\tsnapshotDistroPath := filepath.Join(snapshotDir, distro.Name+\".tar\")\n\t\t\tif err := os.MkdirAll(distro.WorkingDirPath, 0o755); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to create install directory for distro %q: %w\", distro.Name, err)\n\t\t\t}\n\t\t\tif err := snapshotter.ImportDistro(ctx, distro.Name, distro.WorkingDirPath, snapshotDistroPath); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to import WSL distro %q: %w\", distro.Name, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\n\t// copy settings.json back to its working location\n\tworkingSettingsPath := filepath.Join(appPaths.Config, \"settings.json\")\n\tsnapshotSettingsPath := filepath.Join(snapshotDir, \"settings.json\")\n\ttr.Add(func() error {\n\t\tif err := copyFile(workingSettingsPath, snapshotSettingsPath); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to restore %q: %w\", workingSettingsPath, err)\n\t\t}\n\t\treturn nil\n\t})\n\tif err := tr.Wait(); err != nil {\n\t\t_ = os.Remove(workingSettingsPath)\n\t\t_ = snapshotter.UnregisterDistros(ctx)\n\t\treturn fmt.Errorf(\"%w: %w\", ErrDataReset, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// Get the steps-th parent directory of fullPath.\nfunc GetParentDir(fullPath string, steps int) string {\n\tfullPath = filepath.Clean(fullPath)\n\tfor ; steps > 0; steps-- {\n\t\tfullPath = filepath.Dir(fullPath)\n\t}\n\treturn fullPath\n}\n\ntype mapKeyWithString struct {\n\tMapKey       reflect.Value\n\tStringKey    string\n\tlowerCaseKey string // only for sorting\n}\n\nfunc SortKeys(mapKeys []reflect.Value) []mapKeyWithString {\n\tretVals := make([]mapKeyWithString, len(mapKeys))\n\tfor idx, key := range mapKeys {\n\t\tmapKeyAsString := key.String()\n\t\tretVals[idx] = mapKeyWithString{key, mapKeyAsString, strings.ToLower(mapKeyAsString)}\n\t}\n\tsort.Slice(retVals, func(i, j int) bool {\n\t\treturn retVals[i].lowerCaseKey < retVals[j].lowerCaseKey\n\t})\n\treturn retVals\n}\n\ntype structFieldWithString struct {\n\tStructField  reflect.StructField\n\tFieldName    string\n\tlowerCaseKey string // only for sorting\n}\n\nfunc SortStructFields(structType reflect.Type) []structFieldWithString {\n\tnumTypedFields := structType.NumField()\n\tnewInterimFields := make([]structFieldWithString, numTypedFields)\n\tfor i := range numTypedFields {\n\t\tfieldTag := structType.Field(i).Tag.Get(\"json\")\n\t\tfieldName, _, _ := strings.Cut(fieldTag, \",\")\n\t\tnewInterimFields[i] = structFieldWithString{structType.Field(i), fieldName, strings.ToLower(fieldName)}\n\t}\n\tsort.Slice(newInterimFields, func(i, j int) bool {\n\t\treturn newInterimFields[i].lowerCaseKey < newInterimFields[j].lowerCaseKey\n\t})\n\treturn newInterimFields\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/version/version.go",
    "content": "package version\n\nvar Version = \"0.0.0\"\n"
  },
  {
    "path": "src/go/rdctl/pkg/wsl/doc.go",
    "content": "// Package wsl defines an interface, and implements types, that wrap\n// the WSL command line. As of the time of writing, the main purpose\n// of this type is to ease testing.\npackage wsl\n"
  },
  {
    "path": "src/go/rdctl/pkg/wsl/mock_windows.go",
    "content": "package wsl\n\nimport \"context\"\n\ntype MockWSL struct{}\n\nfunc (wsl MockWSL) UnregisterDistros(ctx context.Context) error {\n\treturn nil\n}\n\nfunc (wsl MockWSL) ExportDistro(ctx context.Context, distroName, fileName string) error {\n\treturn nil\n}\n\nfunc (wsl MockWSL) ImportDistro(ctx context.Context, distroName, installLocation, fileName string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/rdctl/pkg/wsl/names.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage wsl\n\nconst (\n\t// The name of the WSL distribution (when not using lima).\n\tDistributionName = \"rancher-desktop\"\n\t// The name of the WSL data distribution (when not using lima).\n\tDataDistributionName = DistributionName + \"-data\"\n)\n"
  },
  {
    "path": "src/go/rdctl/pkg/wsl/wsl_windows.go",
    "content": "package wsl\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/lima\"\n)\n\ntype WSL interface {\n\t// Deletes all WSL distros pertaining to Rancher Desktop.\n\tUnregisterDistros(ctx context.Context) error\n\t// Exports a distro as a .vhdx file and stores the result at\n\t// the path given in fileName.\n\tExportDistro(ctx context.Context, distroName, fileName string) error\n\t// Imports a distro from a .vhdx file stored at path fileName\n\t// and names it distroName. Installs the distro in the directory\n\t// given by installLocation.\n\tImportDistro(ctx context.Context, distroName, installLocation, fileName string) error\n}\n\ntype WSLImpl struct{}\n\nfunc (wsl WSLImpl) UnregisterDistros(ctx context.Context) error {\n\tcmd := exec.CommandContext(ctx, \"wsl\", \"--list\", \"--quiet\")\n\t// Force WSL to output UTF-8 so it's easier to process. (os.Environ returns a\n\t// copy, so appending to it is safe.)\n\tcmd.Env = append(os.Environ(), \"WSL_UTF8=1\")\n\tcmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: windows.CREATE_NO_WINDOW}\n\tcmd.Stderr = os.Stderr\n\trawBytes, err := cmd.Output()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error getting current WSL distributions: %w\", err)\n\t}\n\tdistrosToKill := []string{}\n\tfor _, s := range strings.Fields(string(rawBytes)) {\n\t\tif slices.Contains([]string{DistributionName, DataDistributionName, lima.InstanceFullName}, s) {\n\t\t\tdistrosToKill = append(distrosToKill, s)\n\t\t}\n\t}\n\n\tfor _, distro := range distrosToKill {\n\t\tcmd := exec.CommandContext(ctx, \"wsl\", \"--unregister\", distro)\n\t\tcmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: windows.CREATE_NO_WINDOW}\n\t\tcmd.Stdout = os.Stdout\n\t\tcmd.Stderr = os.Stderr\n\t\tif err := cmd.Run(); err != nil {\n\t\t\tlogrus.Errorf(\"Error unregistering WSL distribution %s: %s\\n\", distro, err)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (wsl WSLImpl) ExportDistro(ctx context.Context, distroName, fileName string) error {\n\tcmd := exec.CommandContext(ctx, \"wsl.exe\", \"--export\", distroName, fileName)\n\t// Prevents \"signals\" (think ctrl+C) from affecting called subprocess\n\tcmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: windows.CREATE_NO_WINDOW}\n\tif output, err := cmd.Output(); err != nil {\n\t\treturn fmt.Errorf(\"failed to export WSL distro %q: %w\", distroName, wrapWSLError(output, err))\n\t}\n\treturn nil\n}\n\nfunc (wsl WSLImpl) ImportDistro(ctx context.Context, distroName, installLocation, fileName string) error {\n\tcmd := exec.CommandContext(ctx, \"wsl.exe\", \"--import\", distroName, installLocation, fileName, \"--version\", \"2\")\n\t// Prevents \"signals\" (think ctrl+C) from affecting called subprocess\n\tcmd.SysProcAttr = &windows.SysProcAttr{CreationFlags: windows.CREATE_NO_WINDOW}\n\tif output, err := cmd.Output(); err != nil {\n\t\treturn fmt.Errorf(\"failed to import WSL distro %q: %w\", distroName, wrapWSLError(output, err))\n\t}\n\treturn nil\n}\n\n// wrapWSLError is used to make errors returned from\n// *exec.Cmd.Output() more helpful. It combines the string from the\n// returned error, any data written to stdout, and any data written\n// to stderr into the string of one error.\nfunc wrapWSLError(output []byte, err error) error {\n\tif exitErr, ok := err.(*exec.ExitError); ok {\n\t\treturn fmt.Errorf(\"%w stdout: %q stderr: %q\", err, string(output), exitErr.Stderr)\n\t}\n\treturn fmt.Errorf(\"%w: stdout: %q\", err, string(output))\n}\n"
  },
  {
    "path": "src/go/spin-stub/README.md",
    "content": "# spin-stub\n\nThis is a stub executable used to launch spin on Windows.\n\n## Usage\n\nUse it as you would with a normal `spin` command. It simply configures the `SPIN_DATA_DIR` environment variable to point to the spin subdirectory within the Rancher Desktop application data directory, and then runs `../internal/spin.exe` (relative to the location of the `spin-stub` binary).\n"
  },
  {
    "path": "src/go/spin-stub/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/spin-stub\n\ngo 1.25.0\n"
  },
  {
    "path": "src/go/spin-stub/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n)\n\nconst appName = \"rancher-desktop\"\n\nfunc main() {\n\thomeDir, err := os.UserHomeDir()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to retrieve the user's home directory: %w\", err))\n\t}\n\tlocalAppData := os.Getenv(\"LOCALAPPDATA\")\n\tif localAppData == \"\" {\n\t\tlocalAppData = filepath.Join(homeDir, \"AppData\", \"Local\")\n\t}\n\terr = os.Setenv(\"SPIN_DATA_DIR\", filepath.Join(localAppData, appName, \"spin\"))\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to set SPIN_DATA_DIR: %w\", err))\n\t}\n\texe, err := os.Executable()\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to get executable path: %w\", err))\n\t}\n\tspin := filepath.Join(filepath.Dir(filepath.Dir(exe)), \"internal\", \"spin.exe\")\n\tcommand := exec.CommandContext(context.Background(), spin, os.Args[1:]...)\n\tcommand.Stdin = os.Stdin\n\tcommand.Stdout = os.Stdout\n\tcommand.Stderr = os.Stderr\n\terr = command.Run()\n\tvar exitError *exec.ExitError\n\tif errors.As(err, &exitError) {\n\t\tos.Exit(exitError.ExitCode())\n\t}\n\tif err != nil {\n\t\tpanic(fmt.Errorf(\"failed to execute command: %w\", err))\n\t}\n}\n"
  },
  {
    "path": "src/go/startup-profile/.gitignore",
    "content": "*.cpuprofile\n*.json\n"
  },
  {
    "path": "src/go/startup-profile/README.md",
    "content": "# startup-profile\n\nThis is a tool to profile Rancher Desktop startup to try to guide optimizing\nstartup times.  It scrapes various logs to generate data that can be loaded into\nChrome developer tools.\n\n## Usage\n\n1. Start Rancher Desktop using `yarn dev`.\n2. Wait until startup is complete; keep it running.\n3. Run this tool (via `go run .`) to generate a `.cpuprofile` file.\n4. Open Chrome (or other Chromium-derived browser) developer tools, and go to\n   the _Performance_ tab.\n5. Click on the _Load Profile_ button (looks something like `↥`) and load the\n   generated file.\n6. Alternatively, load the same file using https://profiler.firefox.com/\n"
  },
  {
    "path": "src/go/startup-profile/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile\n\ngo 1.25.0\n\nrequire golang.org/x/sync v0.20.0\n"
  },
  {
    "path": "src/go/startup-profile/go.sum",
    "content": "golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\n"
  },
  {
    "path": "src/go/startup-profile/main.go",
    "content": "// Command startup-profile generates a Chrome devtools profile format.\n// The output can be loaded via https://profiler.firefox.com/ or via Chrome\n// devtools (Performance tab).\npackage main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// marshalledPath represents a path.  It implements [encoding.TextMarshaler] and\n// [encoding.TextUnmarshaler].\ntype marshalledPath string\n\nfunc (p *marshalledPath) MarshalText() ([]byte, error) {\n\treturn []byte(*p), nil\n}\n\nfunc (p *marshalledPath) UnmarshalText(text []byte) error {\n\tabs, err := filepath.Abs(string(text))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif info, err := os.Stat(filepath.Dir(abs)); err != nil {\n\t\treturn err\n\t} else if !info.IsDir() {\n\t\treturn fmt.Errorf(\"parent %s is not a directory\", filepath.Dir(abs))\n\t}\n\t*p = marshalledPath(abs)\n\treturn nil\n}\n\nfunc main() {\n\toutPath := marshalledPath(\"rancher-desktop.cpuprofile\")\n\tflag.TextVar(&outPath, \"out\", &outPath, \"File name to write the output to\")\n\tflag.Parse()\n\n\tif err := run(context.Background(), outPath); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "src/go/startup-profile/model/event.go",
    "content": "package model\n\nimport \"time\"\n\ntype EventPhase string\n\nconst (\n\tEventPhaseBegin   = EventPhase(\"B\")\n\tEventPhaseEnd     = EventPhase(\"E\")\n\tEventPhaseInstant = EventPhase(\"i\")\n)\n\n// Event describes something happening.  It is in Google's Trace Event Format,\n// as described in https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU/preview?tab=t.0#heading=h.uxpopqvbjezh\ntype Event struct {\n\tName     string     `json:\"name\"`\n\tCategory string     `json:\"cat\"`\n\tPhase    EventPhase `json:\"ph\"`\n\t// Each event must have a timestamp; the `time` field is generated when rendering.\n\tTimeStamp time.Time      `json:\"-time-stamp\"`\n\tPID       int            `json:\"pid\"`\n\tTID       int            `json:\"tid\"`\n\tArgs      map[string]any `json:\"args\"`\n\n\t// Event time in microseconds since start of trace; generated when rendering.\n\tTime int64 `json:\"ts\"`\n\t// Duration of \"begin\" events; generated when rendering.\n\tDuration time.Duration `json:\"-duration\"`\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/const.go",
    "content": "package parsers\n\nconst osWindows = \"windows\"\n"
  },
  {
    "path": "src/go/startup-profile/parsers/dmesg.go",
    "content": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/rdctl\"\n)\n\n// Parse `dmesg` output\nfunc ParseDmesg(ctx context.Context) ([]*model.Event, error) {\n\tif runtime.GOOS == osWindows {\n\t\t// dmesg isn't useful on Windows, because we use WSL2 there so messages may\n\t\t// be related to a different distribution.\n\t\treturn nil, nil\n\t}\n\t_, err := rdctl.Rdctl(ctx, \"shell\", \"sudo\",\n\t\t\"sh\", \"-c\", \"date -u +'@@STOP@@ %FT%TZ' >> /dev/kmsg\") // spellcheck-ignore-line\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to mark timestamp in dmesg: %w\", err)\n\t}\n\tstdout, err := rdctl.Rdctl(ctx, \"shell\", \"sudo\", \"dmesg\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get dmesg: %w\", err)\n\t}\n\tlineMatcher := regexp.MustCompile(`^\\[\\s*(\\d+\\.\\d+)\\] (.*)$`)\n\tignoreMatcher := regexp.MustCompile(`^(?:audit:|cni0:|veth[0-9a-f]+:|kauditd_printk_skb)`)\n\tvar timeOffset time.Time\n\tscanner := bufio.NewScanner(stdout)\n\ttype entry struct {\n\t\toffset  int64\n\t\tmessage string\n\t}\n\tvar entries []entry\n\tfor scanner.Scan() {\n\t\tlineMatch := lineMatcher.FindStringSubmatch(scanner.Text())\n\t\tif len(lineMatch) != 3 {\n\t\t\tcontinue\n\t\t}\n\t\tif strings.HasPrefix(lineMatch[2], \"@@STOP@@ \") {\n\t\t\toffset, err := strconv.ParseFloat(lineMatch[1], 64)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing time offset: %q: %w\", scanner.Text(), err)\n\t\t\t}\n\t\t\ttimeOffset, err = time.Parse(time.RFC3339, lineMatch[2][len(\"@@STOP@@ \"):])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"error parsing current time: %w\", err)\n\t\t\t}\n\t\t\ttimeOffset = timeOffset.Add(-time.Duration(int64(offset * float64(time.Second))))\n\t\t\tbreak\n\t\t}\n\t\tif ignoreMatcher.MatchString(lineMatch[2]) {\n\t\t\t// Skip uninteresting line.\n\t\t\tcontinue\n\t\t}\n\t\toffset, err := strconv.ParseFloat(lineMatch[1], 64)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time offset: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tentries = append(entries, entry{offset: int64(offset * float64(time.Second)), message: lineMatch[2]})\n\t}\n\n\tif timeOffset.IsZero() {\n\t\treturn nil, fmt.Errorf(\"failed to find time offset\")\n\t}\n\tresults := make([]*model.Event, 0, len(entries))\n\tfor _, entry := range entries {\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      entry.message,\n\t\t\tCategory:  \"dmesg\",\n\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\tTimeStamp: timeOffset.Add(time.Duration(entry.offset)),\n\t\t})\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/interface.go",
    "content": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/rdctl\"\n)\n\n// A Parser is a function that processes some kind of log file and emits events\n// into the provided channel.\ntype Parser func(context.Context) ([]*model.Event, error)\n\n// Given the name of a log file, return a scanner that reads from the given file\n// in the Rancher Desktop logs directory.\nfunc readRDLogFile(ctx context.Context, name string) (*bufio.Scanner, error) {\n\tvar paths struct {\n\t\tLogs string `json:\"logs\"`\n\t}\n\tstdout, err := rdctl.Rdctl(ctx, \"paths\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get paths: %w\", err)\n\t}\n\tif err := json.Unmarshal(stdout.Bytes(), &paths); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal paths: %w\", err)\n\t}\n\tlogPath := filepath.Join(paths.Logs, name)\n\tlogFile, err := os.Open(logPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open log file %s: %w\", logPath, err)\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = logFile.Close()\n\t}()\n\n\treturn bufio.NewScanner(logFile), nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/lima-ha.go",
    "content": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/rdctl\"\n)\n\nfunc ParseLimaHostAgentLogs(ctx context.Context) ([]*model.Event, error) {\n\tvar paths struct {\n\t\tLima string `json:\"lima\"`\n\t}\n\tstdout, err := rdctl.Rdctl(ctx, \"paths\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get paths: %w\", err)\n\t}\n\tif err := json.Unmarshal(stdout.Bytes(), &paths); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal paths: %w\", err)\n\t}\n\tif paths.Lima == \"\" {\n\t\t// On Windows, we don't use Lima, so this would not deserialize.\n\t\treturn nil, nil\n\t}\n\tlogPath := filepath.Join(paths.Lima, \"0\", \"ha.stderr.log\")\n\tlogFile, err := os.Open(logPath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open log file %s: %w\", logPath, err)\n\t}\n\tdefer logFile.Close()\n\tvar results []*model.Event\n\tscanner := bufio.NewScanner(logFile)\n\tfor scanner.Scan() {\n\t\tvar data struct {\n\t\t\tLevel   string    `json:\"level\"`\n\t\t\tMessage string    `json:\"msg\"`\n\t\t\tTime    time.Time `json:\"time\"`\n\t\t}\n\t\tif err := json.Unmarshal(scanner.Bytes(), &data); err != nil {\n\t\t\t// Ignore any lines that are not JSON\n\t\t\tcontinue\n\t\t}\n\t\tif !data.Time.IsZero() {\n\t\t\tresults = append(results, &model.Event{\n\t\t\t\tName:      data.Message,\n\t\t\t\tCategory:  \"lima.ha.stderr.log\",\n\t\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\t\tTimeStamp: data.Time,\n\t\t\t})\n\t\t}\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/lima-init.go",
    "content": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/rdctl\"\n)\n\n// ParseLimaInitLogs parses /var/log/lima-init.log in a Lima VM.\nfunc ParseLimaInitLogs(ctx context.Context) ([]*model.Event, error) {\n\t// Run `rdctl shell` to print the lima-init logs; if the file does not exist,\n\t// still return success (this will be the case on Windows).\n\tstdout, err := rdctl.Rdctl(ctx, \"shell\", \"sudo\",\n\t\t\"sh\", \"-c\", \"cat /var/log/lima-init.log || true\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get lima-init logs: %w\", err)\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^LIMA ([^|]+)\\|\\s*(.*)$`)\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) < 3 {\n\t\t\tcontinue\n\t\t}\n\t\tdate, err := time.Parse(time.RFC3339, matches[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      matches[2],\n\t\t\tCategory:  \"lima-init\",\n\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\tTimeStamp: date,\n\t\t})\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/networking.go",
    "content": "package parsers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\nfunc ParseNetworkingLogs(ctx context.Context) ([]*model.Event, error) {\n\tscanner, err := readRDLogFile(ctx, \"networking.log\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^(\\d+-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z): (.*)$`)\n\tbeginMatcher := regexp.MustCompile(`^getting certificates from (.*?)\\.{3}$`)\n\tendMatcher := regexp.MustCompile(`^got certificates from (.*?)$`)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) != matcher.NumSubexp()+1 {\n\t\t\tcontinue\n\t\t}\n\t\tparsedTime, err := time.Parse(time.RFC3339, matches[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tif m := beginMatcher.FindStringSubmatch(matches[2]); len(m) == beginMatcher.NumSubexp()+1 {\n\t\t\tresults = append(results, &model.Event{\n\t\t\t\tName:      m[1],\n\t\t\t\tCategory:  \"networking.log\",\n\t\t\t\tPhase:     model.EventPhaseBegin,\n\t\t\t\tTimeStamp: parsedTime,\n\t\t\t})\n\t\t} else if m := endMatcher.FindStringSubmatch(matches[2]); len(m) == endMatcher.NumSubexp()+1 {\n\t\t\tresults = append(results, &model.Event{\n\t\t\t\tName:      m[1],\n\t\t\t\tCategory:  \"networking.log\",\n\t\t\t\tPhase:     model.EventPhaseEnd,\n\t\t\t\tTimeStamp: parsedTime,\n\t\t\t})\n\t\t} else {\n\t\t\tresults = append(results, &model.Event{\n\t\t\t\tName:      matches[2],\n\t\t\t\tCategory:  \"networking.log\",\n\t\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\t\tTimeStamp: parsedTime,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/progress.go",
    "content": "package parsers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\n// ParseProgress parses the Rancher Desktop backend logs for progress tracker\n// events.\nfunc ParseProgress(ctx context.Context) ([]*model.Event, error) {\n\tlogName := \"lima.log\"\n\tif runtime.GOOS == osWindows {\n\t\tlogName = \"wsl.log\"\n\t}\n\n\tscanner, err := readRDLogFile(ctx, logName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^(\\d+.*?Z): Progress: (started|finished) (.*)$`)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) < matcher.NumSubexp()+1 {\n\t\t\tcontinue\n\t\t}\n\t\tdate := matches[1]\n\t\tstate := matches[2]\n\t\tdescription := matches[3]\n\n\t\tparsedTime, err := time.Parse(time.RFC3339, date)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tphase := model.EventPhaseBegin\n\t\tif state == \"finished\" {\n\t\t\tphase = model.EventPhaseEnd\n\t\t}\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      description,\n\t\t\tCategory:  logName,\n\t\t\tPhase:     phase,\n\t\t\tTimeStamp: parsedTime,\n\t\t})\n\t}\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/rc.go",
    "content": "package parsers\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/rdctl\"\n)\n\nfunc ProcessRCLogs(ctx context.Context) ([]*model.Event, error) {\n\tstdout, err := rdctl.Rdctl(ctx, \"shell\", \"sudo\",\n\t\t\"sh\", \"-c\", \"cat /var/log/rc.log || echo 'MISSING'\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get rc.log: %w\", err)\n\t}\n\tif bytes.HasPrefix(stdout.Bytes(), []byte(\"MISSING\")) {\n\t\tlog.Println(\"Failed to open /var/log/rc.log\")\n\t\treturn nil, nil\n\t}\n\tmatcher := regexp.MustCompile(`^rc (.*?) logging (started|stopped) at (.*)$`)\n\tlocation := time.UTC\n\tif runtime.GOOS == osWindows {\n\t\tlocation = time.Local\n\t}\n\tvar results []*model.Event\n\tscanner := bufio.NewScanner(stdout)\n\tfor scanner.Scan() {\n\t\tmatch := matcher.FindStringSubmatch(scanner.Text())\n\t\tif match == nil {\n\t\t\tcontinue\n\t\t}\n\t\tlevel := match[1]\n\t\taction := match[2]\n\t\ttimeStamp, err := time.ParseInLocation(time.ANSIC, match[3], location)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tphase := model.EventPhaseBegin\n\t\tif action == \"stopped\" {\n\t\t\tphase = model.EventPhaseEnd\n\t\t}\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      fmt.Sprintf(\"runlevel %s\", level),\n\t\t\tCategory:  \"rc\",\n\t\t\tPhase:     phase,\n\t\t\tTimeStamp: timeStamp.UTC(),\n\t\t})\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/windows-guest-agent.go",
    "content": "package parsers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\nfunc ParseWindowsGuestAgentLogs(ctx context.Context) ([]*model.Event, error) {\n\tif runtime.GOOS != osWindows {\n\t\treturn nil, nil\n\t}\n\n\tscanner, err := readRDLogFile(ctx, \"rancher-desktop-guestagent.log\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^(\\d{4}/\\d{2}/\\d{2}\\s+\\d{2}:\\d{2}:\\d{2})\\s+\\[.*?\\]\\s+(.*)$`)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) != matcher.NumSubexp()+1 {\n\t\t\tcontinue\n\t\t}\n\t\ttimestamp, err := time.ParseInLocation(\"2006/01/02 15:04:05\", matches[1], time.Local)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      matches[2],\n\t\t\tCategory:  \"guest-agent\",\n\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\tTimeStamp: timestamp,\n\t\t})\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/windows-integration.go",
    "content": "package parsers\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\nfunc ParseWindowsIntegrationLogs(ctx context.Context) ([]*model.Event, error) {\n\tif runtime.GOOS != osWindows {\n\t\treturn nil, nil\n\t}\n\n\tscanner, err := readRDLogFile(ctx, \"integrations.log\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z):\\s+(.*)$`)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) != matcher.NumSubexp()+1 {\n\t\t\tcontinue\n\t\t}\n\t\ttimestamp, err := time.Parse(time.RFC3339, matches[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      matches[2],\n\t\t\tCategory:  \"integrations\",\n\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\tTimeStamp: timestamp,\n\t\t})\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/parsers/wsl-helper.go",
    "content": "package parsers\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\nfunc ParseWSLHelperLogs(ctx context.Context) ([]*model.Event, error) {\n\tscanner, err := readRDLogFile(ctx, \"wsl-helper.log\")\n\tif err != nil {\n\t\tif errors.Is(err, fs.ErrNotExist) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\tvar results []*model.Event\n\tmatcher := regexp.MustCompile(`^time=\"(.*?)\" level=(\\w+) msg=(\".*?\"|[^\"]\\w+)`)\n\tfor scanner.Scan() {\n\t\tmatches := matcher.FindStringSubmatch(scanner.Text())\n\t\tif len(matches) != matcher.NumSubexp()+1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif matches[2] == \"debug\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ttimestamp, err := time.Parse(time.RFC3339, matches[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error parsing time: %q: %w\", scanner.Text(), err)\n\t\t}\n\n\t\tmsg := matches[3]\n\t\tvar result string\n\t\tif err := json.Unmarshal([]byte(msg), &result); err == nil {\n\t\t\tmsg = result\n\t\t}\n\n\t\tresults = append(results, &model.Event{\n\t\t\tName:      msg,\n\t\t\tCategory:  \"wsl-helper\",\n\t\t\tPhase:     model.EventPhaseInstant,\n\t\t\tTimeStamp: timestamp,\n\t\t})\n\t}\n\n\treturn results, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/rdctl/rdctl.go",
    "content": "package rdctl\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n)\n\n// Run rdctl and return its standard output.\nfunc Rdctl(ctx context.Context, args ...string) (*bytes.Buffer, error) {\n\texe, err := exec.LookPath(\"rdctl\")\n\tif err != nil {\n\t\trelPath := \"resources/linux/bin/rdctl\"\n\t\tswitch runtime.GOOS {\n\t\tcase \"darwin\":\n\t\t\trelPath = \"resources/darwin/bin/rdctl\"\n\t\tcase \"windows\":\n\t\t\trelPath = `resources\\win32\\bin\\rdctl.exe`\n\t\t}\n\t\tdir, err := os.Getwd()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to get working directory: %w\", err)\n\t\t}\n\t\tfor dir != filepath.Dir(dir) {\n\t\t\texe = filepath.Join(dir, relPath)\n\t\t\tif _, err := os.Stat(exe); err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tdir = filepath.Dir(dir)\n\t\t}\n\t\tif dir == filepath.Dir(dir) {\n\t\t\treturn nil, fmt.Errorf(\"could not find rdctl\")\n\t\t}\n\t}\n\tbuf := &bytes.Buffer{}\n\tcmd := exec.CommandContext(ctx, exe, args...)\n\tcmd.Stdout = buf\n\tcmd.Stderr = os.Stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/render/model.go",
    "content": "package render\n\nimport \"time\"\n\ntype nodeId int64\n\n// https://chromedevtools.github.io/devtools-protocol/1-3/Profiler/#type-Profile\n// This is the root object of the document.\ntype profile struct {\n\tNodes      []*profileNode `json:\"nodes\"`\n\tStartTime  int64          `json:\"startTime\"`\n\tEndTime    int64          `json:\"endTime\"`\n\tSamples    []nodeId       `json:\"samples\"`\n\tTimeDeltas []int64        `json:\"timeDeltas\"`\n}\n\n// https://chromedevtools.github.io/devtools-protocol/1-3/Profiler/#type-ProfileNode\ntype profileNode struct {\n\tId        nodeId        `json:\"id\"`\n\tCallFrame callFrame     `json:\"callFrame\"`\n\tChildren  []nodeId      `json:\"children,omitempty\"`\n\tStartTime time.Time     `json:\"-startTime\"`\n\tStopTime  time.Time     `json:\"-stopTime\"`\n\tDuration  time.Duration `json:\"-duration\"`\n}\n\n// https://chromedevtools.github.io/devtools-protocol/1-3/Runtime/#type-CallFrame\ntype callFrame struct {\n\tFunctionName string `json:\"functionName\"`\n\tScriptId     string `json:\"scriptId\"`\n\tUrl          string `json:\"url\"`\n\tLineNumber   int64  `json:\"lineNumber\"`\n\tColumnNumber int64  `json:\"columnNumber\"`\n}\n"
  },
  {
    "path": "src/go/startup-profile/render/process.go",
    "content": "package render\n\nimport (\n\t\"cmp\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\n// Normalize events from a single source.  This means:\n// - All begin/end pairs have a minimum delta (i.e. not zero-time).\n// - All begin events have a duration set (i.e. not zero-time).\n// - All instant events have a duration set (i.e. not zero-time).\n// - Any events following zero-time events have been moved back.\nfunc ProcessSource(ctx context.Context, name string, events []*model.Event) error {\n\t// If we have no events, don't touch anything.\n\tif len(events) == 0 {\n\t\treturn nil\n\t}\n\n\t// Make sure the inputs are in chronological order.\n\tslices.SortStableFunc(events, func(a, b *model.Event) int {\n\t\treturn a.TimeStamp.Compare(b.TimeStamp)\n\t})\n\n\t// For any instant events, as well as begin/end pairs of time zero, set their\n\t// time to one microsecond.\n\tminimumTime := events[0].TimeStamp\n\tfor i, event := range events {\n\t\tif event.TimeStamp.Before(minimumTime) {\n\t\t\tevent.TimeStamp = minimumTime\n\t\t}\n\t\tswitch event.Phase {\n\t\tcase model.EventPhaseBegin:\n\t\t\t// TODO: make this not O(n^2)\n\t\t\tvar endEvent *model.Event\n\t\t\tfor j := i + 1; endEvent == nil && j < len(events); j++ {\n\t\t\t\tcandidate := events[j]\n\t\t\t\tif candidate.Phase != model.EventPhaseEnd {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif candidate.Category != event.Category {\n\t\t\t\t\t// Should not happen: this should be from the same source.\n\t\t\t\t\tpanic(fmt.Sprintf(\"Unexpected category %s/%s\", candidate.Category, event.Category))\n\t\t\t\t}\n\t\t\t\tif candidate.Name != event.Name {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tendEvent = candidate\n\t\t\t}\n\t\t\tif endEvent == nil {\n\t\t\t\treturn fmt.Errorf(\"failed to find end event for %+v\", event)\n\t\t\t}\n\t\t\tif endEvent.TimeStamp.After(event.TimeStamp) {\n\t\t\t\tevent.Duration = endEvent.TimeStamp.Sub(event.TimeStamp)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// If we get here, then this is a begin/end pair with zero time.\n\t\t\tevent.Duration = time.Microsecond\n\t\t\tminimumTime = event.TimeStamp.Add(event.Duration)\n\t\tcase model.EventPhaseEnd:\n\t\t\tendTime := event.TimeStamp.Add(time.Microsecond)\n\t\t\tif minimumTime.Before(endTime) {\n\t\t\t\tminimumTime = endTime\n\t\t\t}\n\t\tcase model.EventPhaseInstant:\n\t\t\tevent.Duration = time.Microsecond\n\t\t\tif event.TimeStamp.Before(minimumTime) {\n\t\t\t\tevent.TimeStamp = minimumTime\n\t\t\t}\n\t\t\tminimumTime = event.TimeStamp.Add(event.Duration)\n\t\t}\n\t}\n\n\tif f, err := os.Create(name + \".json\"); err == nil {\n\t\tencoder := json.NewEncoder(f)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\tif err := encoder.Encode(events); err != nil {\n\t\t\tslog.ErrorContext(ctx, \"error writing debug logs\", \"processor\", name, \"error\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Process the events to normalize them.  At this point, ProcessSource must have\n// been called already (but the events may be out of order).\nfunc processEvents(events []*model.Event) error {\n\t// Start time is the time of the chronologically first event.\n\tstartTime := time.Now()\n\t// Beginnings is a map from category then name to the event index for the \"begin\" event\n\tbeginnings := make(map[string]map[string]int)\n\t// Endings is a map from the begin event to the end event, by event id.\n\tendings := make(map[int]int)\n\n\tfor i, event := range events {\n\t\tif !event.TimeStamp.IsZero() && event.TimeStamp.Before(startTime) {\n\t\t\tstartTime = event.TimeStamp\n\t\t}\n\t\tswitch event.Phase {\n\t\tcase model.EventPhaseBegin:\n\t\t\tif _, ok := beginnings[event.Category]; !ok {\n\t\t\t\tbeginnings[event.Category] = make(map[string]int)\n\t\t\t}\n\t\t\tbeginnings[event.Category][event.Name] = i\n\t\tcase model.EventPhaseEnd:\n\t\t\tnames, ok := beginnings[event.Category]\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"events out of order: found ending event %+v before category\", event)\n\t\t\t}\n\t\t\tbeginId, ok := names[event.Name]\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"events out of order: found ending event %+v before beginning\", event)\n\t\t\t}\n\t\t\tendings[beginId] = i\n\t\t\tevent.TimeStamp = events[beginId].TimeStamp.Add(events[beginId].Duration)\n\t\t}\n\t}\n\n\tfor _, event := range events {\n\t\tif event.TimeStamp.IsZero() {\n\t\t\tevent.TimeStamp = startTime.Add(-time.Nanosecond)\n\t\t}\n\t}\n\n\t// Process the events to make sure they have (offset) times\n\tslices.SortStableFunc(events, func(a, b *model.Event) int {\n\t\tif start := a.TimeStamp.Compare(b.TimeStamp); start != 0 {\n\t\t\treturn start\n\t\t}\n\t\t// Compare by duration, with longer events first.\n\t\t// Do not otherwise sort them, to keep things like dmesg lines in order.\n\t\treturn -cmp.Compare(a.Duration, b.Duration)\n\t})\n\n\t// Set the time-since-start field.\n\tfor _, event := range events {\n\t\tevent.Time = int64(event.TimeStamp.Sub(events[0].TimeStamp) / time.Microsecond)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/render/render.go",
    "content": "package render\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"slices\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n)\n\n// Render the events into a data structure suitable to be JSON-encoded into a\n// file as a Chrome CPU profile.\nfunc Render(ctx context.Context, events []*model.Event) (any, error) {\n\t// Insert a fake root event at the start\n\tevents = append([]*model.Event{\n\t\t{\n\t\t\tName:     \"(root)\",\n\t\t\tCategory: \"root\",\n\t\t\tPhase:    model.EventPhaseBegin,\n\t\t\tTime:     0,\n\t\t},\n\t}, events...)\n\n\tif err := processEvents(events); err != nil {\n\t\treturn nil, err\n\t}\n\n\tprofile := profile{\n\t\tStartTime: events[0].TimeStamp.UnixMicro(),\n\t\tEndTime:   events[len(events)-1].TimeStamp.UnixMicro(),\n\t}\n\n\tevents[0].Duration = events[len(events)-1].TimeStamp.Sub(events[0].TimeStamp)\n\n\t// Insert the end of the fake root event at the end\n\tevents = append(events, &model.Event{\n\t\tName:     \"(root)\",\n\t\tCategory: \"root\",\n\t\tPhase:    model.EventPhaseEnd,\n\t\tTime:     profile.EndTime - profile.StartTime,\n\t})\n\n\tif f, err := os.Create(\"processed.json\"); err == nil {\n\t\tencoder := json.NewEncoder(f)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\tif err := encoder.Encode(events); err != nil {\n\t\t\tslog.ErrorContext(ctx, \"error writing debug logs\", \"error\", err)\n\t\t}\n\t}\n\n\tvar stack []*profileNode\n\tvar lastTime int64\n\tnextId := nodeId(1)\n\tfor i, event := range events {\n\t\tswitch event.Phase {\n\t\tcase model.EventPhaseBegin:\n\t\t\tif i >= len(events)-1 {\n\t\t\t\t// This is the last event; but it's a begin phase\n\t\t\t\treturn nil, fmt.Errorf(\"invalid event stream: last event is in phase begin: %+v\", event)\n\t\t\t}\n\t\t\tnode := profileNode{\n\t\t\t\tId: nextId,\n\t\t\t\tCallFrame: callFrame{\n\t\t\t\t\tFunctionName: event.Name,\n\t\t\t\t\tScriptId:     event.Category,\n\t\t\t\t\tUrl:          event.Category,\n\t\t\t\t\tLineNumber:   -1,\n\t\t\t\t\tColumnNumber: -1,\n\t\t\t\t},\n\t\t\t\tStartTime: event.TimeStamp,\n\t\t\t\tStopTime:  event.TimeStamp.Add(event.Duration),\n\t\t\t\tDuration:  event.Duration,\n\t\t\t}\n\t\t\tprofile.Nodes = append(profile.Nodes, &node)\n\t\t\tnextId++\n\t\t\tif len(stack) > 0 {\n\t\t\t\tstack[len(stack)-1].Children = append(stack[len(stack)-1].Children, node.Id)\n\t\t\t}\n\t\t\tstack = append(stack, &node)\n\t\t\tnextEvent := events[i+1]\n\t\t\tif event.Time < nextEvent.Time {\n\t\t\t\t// The next event is at a different time; insert this node.\n\t\t\t\tprofile.Samples = append(profile.Samples, node.Id)\n\t\t\t\tprofile.TimeDeltas = append(profile.TimeDeltas, max(event.Time-lastTime, 1))\n\t\t\t\tlastTime = event.Time\n\t\t\t}\n\t\tcase model.EventPhaseEnd:\n\t\t\tif i < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid event stream: first event is in phase end: %+v\", event)\n\t\t\t}\n\t\t\tif len(stack) < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid event stream: ending with empty stack: %+v\", event)\n\t\t\t}\n\t\t\tfor j := len(stack) - 1; j >= 0; j-- {\n\t\t\t\tif stack[j].CallFrame.FunctionName != event.Name {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif stack[j].CallFrame.ScriptId != event.Category {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\t// We need to remove this item from the stack; however, logically this\n\t\t\t\t// means we need to recreate any nodes on top (with new IDs) because\n\t\t\t\t// the profile is supposed to have matching stacks.\n\t\t\t\tfor k := j; k < len(stack)-1; k++ {\n\t\t\t\t\tnode := profileNode{\n\t\t\t\t\t\tId:        nextId,\n\t\t\t\t\t\tCallFrame: stack[k+1].CallFrame,\n\t\t\t\t\t\tChildren:  slices.Clone(stack[k+1].Children),\n\t\t\t\t\t\tStartTime: event.TimeStamp,\n\t\t\t\t\t\tStopTime:  event.TimeStamp.Add(event.Duration),\n\t\t\t\t\t\tDuration:  event.Duration,\n\t\t\t\t\t}\n\t\t\t\t\tstack[k] = &node\n\t\t\t\t\tprofile.Nodes = append(profile.Nodes, &node)\n\t\t\t\t\tnextId++\n\t\t\t\t\tif k > 0 {\n\t\t\t\t\t\tstack[k-1].Children = append(stack[k-1].Children, node.Id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstack = stack[:len(stack)-1]\n\t\t\t\tbreak\n\t\t\t}\n\t\t\temitSample := len(stack) > 0\n\t\t\tif len(events)-1 > i {\n\t\t\t\t// There are more events; check if the next event has a time change\n\t\t\t\tnextEvent := events[i+1]\n\t\t\t\temitSample = emitSample && event.Time != nextEvent.Time\n\t\t\t}\n\t\t\tif emitSample {\n\t\t\t\t// The next event is at a different time; insert this node.\n\t\t\t\tprofile.Samples = append(profile.Samples, stack[len(stack)-1].Id)\n\t\t\t\tprofile.TimeDeltas = append(profile.TimeDeltas, max(event.Time-lastTime, 1))\n\t\t\t\tlastTime = event.Time\n\t\t\t}\n\t\tcase model.EventPhaseInstant:\n\t\t\tif i < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid event stream: first event is instant: %+v\", event)\n\t\t\t}\n\t\t\tif len(stack) < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid event stream: instant event with no stack: %+v\", event)\n\t\t\t}\n\t\t\tnode := profileNode{\n\t\t\t\tId: nextId,\n\t\t\t\tCallFrame: callFrame{\n\t\t\t\t\tFunctionName: event.Name,\n\t\t\t\t\tScriptId:     event.Category,\n\t\t\t\t\tUrl:          event.Category,\n\t\t\t\t\tLineNumber:   -1,\n\t\t\t\t\tColumnNumber: -1,\n\t\t\t\t},\n\t\t\t\tStartTime: event.TimeStamp,\n\t\t\t\tStopTime:  event.TimeStamp.Add(event.Duration),\n\t\t\t\tDuration:  event.Duration,\n\t\t\t}\n\t\t\tnextId++\n\t\t\tstackTop := stack[len(stack)-1]\n\t\t\t// Always emit instant events directly, even if no time has passed\n\t\t\tstackTop.Children = append(stackTop.Children, node.Id)\n\t\t\tprofile.Nodes = append(profile.Nodes, &node)\n\t\t\tprofile.Samples = append(profile.Samples, node.Id)\n\t\t\tprofile.TimeDeltas = append(profile.TimeDeltas, max(event.Time-lastTime, 0))\n\t\t\tlastTime = event.Time\n\t\t\t// If there isn't another event at the same time, \"sample\" the parent again\n\t\t\tif events[i+1].Time > event.Time {\n\t\t\t\tprofile.Samples = append(profile.Samples, stackTop.Id)\n\t\t\t\tprofile.TimeDeltas = append(profile.TimeDeltas, 0)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"invalid event stream: unknown phase: %+v\", event)\n\t\t}\n\t}\n\n\treturn profile, nil\n}\n"
  },
  {
    "path": "src/go/startup-profile/run.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"sync\"\n\n\t\"golang.org/x/sync/errgroup\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/model\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/parsers\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/startup-profile/render\"\n)\n\nfunc run(ctx context.Context, outPath marshalledPath) error {\n\t// Normalize the output path, in case it's still the default value.\n\tif err := outPath.UnmarshalText([]byte(outPath)); err != nil {\n\t\treturn fmt.Errorf(\"error normalizing output path %s: %w\", outPath, err)\n\t}\n\n\t// Set up the channel we will use to read events\n\tmutex := sync.Mutex{}\n\tevents := make([]*model.Event, 0, 1024)\n\tgroup, ctx := errgroup.WithContext(context.Background())\n\n\t// Run the individual data collectors\n\tprocessors := map[string]parsers.Parser{\n\t\t\"lima\":                parsers.ParseLimaInitLogs,\n\t\t\"progress\":            parsers.ParseProgress,\n\t\t\"dmesg\":               parsers.ParseDmesg,\n\t\t\"openrc\":              parsers.ProcessRCLogs,\n\t\t\"host-agent\":          parsers.ParseLimaHostAgentLogs,\n\t\t\"networking\":          parsers.ParseNetworkingLogs,\n\t\t\"windows-guest-agent\": parsers.ParseWindowsGuestAgentLogs,\n\t\t\"windows-integration\": parsers.ParseWindowsIntegrationLogs,\n\t\t\"wsl-helper\":          parsers.ParseWSLHelperLogs,\n\t}\n\n\tfor name, p := range processors {\n\t\tgroup.Go(func() error {\n\t\t\tresults, err := p(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err := render.ProcessSource(ctx, name, results); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tmutex.Lock()\n\t\t\tevents = append(events, results...)\n\t\t\tmutex.Unlock()\n\t\t\tslog.InfoContext(ctx, \"got events\", \"source\", name, \"count\", len(results))\n\t\t\treturn nil\n\t\t})\n\t}\n\n\tif err := group.Wait(); err != nil {\n\t\treturn err\n\t}\n\tif len(events) < 1 {\n\t\treturn fmt.Errorf(\"no events found\")\n\t}\n\n\t// Emit the output\n\tdata, err := render.Render(ctx, events)\n\tif err != nil {\n\t\treturn err\n\t}\n\toutFile, err := os.Create(string(outPath))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create output file %s: %w\", outPath, err)\n\t}\n\tencoder := json.NewEncoder(outFile)\n\tencoder.SetIndent(\"\", \"  \")\n\tif err := encoder.Encode(data); err != nil {\n\t\treturn fmt.Errorf(\"failed to encode events: %w\", err)\n\t}\n\tif err := outFile.Close(); err != nil {\n\t\treturn fmt.Errorf(\"failed to flush output file %s: %w\", outPath, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/.gitignore",
    "content": "/pkg/dockerproxy/models/\n/pkg/dockerproxy/swagger.yaml\n/pkg/dockerproxy/swagger-modified.yaml\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/certificates_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"encoding/pem\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/certificates\"\n)\n\nvar certificatesViper = viper.New()\n\n// certificatesCmd represents the `certificates` command.\nvar certificatesCmd = &cobra.Command{\n\tUse:   \"certificates\",\n\tShort: \"Lists the installed system certificates in PEM format\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\tfor _, storeName := range certificatesViper.GetStringSlice(\"stores\") {\n\t\t\tch, err := certificates.GetSystemCertificates(storeName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor entry := range ch {\n\t\t\t\tif entry.Err != nil {\n\t\t\t\t\treturn entry.Err\n\t\t\t\t}\n\t\t\t\tif entry.Cert == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif entry.Cert.NotAfter.Before(time.Now()) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tblock := &pem.Block{Type: \"CERTIFICATE\", Bytes: entry.Cert.Raw}\n\t\t\t\terr = pem.Encode(os.Stdout, block)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\tcertificatesCmd.Flags().StringSlice(\"stores\", []string{\"CA\", \"ROOT\"}, \"Certificate stores to enumerate\")\n\tcertificatesViper.AutomaticEnv()\n\tif err := certificatesViper.BindPFlags(certificatesCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\trootCmd.AddCommand(certificatesCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/dockerproxy.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// dockerproxyStartCmd is the `wsl-helper docker-proxy` command.\n// It only has subcommands, and no functionality of its own.\nvar dockerproxyCmd = &cobra.Command{\n\tUse:   \"docker-proxy\",\n\tShort: \"Commands for managing the docker socket proxy\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(dockerproxyCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/dockerproxy_kill_linux.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t// Pull in to register the mungers\n\t_ \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/mungers\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/process\"\n)\n\nvar dockerproxyKillViper = viper.New()\n\n// dockerproxyKillCmd is the `wsl-helper docker-proxy kill` command.\nvar dockerproxyKillCmd = &cobra.Command{\n\tUse:   \"kill\",\n\tShort: \"Force stop any instances of the docker socket proxy server\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\terr := process.KillOthers(\"docker-proxy\", \"serve\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\tdockerproxyKillViper.AutomaticEnv()\n\tif err := dockerproxyKillViper.BindPFlags(dockerproxyKillCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tdockerproxyCmd.AddCommand(dockerproxyKillCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/dockerproxy_serve_linux.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t// Pull in to register the mungers\n\t_ \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/mungers\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/process\"\n)\n\nvar dockerproxyServeViper = viper.New()\n\n// dockerproxyServeCmd is the `wsl-helper docker-proxy serve` command.\nvar dockerproxyServeCmd = &cobra.Command{\n\tUse:   \"serve\",\n\tShort: \"Start the docker socket proxy server\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\tcmd.SilenceErrors = true\n\t\tendpoint := dockerproxyServeViper.GetString(\"endpoint\")\n\t\tproxyEndpoint := dockerproxyServeViper.GetString(\"proxy-endpoint\")\n\t\terr := process.KillOthers(\"docker-proxy\", \"serve\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdialer, err := platform.MakeDialer(proxyEndpoint)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = dockerproxy.Serve(cmd.Context(), endpoint, dialer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\tdefaultProxyEndpoint, err := dockerproxy.GetDefaultProxyEndpoint()\n\tif err != nil {\n\t\tlogrus.Fatalf(\"could not initialize options: %s\", err)\n\t}\n\tdockerproxyServeCmd.Flags().String(\"endpoint\", platform.DefaultEndpoint, \"Endpoint to listen on\")\n\tdockerproxyServeCmd.Flags().String(\"proxy-endpoint\", defaultProxyEndpoint, \"Endpoint dockerd is listening on\")\n\tdockerproxyServeViper.AutomaticEnv()\n\tif err := dockerproxyServeViper.BindPFlags(dockerproxyServeCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tdockerproxyCmd.AddCommand(dockerproxyServeCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/dockerproxy_serve_windows.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t// Pull in to register the mungers\n\t_ \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/mungers\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n)\n\nvar dockerproxyServeViper = viper.New()\n\n// dockerproxyServeCmd is the `wsl-helper docker-proxy serve` command.\nvar dockerproxyServeCmd = &cobra.Command{\n\tUse:   \"serve\",\n\tShort: \"Start the docker socket proxy server\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tisInternalCommand = true\n\t\tcmd.SilenceUsage = true\n\t\tcmd.SilenceErrors = true\n\t\tendpoint := dockerproxyServeViper.GetString(\"endpoint\")\n\t\tport := dockerproxyServeViper.GetUint32(\"port\")\n\t\tdialer, err := platform.MakeDialer(port)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\terr = dockerproxy.Serve(cmd.Context(), endpoint, dialer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\tdockerproxyServeCmd.Flags().String(\"endpoint\", platform.DefaultEndpoint, \"Endpoint to listen on\")\n\tdockerproxyServeCmd.Flags().Uint32(\"port\", dockerproxy.DefaultPort, \"Vsock port docker is listening on\")\n\tdockerproxyServeViper.AutomaticEnv()\n\tif err := dockerproxyServeViper.BindPFlags(dockerproxyServeCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tdockerproxyCmd.AddCommand(dockerproxyServeCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/dockerproxy_start.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n)\n\nvar dockerproxyStartViper = viper.New()\n\n// dockerproxyStartCmd is the `wsl-helper docker-proxy start` command.\n// This command is Linux-only.\nvar dockerproxyStartCmd = &cobra.Command{\n\tUse:   \"start\",\n\tShort: \"Start the docker daemon using vsock\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tport := dockerproxyStartViper.GetUint32(\"port\")\n\t\tendpoint := dockerproxyStartViper.GetString(\"endpoint\")\n\t\treturn dockerproxy.Start(cmd.Context(), port, endpoint, args)\n\t},\n}\n\nfunc init() {\n\tdefaultProxyEndpoint, err := dockerproxy.GetDefaultProxyEndpoint()\n\tif err != nil {\n\t\tlogrus.Fatalf(\"could not initialize options: %s\", err)\n\t}\n\tdockerproxyStartCmd.Flags().Uint32(\"port\", dockerproxy.DefaultPort, \"Vsock port to listen on\")\n\tdockerproxyStartCmd.Flags().String(\"endpoint\", defaultProxyEndpoint, \"Dockerd socket endpoint\")\n\tdockerproxyStartViper.AutomaticEnv()\n\tif err := dockerproxyStartViper.BindPFlags(dockerproxyStartCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tdockerproxyCmd.AddCommand(dockerproxyStartCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/enum.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport \"fmt\"\n\n// enumValue describes an enumeration for use with github.com/spf13/pflag\n// This struct is currently only used on Linux.\ntype enumValue struct {\n\tallowed []string // Allowed values\n\tval     string   // Current value\n}\n\nfunc (v *enumValue) String() string {\n\treturn v.val\n}\n\nfunc (v *enumValue) Set(newVal string) error {\n\tfor _, candidate := range v.allowed {\n\t\tif candidate == newVal {\n\t\t\tv.val = candidate\n\t\t\treturn nil\n\t\t}\n\t}\n\treturn fmt.Errorf(\"value %q is not one of the allowed values: %+v\", newVal, v.allowed)\n}\n\nfunc (v *enumValue) Type() string {\n\treturn \"enum\"\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/k3s.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// k3sCmd represents the k3s command\nvar k3sCmd = &cobra.Command{\n\tUse:   \"k3s\",\n\tShort: \"Commands for interacting with k3s in WSL\",\n}\n\nfunc init() {\n\trootCmd.AddCommand(k3sCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/k3s_kubeconfig.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype kubeConfig struct {\n\tClusters []struct {\n\t\tCluster struct {\n\t\t\tServer string\n\t\t\tExtras map[string]interface{} `yaml:\",inline\"`\n\t\t} `yaml:\"cluster\"`\n\t\tName   string                 `yaml:\"name\"`\n\t\tExtras map[string]interface{} `yaml:\",inline\"`\n\t} `yaml:\"clusters\"`\n\tContexts []struct {\n\t\tName   string                 `yaml:\"name\"`\n\t\tExtras map[string]interface{} `yaml:\",inline\"`\n\t} `yaml:\"contexts\"`\n\tCurrentContext string `yaml:\"current-context\"`\n\tUsers          []struct {\n\t\tName   string                 `yaml:\"name\"`\n\t\tExtras map[string]interface{} `yaml:\",inline\"`\n\t} `yaml:\"users\"`\n\tExtras map[string]interface{} `yaml:\",inline\"`\n}\n\nconst kubeConfigExistTimeout = 10 * time.Second\n\nvar k3sKubeconfigViper = viper.New()\n\n// k3sKubeconfigCmd represents the `k3s kubeconfig` command.\nvar k3sKubeconfigCmd = &cobra.Command{\n\tUse:   \"kubeconfig\",\n\tShort: \"Fetch kubeconfig from the WSL VM\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t// Read the existing kubeconfig.  Wait up to 10 seconds for it to exist.\n\t\tch := make(chan *os.File)\n\t\tabort := false\n\t\tgo func() {\n\t\t\tconfigPath := k3sKubeconfigViper.GetString(\"k3sconfig\")\n\t\t\tfor {\n\t\t\t\tif abort {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tf, err := os.Open(configPath)\n\t\t\t\tif err == nil {\n\t\t\t\t\tch <- f\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\ttime.Sleep(time.Second)\n\t\t\t}\n\t\t}()\n\t\tvar err error\n\t\ttimeout := time.After(kubeConfigExistTimeout)\n\t\tvar configFile *os.File\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\treturn fmt.Errorf(\"timed out waiting for k3s kubeconfig to exist\")\n\t\tcase configFile = <-ch:\n\t\t\tbreak\n\t\t}\n\n\t\tvar config kubeConfig\n\t\tdefer configFile.Close()\n\t\terr = yaml.NewDecoder(configFile).Decode(&config)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// vm-switch in rdNetworking binds to localhost:Port by default.\n\t\t// Since k3s.yaml comes with servers preset at 127.0.0.1, there\n\t\t// is nothing for us to do here, just write the config and return.\n\t\treturn yaml.NewEncoder(os.Stdout).Encode(config)\n\t},\n}\n\nfunc init() {\n\tk3sKubeconfigCmd.Flags().String(\"k3sconfig\", \"/etc/rancher/k3s/k3s.yaml\", \"Path to k3s kubeconfig\")\n\tk3sKubeconfigViper.AutomaticEnv()\n\tif err := k3sKubeconfigViper.BindPFlags(k3sKubeconfigCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tk3sCmd.AddCommand(k3sKubeconfigCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/kubeconfig.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/client-go/util/homedir\"\n)\n\nvar kubeconfigViper = viper.New()\n\nconst rdCluster = \"rancher-desktop\"\n\n// kubeconfigCmd represents the kubeconfig command, used to set up a symlink on\n// the Linux side to point at the Windows-side kubeconfig.  Note that we must\n// pass the kubeconfig path in as an environment variable to take advantage of\n// the path translation capabilities of WSL2 interop.\nvar kubeconfigCmd = &cobra.Command{\n\tUse:   \"kubeconfig\",\n\tShort: \"Set up ~/.kube/config in the WSL2 environment\",\n\tLong:  `This command configures the Kubernetes configuration inside a WSL2 distribution.`,\n\tArgs:  cobra.ExactArgs(0),\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\n\t\tconfigPath := kubeconfigViper.GetString(\"kubeconfig\")\n\t\tenable := kubeconfigViper.GetBool(\"enable\")\n\t\tverify := kubeconfigViper.GetBool(\"verify\")\n\n\t\tconfigDir := path.Join(homedir.HomeDir(), \".kube\")\n\t\tlinkPath := path.Join(configDir, \"config\")\n\t\tunsupportedConfig, symlinkErr := requireManualSymlink(linkPath)\n\t\tif verify {\n\t\t\tif unsupportedConfig {\n\t\t\t\tlogrus.Fatalf(\"kubeConfig: %s contains non-rancher desktop configuration\", linkPath)\n\t\t\t}\n\t\t\tlogrus.Infof(\"Verified kubeConfig: %s, it only contains Rancher Desktop configuration\", linkPath)\n\t\t\tos.Exit(0)\n\t\t}\n\n\t\tif configPath == \"\" {\n\t\t\treturn errors.New(\"Windows kubeconfig not supplied\")\n\t\t}\n\n\t\t_, err := os.Stat(configPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not open Windows kubeconfig: %w\", err)\n\t\t}\n\n\t\tif !unsupportedConfig && symlinkErr != nil {\n\t\t\treturn symlinkErr\n\t\t}\n\n\t\tif enable {\n\t\t\tif unsupportedConfig {\n\t\t\t\t// Config contains non-Rancher Desktop configuration\n\t\t\t\treturn symlinkErr\n\t\t\t}\n\t\t\terr = os.Mkdir(configDir, 0o750)\n\t\t\tif err != nil && !errors.Is(err, os.ErrExist) {\n\t\t\t\t// The error already contains the full path, we can't do better.\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = os.Symlink(configPath, linkPath)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, os.ErrExist) {\n\t\t\t\t\t// If it already exists, do nothing; even if it's not a symlink.\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\t// No need to create if we want to remove it\n\t\t\ttarget, err := os.Readlink(linkPath)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif target == configPath {\n\t\t\t\tif err = removeConfig(linkPath); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t},\n}\n\n// requireManualSymlink checks the config to determine if it contains a single entry for Contexts, Clusters, and Users.\n// If all three are named 'rancher-desktop', we assume that this configuration was written by Rancher Desktop 1.12,\n// and we can remove it and replace it with a symlink. If a user's config contains Rancher Desktop's specific configuration\n// along with user-provided config, or if it only contains user-provided config, we return a true and an error.\n// This indicates through diagnostics to the user that manual action is required.\nfunc requireManualSymlink(linkPath string) (bool, error) {\n\t// Check to see if config is rancher desktop only\n\tif existingConfig, err := readKubeConfig(linkPath); err == nil {\n\t\tif len(existingConfig.Contexts) == 1 && existingConfig.Contexts[0].Name == rdCluster &&\n\t\t\tlen(existingConfig.Clusters) == 1 && existingConfig.Clusters[0].Name == rdCluster &&\n\t\t\tlen(existingConfig.Users) == 1 && existingConfig.Users[0].Name == rdCluster {\n\t\t\tif err := removeConfig(linkPath); err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t} else {\n\t\t\treturn true, fmt.Errorf(\"not overwriting kubeconfig file %s with non-Rancher Desktop contents\", linkPath)\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc removeConfig(configPath string) error {\n\terr := os.Remove(configPath)\n\tif err != nil && !errors.Is(err, os.ErrNotExist) {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc readKubeConfig(configPath string) (kubeConfig, error) {\n\tvar config kubeConfig\n\tconfigFile, err := os.Open(configPath)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"could not open kubeconfig file %s: %w\", configPath, err)\n\t}\n\tdefer configFile.Close()\n\terr = yaml.NewDecoder(configFile).Decode(&config)\n\tif err != nil {\n\t\treturn config, fmt.Errorf(\"could not read kubeconfig %s: %w\", configPath, err)\n\t}\n\n\treturn config, nil\n}\n\nfunc init() {\n\tkubeconfigCmd.PersistentFlags().Bool(\"verify\", false, \"Checks whether the symlinked config contains non-Rancher Desktop configuration.\")\n\tkubeconfigCmd.PersistentFlags().Bool(\"enable\", true, \"Set up config file\")\n\tkubeconfigCmd.PersistentFlags().String(\"kubeconfig\", \"\", \"Path to Windows kubeconfig, in /mnt/... form.\")\n\tkubeconfigViper.AutomaticEnv()\n\tif err := kubeconfigViper.BindPFlags(kubeconfigCmd.PersistentFlags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\trootCmd.AddCommand(kubeconfigCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/process_kill_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/process\"\n)\n\nvar processKillViper = viper.New()\n\n// processKillCmd is the `wsl-helper process kill` command.\nvar processKillCmd = &cobra.Command{\n\tUse:   \"kill\",\n\tShort: \"Kill a given process\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\treturn process.Kill(processKillViper.GetInt(\"pid\"))\n\t},\n}\n\nfunc init() {\n\tprocessKillCmd.Flags().Int(\"pid\", 0, \"PID of process to kill\")\n\tprocessKillViper.AutomaticEnv()\n\tif err := processKillViper.BindPFlags(processKillCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tprocessCmd.AddCommand(processKillCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/process_spawn_windows.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/process\"\n)\n\nvar processSpawnViper = viper.New()\n\n// processSpawnCmd is the `wsl-helper process spawn` command.\nvar processSpawnCmd = &cobra.Command{\n\tUse:   \"spawn\",\n\tShort: \"Spawn a new process\",\n\tLong: `Spawn a new process, attached to a job that would be terminated when\n\tthe given parent process exits.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\tppid := processSpawnViper.GetUint32(\"parent\")\n\n\t\tif ppid == 0 {\n\t\t\tppid = uint32(os.Getpid())\n\t\t}\n\n\t\tstate, err := process.SpawnProcessInRDJob(ppid, args)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to spawn process: %w\", err)\n\t\t}\n\n\t\tos.Exit(state.ExitCode())\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\tprocessSpawnCmd.Flags().Uint32(\"parent\", 0, \"PID of the parent process\")\n\tprocessSpawnViper.AutomaticEnv()\n\tif err := processSpawnViper.BindPFlags(processSpawnCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\tprocessCmd.AddCommand(processSpawnCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/process_windows.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// processCmd represents the `wsl-helper process ...` subcommand\nvar processCmd = &cobra.Command{\n\tUse:    \"process\",\n\tShort:  \"Commands for managing processes on Windows\",\n\tHidden: true,\n}\n\nfunc init() {\n\trootCmd.AddCommand(processCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/root.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package cmd expresses the command-line interface.\npackage cmd\n\nimport (\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n)\n\n// If isInternalCommand is set, when the command has an error we will use logrus\n// to display the error.\nvar isInternalCommand bool\n\n// rootCmd represents the base command when called without any subcommands\nvar rootCmd = &cobra.Command{\n\tUse:   \"wsl-helper\",\n\tShort: \"Rancher Desktop WSL2 integration helper\",\n\tLong:  `This command handles various WSL2 integration tasks for Rancher Desktop.`,\n\tPersistentPreRun: func(cmd *cobra.Command, args []string) {\n\t\tlogrus.SetLevel(logrus.InfoLevel + logrus.Level(viper.GetInt(\"verbose\")))\n\t},\n}\n\n// Execute adds all child commands to the root command and sets flags appropriately.\n// This is called by main.main(). It only needs to happen once to the rootCmd.\nfunc Execute() {\n\terr := rootCmd.Execute()\n\tif !isInternalCommand {\n\t\tcobra.CheckErr(err)\n\t} else if err != nil {\n\t\tlogrus.Error(err)\n\t\tos.Exit(1)\n\t}\n}\n\nfunc init() {\n\trootCmd.PersistentFlags().Count(\"verbose\", \"enable extra logging\")\n\tcobra.OnInitialize(initConfig)\n}\n\n// initConfig reads in config file and ENV variables if set.\nfunc initConfig() {\n\tviper.AutomaticEnv() // read in environment variables that match\n\tif err := viper.BindPFlags(rootCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/version.go",
    "content": "/*\nCopyright © 2022 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/spf13/cobra\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/version\"\n)\n\n// showVersionCmd represents the showVersion command\nvar showVersionCmd = &cobra.Command{\n\tUse:   \"version\",\n\tShort: \"Shows the wsl-helper version.\",\n\tLong:  `Shows the wsl-helper version.`,\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\t_, err := fmt.Printf(\"wsl-helper version: %s\\n\", version.Version)\n\t\treturn err\n\t},\n}\n\nfunc init() {\n\trootCmd.AddCommand(showVersionCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/wsl.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// k3sCmd represents the k3s command\nvar wslCmd = &cobra.Command{\n\tUse:    \"wsl\",\n\tShort:  \"Commands for interacting with WSL\",\n\tHidden: true,\n}\n\nfunc init() {\n\trootCmd.AddCommand(wslCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/wsl_info.go",
    "content": "//go:build windows\n\n/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\n\twslutils \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/wsl-utils\"\n)\n\n// wslInfoCmd represents the `wsl info` command.\nvar wslInfoCmd = &cobra.Command{\n\tUse:   \"info\",\n\tShort: \"Determine information about the installed WSL\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\t\tlog := logrus.NewEntry(logrus.StandardLogger())\n\t\tinfo, err := wslutils.GetWSLInfo(cmd.Context(), log)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tencoder := json.NewEncoder(os.Stdout)\n\t\tencoder.SetIndent(\"\", \"  \")\n\t\treturn encoder.Encode(info)\n\t},\n}\n\nfunc init() {\n\twslCmd.AddCommand(wslInfoCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/wsl_integration_docker_linux.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/integration\"\n)\n\nvar wslIntegrationDockerViper = viper.New()\n\n// wslIntegrationDockerCmd represents the `wsl integration docker` command\nvar wslIntegrationDockerCmd = &cobra.Command{\n\tUse:   \"docker\",\n\tShort: \"Commands for managing docker config for WSL integration\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\n\t\tstate := wslIntegrationDockerViper.GetBool(\"state\")\n\t\tpluginDir := wslIntegrationDockerViper.GetString(\"plugin-dir\")\n\t\tbinDir := wslIntegrationDockerViper.GetString(\"bin-dir\")\n\t\thomeDir, err := os.UserHomeDir()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to locate home directory: %w\", err)\n\t\t}\n\n\t\tif err := integration.UpdateDockerConfig(cmd.Context(), homeDir, pluginDir, state); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := integration.RemoveObsoletePluginSymlinks(homeDir, binDir); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t},\n}\n\nfunc init() {\n\twslIntegrationDockerCmd.Flags().String(\"plugin-dir\", \"\", \"Full path to plugin directory\")\n\twslIntegrationDockerCmd.Flags().String(\"bin-dir\", \"\", \"Full path to bin directory to clean up deprecated links\")\n\twslIntegrationDockerCmd.Flags().Bool(\"state\", false, \"Desired state\")\n\tif err := wslIntegrationDockerCmd.MarkFlagRequired(\"plugin-dir\"); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\twslIntegrationDockerViper.AutomaticEnv()\n\tif err := wslIntegrationDockerViper.BindPFlags(wslIntegrationDockerCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\twslIntegrationCmd.AddCommand(wslIntegrationDockerCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/wsl_integration_linux.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"github.com/spf13/cobra\"\n)\n\n// wslIntegrationCmd represents the `wsl integration` command\nvar wslIntegrationCmd = &cobra.Command{\n\tUse:   \"integration\",\n\tShort: \"Commands for managing with WSL integration\",\n}\n\nfunc init() {\n\twslCmd.AddCommand(wslIntegrationCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/cmd/wsl_integration_state_linux.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/spf13/cobra\"\n\t\"github.com/spf13/viper\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/integration\"\n)\n\nvar wslIntegrationStateViper = viper.New()\n\n// wslIntegrationStateCmd represents the `wsl integration state` command.\nvar wslIntegrationStateCmd = &cobra.Command{\n\tUse:   \"state\",\n\tShort: \"Manage markers for WSL integration state\",\n\tLong:  \"Manage markers for Rancher Desktop WSL distro integration state\",\n\tRunE: func(cmd *cobra.Command, args []string) error {\n\t\tcmd.SilenceUsage = true\n\n\t\tmode := cmd.Flags().Lookup(\"mode\").Value.String()\n\t\tswitch mode {\n\t\tcase \"show\":\n\t\t\treturn integration.Show()\n\t\tcase \"set\":\n\t\t\tlogrus.Trace(\"Setting wsl integration state marker\")\n\t\t\treturn integration.Set()\n\t\tcase \"delete\":\n\t\t\tlogrus.Trace(\"Deleting wsl integration state marker\")\n\t\t\treturn integration.Delete()\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unknown operation %q\", mode)\n\t\t}\n\t},\n}\n\nfunc init() {\n\twslIntegrationStateCmd.Flags().Var(&enumValue{val: \"show\", allowed: []string{\"show\", \"set\", \"delete\"}}, \"mode\", \"Operation mode\")\n\tif err := wslIntegrationStateCmd.MarkFlagRequired(\"mode\"); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\twslIntegrationStateViper.AutomaticEnv()\n\tif err := wslIntegrationStateViper.BindPFlags(wslIntegrationStateCmd.Flags()); err != nil {\n\t\tlogrus.WithError(err).Fatal(\"Failed to set up flags\")\n\t}\n\twslIntegrationCmd.AddCommand(wslIntegrationStateCmd)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/go.mod",
    "content": "module github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper\n\ngo 1.25.0\n\nrequire (\n\tgithub.com/Masterminds/semver v1.5.0\n\tgithub.com/Microsoft/go-winio v0.6.2\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/go-openapi/errors v0.22.7\n\tgithub.com/go-openapi/strfmt v0.26.1\n\tgithub.com/go-openapi/swag v0.25.5\n\tgithub.com/go-openapi/validate v0.25.2\n\tgithub.com/go-swagger/go-swagger v0.33.2\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2\n\tgithub.com/moby/moby/api v1.54.0\n\tgithub.com/pkg/errors v0.9.1\n\tgithub.com/rancher-sandbox/rancher-desktop/src/go/rdctl v0.0.0-20241129182547-3cfd26786896\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/spf13/viper v1.21.0\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/sys v0.42.0\n\tgopkg.in/yaml.v3 v3.0.1\n\tk8s.io/client-go v0.35.3\n)\n\nrequire (\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fsnotify/fsnotify v1.9.0 // indirect\n\tgithub.com/go-openapi/analysis v0.24.3 // indirect\n\tgithub.com/go-openapi/inflect v0.21.5 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.22.5 // indirect\n\tgithub.com/go-openapi/jsonreference v0.21.5 // indirect\n\tgithub.com/go-openapi/loads v0.23.3 // indirect\n\tgithub.com/go-openapi/runtime v0.29.3 // indirect\n\tgithub.com/go-openapi/spec v0.22.4 // indirect\n\tgithub.com/go-openapi/swag/cmdutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/conv v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/fileutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonname v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/jsonutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/loading v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/mangling v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/netutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/stringutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/typeutils v0.25.5 // indirect\n\tgithub.com/go-openapi/swag/yamlutils v0.25.5 // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/gorilla/handlers v1.5.2 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/jessevdk/go-flags v1.6.1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/kr/text v0.2.0 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/oklog/ulid/v2 v2.1.1 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rogpeppe/go-internal v1.14.1 // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/toqueteos/webbrowser v1.2.1 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgolang.org/x/crypto v0.49.0 // indirect\n\tgolang.org/x/mod v0.34.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/sync v0.20.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/tools v0.42.0 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n)\n\ntool github.com/go-swagger/go-swagger/cmd/swagger\n"
  },
  {
    "path": "src/go/wsl-helper/go.sum",
    "content": "dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=\ngithub.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-openapi/analysis v0.24.3 h1:a1hrvMr8X0Xt69KP5uVTu5jH62DscmDifrLzNglAayk=\ngithub.com/go-openapi/analysis v0.24.3/go.mod h1:Nc+dWJ/FxZbhSow5Yh3ozg5CLJioB+XXT6MdLvJUsUw=\ngithub.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=\ngithub.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=\ngithub.com/go-openapi/inflect v0.21.5 h1:M2RCq6PPS3YbIaL7CXosGL3BbzAcmfBAT0nC3YfesZA=\ngithub.com/go-openapi/inflect v0.21.5/go.mod h1:GypUyi6bU880NYurWaEH2CmH84zFDNd+EhhmzroHmB4=\ngithub.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=\ngithub.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=\ngithub.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE=\ngithub.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw=\ngithub.com/go-openapi/loads v0.23.3 h1:g5Xap1JfwKkUnZdn+S0L3SzBDpcTIYzZ5Qaag0YDkKQ=\ngithub.com/go-openapi/loads v0.23.3/go.mod h1:NOH07zLajXo8y55hom0omlHWDVVvCwBM/S+csCK8LqA=\ngithub.com/go-openapi/runtime v0.29.3 h1:h5twGaEqxtQg40ePiYm9vFFH1q06Czd7Ot6ufdK0w/Y=\ngithub.com/go-openapi/runtime v0.29.3/go.mod h1:8A1W0/L5eyNJvKciqZtvIVQvYO66NlB7INMSZ9bw/oI=\ngithub.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ=\ngithub.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ=\ngithub.com/go-openapi/strfmt v0.26.1 h1:7zGCHji7zSYDC2tCXIusoxYQz/48jAf2q+sF6wXTG+c=\ngithub.com/go-openapi/strfmt v0.26.1/go.mod h1:Zslk5VZPOISLwmWTMBIS7oiVFem1o1EI6zULY8Uer7Y=\ngithub.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU=\ngithub.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA=\ngithub.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c=\ngithub.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0=\ngithub.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g=\ngithub.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k=\ngithub.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk=\ngithub.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc=\ngithub.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=\ngithub.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU=\ngithub.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo=\ngithub.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U=\ngithub.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo=\ngithub.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU=\ngithub.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g=\ngithub.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw=\ngithub.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY=\ngithub.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU=\ngithub.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14=\ngithub.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M=\ngithub.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII=\ngithub.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E=\ngithub.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc=\ngithub.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ=\ngithub.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1 h1:NZOrZmIb6PTv5LTFxr5/mKV/FjbUzGE7E6gLz7vFoOQ=\ngithub.com/go-openapi/testify/enable/yaml/v2 v2.4.1/go.mod h1:r7dwsujEHawapMsxA69i+XMGZrQ5tRauhLAjV/sxg3Q=\ngithub.com/go-openapi/testify/v2 v2.4.1 h1:zB34HDKj4tHwyUQHrUkpV0Q0iXQ6dUCOQtIqn8hE6Iw=\ngithub.com/go-openapi/testify/v2 v2.4.1/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=\ngithub.com/go-openapi/validate v0.25.2 h1:12NsfLAwGegqbGWr2CnvT65X/Q2USJipmJ9b7xDJZz0=\ngithub.com/go-openapi/validate v0.25.2/go.mod h1:Pgl1LpPPGFnZ+ys4/hTlDiRYQdI1ocKypgE+8Q8BLfY=\ngithub.com/go-swagger/go-swagger v0.33.2 h1:L1dxjjI29MKSWpRT0xXTOOaI3jzDWws2vR9oQmtYBZU=\ngithub.com/go-swagger/go-swagger v0.33.2/go.mod h1:1HGAWunq7SIuIPIWPHlZEDBURdzUk3BxSPr7x4dFHjc=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=\ngithub.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y=\ngithub.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2/go.mod h1:SWzULI85WerrFt3u+nIm5F9l7EvxZTKQvd0InF3nmgM=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=\ngithub.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=\ngithub.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=\ngithub.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rancher-sandbox/rancher-desktop/src/go/rdctl v0.0.0-20241129182547-3cfd26786896 h1:VbEMcZuDq9q13bAXigZxQBKymwNqp++W8COjWlFpW8o=\ngithub.com/rancher-sandbox/rancher-desktop/src/go/rdctl v0.0.0-20241129182547-3cfd26786896/go.mod h1:6MtWsvj6s8fmoKiobs+bj/61+D+OdSVZZPfjBY18tfE=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/toqueteos/webbrowser v1.2.1 h1:O7IsnnU7XQyJ1nHMRfAktUUJOAZD3aQyUVnxzhWphCg=\ngithub.com/toqueteos/webbrowser v1.2.1/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=\ngolang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=\ngotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=\nk8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg=\nk8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c=\n"
  },
  {
    "path": "src/go/wsl-helper/main.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\npackage main\n\nimport \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/cmd\"\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/certificates/certificates_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package certificates is used to enumerate the system certificate authorities\n// on Windows.\npackage certificates\n\nimport (\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"fmt\"\n\t\"unsafe\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// Entry is one enumeration result; it holds either a valid certificate or an\n// error.  They are mutually exclusive.\ntype Entry struct {\n\tCert *x509.Certificate\n\tErr  error\n}\n\n// getCertName returns a string describing the given certificate context.\nfunc getCertName(ctx *windows.CertContext) string {\n\tlength := windows.CertGetNameString(ctx, windows.CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, unsafe.Pointer(nil), nil, 0)\n\tif length == 1 {\n\t\treturn \"<error parsing certificate name>\"\n\t}\n\tbuf := make([]uint16, length)\n\t_ = windows.CertGetNameString(ctx, windows.CERT_NAME_FRIENDLY_DISPLAY_TYPE, 0, unsafe.Pointer(nil), unsafe.SliceData(buf), length)\n\treturn windows.UTF16ToString(buf)\n}\n\n// GetSystemCertificates returns the Windows system certificates from the given\n// certificate store.  Typical store names are strings like \"CA\", \"Root\", \"My\".\nfunc GetSystemCertificates(storeName string) (<-chan Entry, error) {\n\tstoreNameBytes, err := windows.UTF16PtrFromString(storeName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstore, err := windows.CertOpenSystemStore(windows.Handle(0), storeNameBytes)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to open store %q: %w\", storeName, err)\n\t}\n\tch := make(chan Entry)\n\tgo func() {\n\t\tdefer close(ch)\n\t\tdefer func() { _ = windows.CertCloseStore(store, 0) }()\n\t\tvar certCtx *windows.CertContext\n\t\tvar err error\n\t\tfor {\n\t\t\tcertCtx, err = windows.CertEnumCertificatesInStore(store, certCtx)\n\t\t\tif err != nil {\n\t\t\t\tswitch {\n\t\t\t\tcase errors.Is(err, windows.ERROR_NO_MORE_FILES):\n\t\t\t\tcase errors.Is(err, windows.Errno(windows.CRYPT_E_NOT_FOUND)):\n\t\t\t\tdefault:\n\t\t\t\t\tch <- Entry{Err: fmt.Errorf(\"error enumerating certificate in %q: %w\", storeName, err)}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// Make a copy of the encoded cert, because the parsed cert may have\n\t\t\t// references to the memory (that isn't owned by the GC) and we'll return\n\t\t\t// it in a channel, so HeapFree() might get called on it before it's used.\n\t\t\t// See #6295 / #6307.\n\t\t\tcertData := make([]byte, certCtx.Length)\n\t\t\tcopy(certData, unsafe.Slice(certCtx.EncodedCert, certCtx.Length))\n\t\t\tcert, err := x509.ParseCertificate(certData)\n\t\t\tif err != nil {\n\t\t\t\t// Skip invalid certs\n\t\t\t\tlogrus.Tracef(\"Skipping invalid certificate %q in %q: %s\", getCertName(certCtx), storeName, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlogrus.Tracef(\"Found cert %q in %q\", getCertName(certCtx), storeName)\n\t\t\tch <- Entry{Cert: cert}\n\t\t}\n\t}()\n\treturn ch, nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/certificates/certificates_windows_test.go",
    "content": "package certificates_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/x509\"\n\t\"encoding/pem\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/certificates\"\n)\n\n// Test that we don't use memory that we don't own\nfunc TestGetSystemCertificates_UseAfterFree(t *testing.T) {\n\tvar certs []*x509.Certificate\n\tch, err := certificates.GetSystemCertificates(\"CA\")\n\trequire.NoError(t, err, \"failed to get CA certificates\")\n\tfor entry := range ch {\n\t\tif assert.NoError(t, err, entry.Err) {\n\t\t\tcerts = append(certs, entry.Cert)\n\t\t}\n\t}\n\tch, err = certificates.GetSystemCertificates(\"ROOT\")\n\trequire.NoError(t, err, \"failed to get ROOT certificates\")\n\tfor entry := range ch {\n\t\tif assert.NoError(t, err, entry.Err) {\n\t\t\tcerts = append(certs, entry.Cert)\n\t\t}\n\t}\n\n\t// By this point, both channels have been closed, which also means we have\n\t// closed both cert stores.\n\tfor _, cert := range certs {\n\t\tbuf := bytes.Buffer{}\n\t\tblock := &pem.Block{Type: \"CERTIFICATE\", Bytes: cert.Raw}\n\t\terr = pem.Encode(&buf, block)\n\t\tif assert.NoError(t, err, \"Failed to encode certificate\") {\n\t\t\t// Look for invalid certificates:\n\t\t\t// - A line of all A (nulls)\n\t\t\t// - A line with 0xFEEEFEEE (HeapAlloc freed marker)\n\t\t\toutput := buf.String()\n\t\t\tassert.NotContains(t, output, \"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\", \"encoded cert contains nulls\")\n\t\t\tassert.NotContains(t, output, \"7v7u/u7+7v7u/u7+7v7u/u7+7v7u/u7+7v7u/u7+7v7u/u7+7v7u/u7+7v7u/u7+\", \"encoded cert contains FEEEFEEE\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/defaults.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage dockerproxy\n\n// DefaultPort is the default (vsock) port we're listening on.\nconst DefaultPort = 23752375\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/generate.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage dockerproxy\n\nimport (\n\t_ \"github.com/go-swagger/go-swagger\"\n)\n\n//go:generate -command swagger go tool swagger\n//go:generate swagger generate server --quiet --skip-validation --config-file swagger-configuration.yaml --server-package models --spec swagger-modified.yaml\n\nfunc init() {\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/models/doc.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package models contains the auto-generated OpenAPI models.\npackage models\n\n// These imports exist so that dependabot is aware we use them.\nimport (\n\t_ \"github.com/go-openapi/errors\"\n\t_ \"github.com/go-openapi/strfmt\" // spellcheck-ignore-line\n\t_ \"github.com/go-openapi/swag\"\n\t_ \"github.com/go-openapi/validate\"\n\t_ \"github.com/moby/moby/api/types/container\"\n)\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/containers_create_linux.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"sync\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/google/uuid\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/models\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n)\n\n// For Linux (non-rancher-desktop WSL2 containers), we need to do a little more\n// work.  The mounts are defined in /containers/create, but used in\n// /containers/{id}/start instead; additionally, start may be called multiple\n// times (or /containers/{id}/restart may be called).\n// To handle this, we need to:\n// - in POST /containers/create:\n//   - on request, modify the bindings to point to temporary directories.\n//   - on response, record the container id + bind mappings.\n// - in POST /containers/{id}/start|restart|etc\n//   - on request, reconstruct the bind mappings as needed.\n//   - on response, remove those bind mappings.\n// - in DELETE /containers/{id}\n//   - remove the recording binding information\n// Note that all persisted info needs to live on disk; it's possible to run\n// containers while restarting the docker proxy (or indeed the machine).\n\n// mountRoot is where we can keep our temporary mounts, relative to the WSL\n// mount root (typically /mnt/wsl).\nconst mountRoot = \"rancher-desktop/run/docker-mounts\"\n\n// contextKey is the key used to locate the bind manager in the request/response\n// context.  This only lasts for a single request/response pair.\nvar contextKey = struct{}{}\n\n// bindManager manages the binding data (but does not do binding itself)\ntype bindManager struct {\n\t// mountRoot is where we can keep our temporary mounts.\n\tmountRoot string\n\n\t// Recorded entries, keyed by the random mount point string (the leaf name\n\t// of the bind host location, as reported to dockerd).  Each entry is only\n\t// used by one container; multiple entries may map to the same host path.\n\tentries map[string]bindManagerEntry\n\n\t// Name of the file we use for persisting data.\n\tstatePath string\n\n\t// Mutex for managing concurrency for the bindManager.\n\tsync.RWMutex\n}\n\n// bindManagerEntry is one entry in the bind manager.  If all the fields are\n// empty, then the bind is incomplete (the container create failed) and it\n// should not be used.\ntype bindManagerEntry struct {\n\tContainerID string `json:\"ContainerId\"`\n\tHostPath    string\n}\n\nfunc newBindManager() (*bindManager, error) {\n\tstatePath, err := xdg.StateFile(\"rancher-desktop/docker-binds.json\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmountPoint, err := platform.GetWSLMountPoint()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresult := bindManager{\n\t\tmountRoot: path.Join(mountPoint, mountRoot),\n\t\tentries:   make(map[string]bindManagerEntry),\n\t\tstatePath: statePath,\n\t}\n\terr = result.load()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &result, nil\n}\n\n// load the persisted bind manager data; this should only be called from\n// newBindManager().\nfunc (b *bindManager) load() error {\n\tfile, err := os.Open(b.statePath)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tb.entries = make(map[string]bindManagerEntry)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"error opening state file %s: %w\", b.statePath, err)\n\t}\n\tdefer file.Close()\n\terr = json.NewDecoder(file).Decode(&b.entries)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error reading state file %s: %w\", b.statePath, err)\n\t}\n\treturn nil\n}\n\n// persist the bind manager data; this should be called with the lock held.\nfunc (b *bindManager) persist() error {\n\tfile, err := os.CreateTemp(path.Dir(b.statePath), \"docker-binds.*.json\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error opening state file %s for writing: %w\", b.statePath, err)\n\t}\n\tdefer file.Close()\n\terr = json.NewEncoder(file).Encode(b.entries)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error writing state file %s: %w\", b.statePath, err)\n\t}\n\tif err = file.Sync(); err != nil {\n\t\treturn fmt.Errorf(\"error syncing state file %s: %w\", b.statePath, err)\n\t}\n\tif err = file.Close(); err != nil {\n\t\treturn fmt.Errorf(\"error closing state file %s: %w\", b.statePath, err)\n\t}\n\tif err := os.Rename(file.Name(), b.statePath); err != nil {\n\t\treturn fmt.Errorf(\"error committing state file %s: %w\", b.statePath, err)\n\t}\n\n\tlogrus.WithField(\"path\", b.statePath).Debug(\"persisted mount state\")\n\treturn nil\n}\n\n// makeMount creates a new, unused mount point.\nfunc (b *bindManager) makeMount() string {\n\tb.Lock()\n\tdefer b.Unlock()\n\tfor {\n\t\tentry := uuid.New().String()\n\t\t_, ok := b.entries[entry]\n\t\tif ok {\n\t\t\tcontinue\n\t\t}\n\t\tb.entries[entry] = bindManagerEntry{}\n\t\treturn entry\n\t}\n}\n\n// prepareMountPath creates target directory or file, as mount point\nfunc (b *bindManager) prepareMountPath(target, bindKey string) error {\n\tmountPath := path.Join(b.mountRoot, bindKey)\n\thostPathStat, err := os.Stat(target)\n\tif os.IsNotExist(err) {\n\t\treturn fmt.Errorf(\"host path (%s) doesn't exist: %w\", target, err)\n\t}\n\tvar pathToCreate string\n\tmountingFile := false\n\tif hostPathStat.IsDir() {\n\t\tpathToCreate = mountPath\n\t} else {\n\t\tpathToCreate = b.mountRoot\n\t\tmountingFile = true\n\t}\n\terr = os.MkdirAll(pathToCreate, 0o700)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not create bind mount directory %s: %w\", mountPath, err)\n\t}\n\tif mountingFile {\n\t\t// We're mounting a file; create a file to be mounted over.\n\t\tfd, err := os.Create(mountPath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not create volume mount file %s: %w\", mountPath, err)\n\t\t}\n\t\tfd.Close()\n\t}\n\treturn nil\n}\n\n// containersCreateRequestBody describes the contents of a /containers/create request.\ntype containersCreateRequestBody struct {\n\tmodels.ContainerConfig\n\tHostConfig       models.HostConfig\n\tNetworkingConfig models.NetworkingConfig\n}\n\n// munge incoming request for POST /containers/create\nfunc (b *bindManager) mungeContainersCreateRequest(req *http.Request, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\tbody := containersCreateRequestBody{}\n\terr := readRequestBodyJSON(req, &body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.WithField(\"body\", fmt.Sprintf(\"%+v\", body)).Trace(\"read body\")\n\n\t// The list of bindings\n\tbinds := make(map[string]string)\n\n\tmodified := false\n\tfor bindIndex, bind := range body.HostConfig.Binds {\n\t\tlogrus.WithField(fmt.Sprintf(\"bind %d\", bindIndex), bind).Trace(\"got bind\")\n\t\thost, container, options, isPath := platform.ParseBindString(bind)\n\t\tif !isPath {\n\t\t\tcontinue\n\t\t}\n\n\t\tbindKey := b.makeMount()\n\t\tbinds[bindKey] = host\n\t\thost = path.Join(b.mountRoot, bindKey)\n\t\tmodified = true\n\t\tif options == \"\" {\n\t\t\tbody.HostConfig.Binds[bindIndex] = fmt.Sprintf(\"%s:%s\", host, container)\n\t\t} else {\n\t\t\tbody.HostConfig.Binds[bindIndex] = fmt.Sprintf(\"%s:%s:%s\", host, container, options)\n\t\t}\n\t}\n\n\tfor _, mount := range body.HostConfig.Mounts {\n\t\tlogEntry := logrus.WithField(\"mount\", fmt.Sprintf(\"%+v\", mount))\n\t\tif mount.Type.MountType != \"bind\" {\n\t\t\tlogEntry.Trace(\"skipping mount of unsupported type\")\n\t\t\tcontinue\n\t\t}\n\t\tif !path.IsAbs(mount.Source) {\n\t\t\tlogEntry.Trace(\"skipping non-host mount\")\n\t\t\tcontinue\n\t\t}\n\n\t\tbindKey := b.makeMount()\n\t\ttarget := mount.Source\n\t\tbinds[bindKey] = target\n\t\tmount.Source = path.Join(b.mountRoot, bindKey)\n\t\t// Unlike .HostConfig.Binds, the source for .HostConfig.Mounts must\n\t\t// exist at container create time.\n\t\terr := b.prepareMountPath(target, bindKey)\n\t\tif err != nil {\n\t\t\tlogEntry.WithError(err).Error(\"could not prepare mount volume\")\n\t\t\treturn err\n\t\t}\n\t\tlogEntry.WithField(\"bind key\", bindKey).Trace(\"got mount\")\n\t\tmodified = true\n\t}\n\n\tif !modified {\n\t\treturn nil\n\t}\n\n\t(*contextValue)[contextKey] = &binds\n\tbuf, err := json.Marshal(&body)\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"could not re-serialize modified body\")\n\t\treturn err\n\t}\n\treq.Body = io.NopCloser(bytes.NewBuffer(buf))\n\treq.ContentLength = int64(len(buf))\n\treq.Header.Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(buf)))\n\tlogrus.WithField(\"binds\", fmt.Sprintf(\"%+v\", binds)).Debug(\"modified binds\")\n\n\treturn nil\n}\n\n// containersCreateResponseBody describes the contents of a /containers/create response.\ntype containersCreateResponseBody struct {\n\tID       string `json:\"Id\"`\n\tWarnings []string\n}\n\n// munge outgoing response for POST /containers/create\nfunc (b *bindManager) mungeContainersCreateResponse(resp *http.Response, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\tbinds, ok := (*contextValue)[contextKey].(*map[string]string)\n\tif !ok {\n\t\t// No binds, meaning either the user didn't specify any, or we didn't need to remap.\n\t\treturn nil\n\t}\n\n\tif resp.StatusCode != http.StatusCreated {\n\t\t// If the response wasn't a success; just clean up the bind mappings.\n\t\tb.Lock()\n\t\tfor key := range *binds {\n\t\t\tdelete(b.entries, key)\n\t\t}\n\t\tb.Unlock()\n\t\t// No need to call persist() here, since empty mounts are not written.\n\t\treturn nil\n\t}\n\n\tvar body containersCreateResponseBody\n\terr := readResponseBodyJSON(resp, &body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tb.Lock()\n\tfor mountID, hostPath := range *binds {\n\t\tb.entries[mountID] = bindManagerEntry{\n\t\t\tContainerID: body.ID,\n\t\t\tHostPath:    hostPath,\n\t\t}\n\t}\n\terr = b.persist()\n\tb.Unlock()\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"error writing state file\")\n\t\treturn fmt.Errorf(\"could not write state: %w\", err)\n\t}\n\n\tlogrus.WithField(\"binds\", binds).WithField(\"body\", body).Debug(\"got stored binds\")\n\treturn nil\n}\n\n// munge incoming request to activate the mount, on\n// POST /containers/{id}/start\n// POST /containers/{id}/restart\nfunc (b *bindManager) mungeContainersStartRequest(req *http.Request, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\t// Look up all the mappings this container needs\n\tmapping := make(map[string]string)\n\tb.RLock()\n\tfor key, data := range b.entries {\n\t\tif data.ContainerID == templates[\"id\"] {\n\t\t\tmapping[key] = data.HostPath\n\t\t}\n\t}\n\tb.RUnlock()\n\tif len(mapping) < 1 {\n\t\treturn nil\n\t}\n\n\t// Do bind mounts\n\tfor bindKey, target := range mapping {\n\t\tmountPath := path.Join(b.mountRoot, bindKey)\n\t\tlogEntry := logrus.WithFields(logrus.Fields{\n\t\t\t\"container\": templates[\"id\"],\n\t\t\t\"bind\":      mountPath,\n\t\t\t\"target\":    target,\n\t\t})\n\t\terr := b.prepareMountPath(target, bindKey)\n\t\tif err != nil {\n\t\t\tlogEntry.WithError(err).Error(\"could not prepare mount volume\")\n\t\t\treturn err\n\t\t}\n\t\terr = unix.Mount(target, mountPath, \"none\", unix.MS_BIND|unix.MS_REC, \"\")\n\t\tif err != nil {\n\t\t\tlogEntry.WithError(err).Error(\"could not perform bind mount\")\n\t\t\treturn fmt.Errorf(\"could not mount volume %s: %w\", target, err)\n\t\t}\n\t\tlogEntry.Debug(\"created bind mount\")\n\t}\n\n\t(*contextValue)[contextKey] = &mapping\n\n\treturn nil\n}\n\n// munge outgoing response to deactivate the mount, on\n// POST /containers/{id}/start\n// POST /containers/{id}/restart\nfunc (b *bindManager) mungeContainersStartResponse(req *http.Response, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\tbinds, ok := (*contextValue)[contextKey].(*map[string]string)\n\tif !ok {\n\t\t// No binds, meaning we didn't do any mounting; nothing to undo here.\n\t\treturn nil\n\t}\n\n\tfor bindKey := range *binds {\n\t\tmountDir := path.Join(b.mountRoot, bindKey)\n\t\tlogEntry := logrus.WithFields(logrus.Fields{\n\t\t\t\"container\": templates[\"id\"],\n\t\t\t\"bind\":      mountDir,\n\t\t})\n\t\terr := unix.Unmount(mountDir, unix.MNT_DETACH|unix.UMOUNT_NOFOLLOW)\n\t\tif err != nil {\n\t\t\tlogEntry.WithError(err).Error(\"failed to unmount\")\n\t\t\treturn fmt.Errorf(\"could not unmount bind mount %s: %w\", mountDir, err)\n\t\t}\n\t\terr = os.Remove(mountDir)\n\t\tif err != nil {\n\t\t\tlogEntry.WithError(err).Error(\"failed to remove bind directory\")\n\t\t\treturn fmt.Errorf(\"could not remove bind mount directory %s: %w\", mountDir, err)\n\t\t}\n\t\tlogEntry.Debug(\"removed bind mount\")\n\t}\n\n\treturn nil\n}\n\n// DELETE /containers/{id}\nfunc (b *bindManager) mungeContainersDeleteResponse(resp *http.Response, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\tlogEntry := logrus.WithField(\"templates\", templates)\n\tif resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {\n\t\tlogEntry.WithField(\"status-code\", resp.StatusCode).Debug(\"unexpected status code\")\n\t\treturn nil\n\t}\n\tb.Lock()\n\tdefer b.Unlock()\n\n\tvar toDelete []string\n\tfor key, data := range b.entries {\n\t\tif data.ContainerID == templates[\"id\"] {\n\t\t\ttoDelete = append(toDelete, key)\n\t\t}\n\t}\n\tfor _, key := range toDelete {\n\t\tdelete(b.entries, key)\n\t}\n\tif err := b.persist(); err != nil {\n\t\tlogrus.WithError(err).Error(\"error writing state file\")\n\t\treturn fmt.Errorf(\"could not write state: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc init() {\n\tb, err := newBindManager()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdockerproxy.RegisterRequestMunger(http.MethodPost, \"/containers/create\", b.mungeContainersCreateRequest)\n\tdockerproxy.RegisterResponseMunger(http.MethodPost, \"/containers/create\", b.mungeContainersCreateResponse)\n\tdockerproxy.RegisterRequestMunger(http.MethodPost, \"/containers/{id}/start\", b.mungeContainersStartRequest)\n\tdockerproxy.RegisterRequestMunger(http.MethodPost, \"/containers/{id}/restart\", b.mungeContainersStartRequest)\n\tdockerproxy.RegisterResponseMunger(http.MethodPost, \"/containers/{id}/start\", b.mungeContainersStartResponse)\n\tdockerproxy.RegisterResponseMunger(http.MethodPost, \"/containers/{id}/restart\", b.mungeContainersStartResponse)\n\tdockerproxy.RegisterResponseMunger(http.MethodDelete, \"/containers/{id}\", b.mungeContainersDeleteResponse)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/containers_create_linux_test.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/models\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n)\n\nfunc TestNewBindManager(t *testing.T) {\n\tbindManager, err := newBindManager()\n\t// We're not testing loading the state here; if it happens to fail, we'll\n\t// just have to skip the test.\n\tif err != nil {\n\t\tt.Skipf(\"skipping test, got error %s\", err)\n\t} else {\n\t\tmountPoint, err := platform.GetWSLMountPoint()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, path.Join(mountPoint, mountRoot), bindManager.mountRoot)\n\t\tassert.Equal(t, \"docker-binds.json\", path.Base(bindManager.statePath))\n\t}\n}\n\nfunc TestBindManagerPersist(t *testing.T) {\n\toriginal := &bindManager{\n\t\tmountRoot: t.TempDir(),\n\t\tstatePath: path.Join(t.TempDir(), \"state.json\"),\n\t}\n\t// Loading a file that doesn't exist should succeed\n\terr := original.load()\n\trequire.NoError(t, err)\n\tassert.Empty(t, original.entries)\n\tassert.NoFileExists(t, original.statePath)\n\toriginal.entries = map[string]bindManagerEntry{\n\t\t\"foo\": {\n\t\t\tContainerID: \"hello\",\n\t\t\tHostPath:    \"world\",\n\t\t},\n\t}\n\terr = original.persist()\n\trequire.NoError(t, err)\n\tassert.FileExists(t, original.statePath)\n\tloaded := &bindManager{\n\t\tmountRoot: original.mountRoot,\n\t\tstatePath: original.statePath,\n\t}\n\terr = loaded.load()\n\trequire.NoError(t, err)\n\tassert.Equal(t, original, loaded)\n}\n\nfunc TestContainersCreate(t *testing.T) {\n\tt.Run(\"bind\", func(t *testing.T) {\n\t\t// Create a bind manager\n\t\tbindManager := &bindManager{\n\t\t\tmountRoot: t.TempDir(),\n\t\t\tentries:   make(map[string]bindManagerEntry),\n\t\t\tstatePath: path.Join(t.TempDir(), \"state.json\"),\n\t\t}\n\n\t\t// Emit the request\n\t\tctx := context.Background()\n\t\tbuf, err := json.Marshal(&containersCreateRequestBody{\n\t\t\tHostConfig: models.HostConfig{\n\t\t\t\tBinds: []string{\n\t\t\t\t\t\"/foo\",\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\treq, err := http.NewRequestWithContext(\n\t\t\tctx,\n\t\t\thttp.MethodPost,\n\t\t\t\"http://nowhere.invalid/\",\n\t\t\tio.NopCloser(bytes.NewReader(buf)))\n\t\trequire.NoError(t, err)\n\t\tcontextValue := &dockerproxy.RequestContextValue{}\n\t\ttemplates := make(map[string]string)\n\t\terr = bindManager.mungeContainersCreateRequest(req, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\t// Handle the response\n\t\tbuf, err = json.Marshal(&containersCreateResponseBody{\n\t\t\tID: \"hello\",\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tresp := &http.Response{\n\t\t\tStatusCode:    http.StatusCreated,\n\t\t\tBody:          io.NopCloser(bytes.NewBuffer(buf)),\n\t\t\tContentLength: int64(len(buf)),\n\t\t\tRequest:       req,\n\t\t}\n\t\terr = bindManager.mungeContainersCreateResponse(resp, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\t// Read the request body\n\t\tvar requestBody containersCreateRequestBody\n\t\terr = readRequestBodyJSON(req, &requestBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, requestBody.HostConfig.Binds, 1)\n\n\t\t// Read the response body\n\t\tvar responseBody containersCreateResponseBody\n\t\terr = readResponseBodyJSON(resp, &responseBody)\n\t\tassert.NoError(t, err)\n\n\t\t// Assert state\n\t\tassert.Len(t, bindManager.entries, 1)\n\t\tvar mountID string\n\t\tvar entry bindManagerEntry\n\t\tfor mountID, entry = range bindManager.entries {\n\t\t}\n\t\tassert.NotEmpty(t, mountID)\n\t\tassert.Equal(t, \"hello\", entry.ContainerID)\n\t\tassert.Equal(t, \"hello\", responseBody.ID)\n\t\texpectedMount := path.Join(bindManager.mountRoot, mountID)\n\t\texpectedBind := fmt.Sprintf(\"%s:/foo\", expectedMount)\n\t\tassert.Equal(t, expectedBind, requestBody.HostConfig.Binds[0])\n\t})\n\n\tt.Run(\"mount\", func(t *testing.T) {\n\t\t// Create a bind manager\n\t\tbindManager := &bindManager{\n\t\t\tmountRoot: t.TempDir(),\n\t\t\tentries:   make(map[string]bindManagerEntry),\n\t\t\tstatePath: path.Join(t.TempDir(), \"state.json\"),\n\t\t}\n\n\t\t// Emit the request\n\t\tctx := context.Background()\n\t\tbindPath := t.TempDir()\n\t\tmount := models.Mount{\n\t\t\tConsistency: \"cached\",\n\t\t\tSource:      bindPath,\n\t\t\tTarget:      \"/host\",\n\t\t\tType:        struct{ models.MountType }{\"bind\"},\n\t\t}\n\t\tbuf, err := json.Marshal(&containersCreateRequestBody{\n\t\t\tHostConfig: models.HostConfig{\n\t\t\t\tMounts: []*models.Mount{&mount},\n\t\t\t},\n\t\t})\n\t\trequire.NoError(t, err)\n\t\treq, err := http.NewRequestWithContext(\n\t\t\tctx,\n\t\t\thttp.MethodPost,\n\t\t\t\"http://nowhere.invalid/\",\n\t\t\tio.NopCloser(bytes.NewReader(buf)))\n\t\trequire.NoError(t, err)\n\t\tcontextValue := &dockerproxy.RequestContextValue{}\n\t\ttemplates := make(map[string]string)\n\t\terr = bindManager.mungeContainersCreateRequest(req, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\t// Handle the response\n\t\tbuf, err = json.Marshal(&containersCreateResponseBody{\n\t\t\tID: \"hello\",\n\t\t})\n\t\trequire.NoError(t, err)\n\t\tresp := &http.Response{\n\t\t\tStatusCode:    http.StatusCreated,\n\t\t\tBody:          io.NopCloser(bytes.NewBuffer(buf)),\n\t\t\tContentLength: int64(len(buf)),\n\t\t\tRequest:       req,\n\t\t}\n\t\terr = bindManager.mungeContainersCreateResponse(resp, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\t// Read the request body\n\t\tvar requestBody containersCreateRequestBody\n\t\terr = readRequestBodyJSON(req, &requestBody)\n\t\tassert.NoError(t, err)\n\t\tassert.Len(t, requestBody.HostConfig.Mounts, 1)\n\n\t\t// Read the response body\n\t\tvar responseBody containersCreateResponseBody\n\t\terr = readResponseBodyJSON(resp, &responseBody)\n\t\tassert.NoError(t, err)\n\n\t\t// Assert state\n\t\tassert.Len(t, bindManager.entries, 1)\n\t\tvar mountID string\n\t\tvar entry bindManagerEntry\n\t\tfor mountID, entry = range bindManager.entries {\n\t\t}\n\t\tassert.NotEmpty(t, mountID)\n\t\tassert.Equal(t, \"hello\", entry.ContainerID)\n\t\tassert.ElementsMatch(t, []*models.Mount{\n\t\t\t{\n\t\t\t\tConsistency: \"cached\",\n\t\t\t\tSource:      path.Join(bindManager.mountRoot, mountID),\n\t\t\t\tTarget:      \"/host\",\n\t\t\t\tType:        struct{ models.MountType }{\"bind\"},\n\t\t\t},\n\t\t}, requestBody.HostConfig.Mounts)\n\t\tassert.Equal(t, \"hello\", responseBody.ID)\n\t})\n}\n\nfunc TestContainersStart(t *testing.T) {\n\tif os.Geteuid() != 0 {\n\t\tt.Skip(\"test requires privileges (use go test -exec sudo)\")\n\t}\n\n\thostPath := t.TempDir()\n\tbindManager := &bindManager{\n\t\tmountRoot: t.TempDir(),\n\t\tentries: map[string]bindManagerEntry{\n\t\t\t\"mount-id\": {\n\t\t\t\tContainerID: \"container-id\",\n\t\t\t\tHostPath:    hostPath,\n\t\t\t},\n\t\t},\n\t\tstatePath: path.Join(t.TempDir(), \"state.json\"),\n\t}\n\treq, err := http.NewRequestWithContext(\n\t\tcontext.Background(),\n\t\thttp.MethodPost,\n\t\t\"http://nowhere.invalid/\",\n\t\tio.NopCloser(bytes.NewReader([]byte{})))\n\trequire.NoError(t, err)\n\tresp := &http.Response{\n\t\tStatusCode:    http.StatusOK,\n\t\tBody:          io.NopCloser(bytes.NewBuffer([]byte{})),\n\t\tContentLength: int64(0),\n\t\tRequest:       req,\n\t}\n\tcontextValue := &dockerproxy.RequestContextValue{}\n\ttemplates := map[string]string{\n\t\t\"id\": \"container-id\",\n\t}\n\terr = bindManager.mungeContainersStartRequest(req, contextValue, templates)\n\tassert.NoError(t, err)\n\tassert.DirExists(t, path.Join(bindManager.mountRoot, \"mount-id\"))\n\n\t// getBindMounts returns a map of bind mount directory -> underlying path\n\t// Note that this may also return items that are not bind mounts.\n\tgetBindMounts := func() (map[string]string, error) {\n\t\tmountBuf, err := os.ReadFile(\"/proc/self/mountinfo\")\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not read /proc/self/mountinfo: %w\", err)\n\t\t}\n\n\t\tresult := make(map[string]string)\n\t\tfor _, line := range strings.Split(string(mountBuf), \"\\n\") {\n\t\t\tfields := strings.Fields(line)\n\t\t\tif len(fields) < 5 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsourcePath := fields[3]\n\t\t\tdestPath := fields[4]\n\t\t\tresult[destPath] = sourcePath\n\t\t}\n\t\treturn result, nil\n\t}\n\n\tmounts, err := getBindMounts()\n\tif assert.NoError(t, err) {\n\t\tassert.Contains(t, mounts, path.Join(bindManager.mountRoot, \"mount-id\"))\n\t\tassert.Equal(t, hostPath, mounts[path.Join(bindManager.mountRoot, \"mount-id\")])\n\t}\n\n\terr = bindManager.mungeContainersStartResponse(resp, contextValue, templates)\n\tassert.NoError(t, err)\n\n\t// Check that the bind mount went away\n\tmounts, err = getBindMounts()\n\tif assert.NoError(t, err) {\n\t\tassert.NotContains(t, mounts, path.Join(bindManager.mountRoot, \"mount-id\"))\n\t}\n}\n\nfunc TestContainerDelete(t *testing.T) {\n\thostPath := t.TempDir()\n\tbindManager := &bindManager{\n\t\tmountRoot: t.TempDir(),\n\t\tentries: map[string]bindManagerEntry{\n\t\t\t\"mount-id\": {\n\t\t\t\tContainerID: \"container-id\",\n\t\t\t\tHostPath:    hostPath,\n\t\t\t},\n\t\t},\n\t\tstatePath: path.Join(t.TempDir(), \"state.json\"),\n\t}\n\n\treq, err := http.NewRequestWithContext(\n\t\tcontext.Background(),\n\t\thttp.MethodDelete,\n\t\t\"http://nowhere.invalid/\",\n\t\tio.NopCloser(bytes.NewReader([]byte{})))\n\trequire.NoError(t, err)\n\tresp := &http.Response{\n\t\tStatusCode:    http.StatusNoContent,\n\t\tBody:          io.NopCloser(bytes.NewBuffer([]byte{})),\n\t\tContentLength: int64(0),\n\t\tRequest:       req,\n\t}\n\tcontextValue := &dockerproxy.RequestContextValue{}\n\ttemplates := map[string]string{\n\t\t\"id\": \"container-id\",\n\t}\n\n\terr = bindManager.mungeContainersDeleteResponse(resp, contextValue, templates)\n\tassert.NoError(t, err)\n\tassert.Empty(t, bindManager.entries)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/containers_create_windows.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/models\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n)\n\ntype containersCreateBody struct {\n\tmodels.ContainerConfig\n\tHostConfig       models.HostConfig\n\tNetworkingConfig models.NetworkingConfig\n}\n\n// munge POST /containers/create to use WSL paths\nfunc mungeContainersCreate(req *http.Request, contextValue *dockerproxy.RequestContextValue, templates map[string]string) error {\n\tbody := containersCreateBody{}\n\terr := readRequestBodyJSON(req, &body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlogrus.WithField(\"body\", fmt.Sprintf(\"%+v\", body)).Debug(\"read body\")\n\n\tmodified := false\n\tfor bindIndex, bind := range body.HostConfig.Binds {\n\t\tlogrus.WithField(fmt.Sprintf(\"bind %d\", bindIndex), bind).Debug(\"got bind\")\n\t\thost, container, options, isPath := platform.ParseBindString(bind)\n\t\tif isPath {\n\t\t\ttranslated, err := platform.TranslatePathFromClient(req.Context(), host)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"could not translate bind path %s: %w\", host, err)\n\t\t\t}\n\t\t\thost = translated\n\t\t\tmodified = true\n\t\t}\n\t\tif options == \"\" {\n\t\t\tbody.HostConfig.Binds[bindIndex] = fmt.Sprintf(\"%s:%s\", host, container)\n\t\t} else {\n\t\t\tbody.HostConfig.Binds[bindIndex] = fmt.Sprintf(\"%s:%s:%s\", host, container, options)\n\t\t}\n\t}\n\n\tfor _, mount := range body.HostConfig.Mounts {\n\t\tif mount == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif mount.Type.MountType == \"npipe\" {\n\t\t\tlogrus.WithField(\"mount\", mount).Warn(\"named pipes are not supported\")\n\t\t}\n\t\tif mount.Type.MountType != \"bind\" {\n\t\t\t// We only support bind mounts for now\n\t\t\tcontinue\n\t\t}\n\t\tif !platform.IsAbsolutePath(mount.Source) {\n\t\t\tcontinue\n\t\t}\n\t\ttranslated, err := platform.TranslatePathFromClient(req.Context(), mount.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"could not translate mount path %s: %w\", mount.Source, err)\n\t\t}\n\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\"mount\":      mount,\n\t\t\t\"translated\": translated,\n\t\t}).Trace(\"munging mount\")\n\t\tmount.Source = translated\n\t\tmodified = true\n\t}\n\n\tif !modified {\n\t\treturn nil\n\t}\n\n\tbuf, err := json.Marshal(body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not re-marshal parameters: %w\", err)\n\t}\n\treq.Body = io.NopCloser(bytes.NewBuffer(buf))\n\treq.ContentLength = int64(len(buf))\n\treq.Header.Set(\"Content-Length\", fmt.Sprintf(\"%d\", len(buf)))\n\n\treturn nil\n}\n\nfunc init() {\n\tdockerproxy.RegisterRequestMunger(http.MethodPost, \"/containers/create\", mungeContainersCreate)\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/containers_create_windows_test.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/models\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n)\n\nfunc TestContainersCreate(t *testing.T) {\n\tt.Run(\"bind\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tbindPath := t.TempDir()\n\t\tbody := containersCreateBody{\n\t\t\tHostConfig: models.HostConfig{\n\t\t\t\tBinds: []string{\n\t\t\t\t\tfmt.Sprintf(\"%s:/host\", bindPath),\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tbuf, err := json.Marshal(&body)\n\t\trequire.NoError(t, err)\n\t\treq, err := http.NewRequestWithContext(\n\t\t\tctx,\n\t\t\thttp.MethodPost,\n\t\t\t\"http://nowhere.invalid/\",\n\t\t\tio.NopCloser(bytes.NewReader(buf)))\n\t\trequire.NoError(t, err)\n\t\tcontextValue := &dockerproxy.RequestContextValue{}\n\t\ttemplates := make(map[string]string)\n\t\terr = mungeContainersCreate(req, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\terr = readRequestBodyJSON(req, &body)\n\t\tassert.NoError(t, err)\n\t\tslashPath, err := platform.TranslatePathFromClient(t.Context(), bindPath)\n\t\tassert.NoError(t, err)\n\t\texpectedBind := fmt.Sprintf(\"%s:/host\", slashPath)\n\t\tassert.Equal(t, []string{expectedBind}, body.HostConfig.Binds)\n\t})\n\n\tt.Run(\"mount\", func(t *testing.T) {\n\t\tctx := context.Background()\n\t\tbindPath := t.TempDir()\n\t\tmount := models.Mount{\n\t\t\tConsistency: \"cached\",\n\t\t\tSource:      bindPath,\n\t\t\tTarget:      \"/host\",\n\t\t\tType:        struct{ models.MountType }{\"bind\"},\n\t\t}\n\t\tbody := containersCreateBody{\n\t\t\tHostConfig: models.HostConfig{\n\t\t\t\tMounts: []*models.Mount{&mount},\n\t\t\t},\n\t\t}\n\t\tbuf, err := json.Marshal(&body)\n\t\trequire.NoError(t, err)\n\t\treq, err := http.NewRequestWithContext(\n\t\t\tctx,\n\t\t\thttp.MethodPost,\n\t\t\t\"http://nowhere.invalid/\",\n\t\t\tio.NopCloser(bytes.NewReader(buf)))\n\t\trequire.NoError(t, err)\n\t\tcontextValue := &dockerproxy.RequestContextValue{}\n\t\ttemplates := make(map[string]string)\n\t\terr = mungeContainersCreate(req, contextValue, templates)\n\t\trequire.NoError(t, err)\n\n\t\terr = readRequestBodyJSON(req, &body)\n\t\tassert.NoError(t, err)\n\t\texpected := mount\n\t\tslashPath, err := platform.TranslatePathFromClient(t.Context(), bindPath)\n\t\texpected.Source = slashPath\n\t\tassert.NoError(t, err)\n\t\trequire.NotEmpty(t, body.HostConfig.Mounts)\n\t\trequire.NotNil(t, body.HostConfig.Mounts[0])\n\t\tassert.Equal(t, expected, *body.HostConfig.Mounts[0])\n\t})\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/doc.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package mungers includes the code to modify each moby API.\npackage mungers\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/helpers.go",
    "content": "//go:build linux || windows\n\n/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// readRequestBodyJSON reads the incoming HTTP request body as if it was JSON,\n// unmarshalled into the provided object.  A copy of the data is placed in the\n// request body, so that it can be used by downstream consumers as necessary.\nfunc readRequestBodyJSON(req *http.Request, data interface{}) error {\n\tbuf, err := io.ReadAll(req.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not read request body: %w\", err)\n\t}\n\n\terr = json.Unmarshal(buf, data)\n\treq.Body = io.NopCloser(bytes.NewBuffer(buf))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not unmarshal request body: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/mungers/helpers_linux.go",
    "content": "/*\nCopyright © 2025 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage mungers\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n)\n\n// readResponseBodyJSON reads the outgoing HTTP response body as if it was JSON,\n// unmarshalled into the provided object.  A copy of the data is placed in the\n// response body, so that it can be used directly if no modification needed to\n// occur.\nfunc readResponseBodyJSON(resp *http.Response, data any) error {\n\tbuf, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not read response body: %w\", err)\n\t}\n\n\terr = json.Unmarshal(buf, data)\n\tresp.Body = io.NopCloser(bytes.NewBuffer(buf))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not unmarshal response body: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/hyperv.go",
    "content": "//go:build windows\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows/registry\"\n)\n\n// Racer is a helper structure to return either a successful result (the GUID of\n// the Hyper-V virtual machine for WSL2), or an error if all attempts have\n// failed.\ntype racer struct {\n\tresult hvsock.GUID\n\terrors []error\n\tcount  int\n\tlock   sync.Locker\n\tcond   *sync.Cond\n}\n\n// newRacer constructs a new racer that will return an error after the given\n// number of calls to reject().\nfunc newRacer(n int) *racer {\n\tmutex := &sync.Mutex{}\n\treturn &racer{\n\t\tresult: hvsock.GUIDZero,\n\t\tcount:  n,\n\t\tlock:   mutex,\n\t\tcond:   sync.NewCond(mutex),\n\t}\n}\n\n// Wait for a result; either the parameter of any resolve() call, or the last\n// error from a reject() call.\nfunc (r *racer) wait() (hvsock.GUID, error) {\n\tr.lock.Lock()\n\tdefer r.lock.Unlock()\n\tfor r.count > 0 {\n\t\tr.cond.Wait()\n\t}\n\tif r.result != hvsock.GUIDZero {\n\t\treturn r.result, nil\n\t}\n\treturn hvsock.GUIDZero, r\n}\n\n// Resolve the racer with the given successful result.\nfunc (r *racer) resolve(guid hvsock.GUID) {\n\tr.lock.Lock()\n\tdefer r.lock.Unlock()\n\tif r.result == hvsock.GUIDZero {\n\t\tr.result = guid\n\t}\n\tr.count = 0\n\tr.cond.Signal()\n}\n\n// Reject the racer with the given unsuccessful result.\nfunc (r *racer) reject(err error) {\n\tr.lock.Lock()\n\tdefer r.lock.Unlock()\n\tr.errors = append(r.errors, err)\n\tr.count -= 1\n\tr.cond.Signal()\n}\n\nfunc (r racer) Error() string {\n\tr.lock.Lock()\n\tdefer r.lock.Unlock()\n\tif len(r.errors) == 0 {\n\t\treturn \"<no error>\"\n\t}\n\tif len(r.errors) == 1 {\n\t\treturn r.errors[0].Error()\n\t}\n\tmessages := make([]string, 0, len(r.errors))\n\tfor _, err := range r.errors {\n\t\tmessages = append(messages, err.Error())\n\t}\n\treturn fmt.Sprintf(\"multiple errors: \\n\\t- %s\", strings.Join(messages, \"\\n\\t- \"))\n}\n\nfunc (r racer) Unwrap() error {\n\tr.lock.Lock()\n\tdefer r.lock.Unlock()\n\tif len(r.errors) == 1 {\n\t\treturn r.errors[0]\n\t}\n\treturn nil\n}\n\n// Probe the system to detect the correct VM GUID for the WSL virtual machine.\n// This requires that WSL2 is already running.\nfunc probeVMGUID(port uint32) (hvsock.GUID, error) {\n\tkey, err := registry.OpenKey(\n\t\tregistry.LOCAL_MACHINE,\n\t\t`SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\HostComputeService\\VolatileStore\\ComputeSystem`,\n\t\tregistry.ENUMERATE_SUB_KEYS)\n\tif err != nil {\n\t\treturn hvsock.GUIDZero, fmt.Errorf(\"could not open registry key: %w\", err)\n\t}\n\tnames, err := key.ReadSubKeyNames(0)\n\tif err != nil {\n\t\treturn hvsock.GUIDZero, fmt.Errorf(\"could not list virtual machine IDs: %w\", err)\n\t}\n\n\tr := newRacer(len(names))\n\tfor _, name := range names {\n\t\tgo func(name string) {\n\t\t\tguid, err := hvsock.GUIDFromString(name)\n\t\t\tif err != nil {\n\t\t\t\tr.reject(fmt.Errorf(\"invalid VM name %w\", err))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconn, err := dialHvsock(guid, port)\n\t\t\tif err != nil {\n\t\t\t\terr := fmt.Errorf(\"could not dial VM %s: %w\", name, err)\n\t\t\t\tr.reject(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer conn.Close()\n\n\t\t\tlogrus.WithField(\"guid\", guid.String()).Info(\"Got WSL2 VM\")\n\t\t\tr.resolve(guid)\n\t\t}(name)\n\t}\n\n\tresult, err := r.wait()\n\tif err != nil {\n\t\treturn hvsock.GUIDZero, fmt.Errorf(\"could not find WSL2 VM ID: %w\", err)\n\t}\n\treturn result, nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/hyperv_test.go",
    "content": "//go:build windows\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRacer(t *testing.T) {\n\tt.Run(\"simple-resolve\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tr := newRacer(1)\n\n\t\tgo func() {\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tr.resolve(hvsock.GUIDLoopback)\n\t\t}()\n\n\t\tv, err := r.wait()\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, hvsock.GUIDLoopback, v)\n\t})\n\n\tt.Run(\"simple-reject\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tr := newRacer(1)\n\n\t\tgo func() {\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tr.reject(os.ErrInvalid)\n\t\t}()\n\n\t\t_, err := r.wait()\n\t\tassert.ErrorIs(t, err, os.ErrInvalid)\n\t})\n\n\tt.Run(\"any-resolve\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tr := newRacer(2)\n\n\t\tgo func() {\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tr.reject(os.ErrInvalid)\n\t\t\tr.resolve(hvsock.GUIDLoopback)\n\t\t}()\n\n\t\tv, err := r.wait()\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, v, hvsock.GUIDLoopback)\n\t\t}\n\t})\n\n\tt.Run(\"first-resolve\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tr := newRacer(2)\n\n\t\tgo func() {\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t\tr.resolve(hvsock.GUIDLoopback)\n\t\t\tr.resolve(hvsock.GUIDParent)\n\t\t}()\n\n\t\tv, err := r.wait()\n\t\tif assert.NoError(t, err) {\n\t\t\tassert.Equal(t, v, hvsock.GUIDLoopback)\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/serve_linux.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// DefaultEndpoint is the platform-specific location that dockerd listens on by\n// default.\nconst DefaultEndpoint = \"unix:///var/run/docker.sock\"\n\n// ErrListenerClosed is the error that is returned when we attempt to call\n// Accept() on a closed listener.\nvar ErrListenerClosed = net.ErrClosed\n\n// MakeDialer computes the dial function.\nfunc MakeDialer(proxyEndpoint string) (func(ctx context.Context) (net.Conn, error), error) {\n\tdialer := net.Dialer{}\n\treturn func(ctx context.Context) (net.Conn, error) {\n\t\tconn, err := dialer.DialContext(ctx, \"unix\", proxyEndpoint)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn conn, nil\n\t}, nil\n}\n\n// Listen on the given Unix socket endpoint.\nfunc Listen(ctx context.Context, endpoint string) (net.Listener, error) {\n\tprefix := \"unix://\"\n\tif !strings.HasPrefix(endpoint, prefix) {\n\t\treturn nil, fmt.Errorf(\"endpoint %s does not start with protocol %s\", endpoint, prefix)\n\t}\n\n\tfilepath := endpoint[len(prefix):]\n\taddr, err := net.ResolveUnixAddr(\"unix\", filepath)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not resolve endpoint %s: %w\", endpoint, err)\n\t}\n\n\t// First, try to connect to it; if it's connection refused, then the socket\n\t// file exists but nobody is listening, in which case we can delete it.\n\tdialer := net.Dialer{}\n\tconn, err := dialer.DialContext(ctx, \"unix\", filepath)\n\tif err != nil {\n\t\tif errors.Is(err, syscall.ECONNREFUSED) {\n\t\t\tif err = os.Remove(filepath); err != nil {\n\t\t\t\tlogrus.WithError(err).WithField(\"path\", filepath).Debug(\"could not remove dead socket, ignoring.\")\n\t\t\t}\n\t\t} else if !errors.Is(err, os.ErrNotExist) {\n\t\t\tlogrus.WithError(err).Debug(\"unexpected error connecting to existing socket, ignoring.\")\n\t\t}\n\t} else {\n\t\tconn.Close()\n\t\t// Another process is listening; we'll just continue and let ListenUnix\n\t\t// fail and return an error.\n\t}\n\n\tlistener, err := net.ListenUnix(\"unix\", addr)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not listen on %s: %w\", endpoint, err)\n\t}\n\n\tsuccess := false\n\tdefer func() {\n\t\tif !success {\n\t\t\tlistener.Close()\n\t\t}\n\t}()\n\n\tvar stat unix.Stat_t\n\terr = unix.Stat(filepath, &stat)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not get socket %s permissions: %w\", filepath, err)\n\t}\n\n\tdesiredPerms := os.FileMode(stat.Mode | 0o777)\n\terr = os.Chmod(filepath, desiredPerms)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not change socket %s permissions: %w\", filepath, err)\n\t}\n\n\tsuccess = true\n\treturn listener, nil\n}\n\n// ParseBindString parses a HostConfig.Binds entry, returning the (<host-src> or\n// <volume-name>), <container-dest>, and (optional) <options>.  Additionally, it\n// also returns a boolean indicating if the first argument is a host path.\nfunc ParseBindString(input string) (string, string, string, bool) {\n\t// The volumes here are [<host-src>:]<container-dest>[:options]\n\t// For a first pass, let's just assume there are no colons in any of this...\n\t// The API spec says that if the first part is a host path, then it _must_\n\t// be absolute.\n\thostIsPath := strings.HasPrefix(input, \"/\")\n\tfirstIndex := strings.Index(input, \":\")\n\tlastIndex := strings.LastIndex(input, \":\")\n\tif firstIndex < 0 {\n\t\t// just /foo -- map the same path on the host to the container.\n\t\treturn input, input, \"\", hostIsPath\n\t}\n\tstart := input[:firstIndex]\n\tend := input[lastIndex+1:]\n\tif lastIndex > firstIndex {\n\t\t// /foo:/bar:ro\n\t\tmiddle := input[firstIndex+1 : lastIndex]\n\t\treturn start, middle, end, hostIsPath\n\t}\n\t// either /foo:/bar or /foo:ro\n\tif strings.HasPrefix(end, \"/\") {\n\t\treturn start, end, \"\", hostIsPath\n\t}\n\treturn start, start, end, hostIsPath\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/serve_windows.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/Microsoft/go-winio\"\n\t\"github.com/linuxkit/virtsock/pkg/hvsock\"\n)\n\n// DefaultEndpoint is the platform-specific location that dockerd listens on by\n// default.\nconst DefaultEndpoint = \"npipe:////./pipe/docker_engine\"\n\n// ErrListenerClosed is the error that is returned when we attempt to call\n// Accept() on a closed listener.\nvar ErrListenerClosed = winio.ErrPipeListenerClosed\n\n// MakeDialer computes the dial function.\nfunc MakeDialer(port uint32) (func(ctx context.Context) (net.Conn, error), error) {\n\tvmGUID, err := probeVMGUID(port)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not detect WSL2 VM: %w\", err)\n\t}\n\tdial := func(ctx context.Context) (net.Conn, error) {\n\t\tconn, err := dialHvsock(vmGUID, port)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn conn, nil\n\t}\n\treturn dial, nil\n}\n\n// dialHvsock creates a net.Conn to a Hyper-V VM running Linux with the given\n// GUID, listening on the given vsock port.\nfunc dialHvsock(vmGUID hvsock.GUID, port uint32) (net.Conn, error) {\n\t// go-winio doesn't implement DialHvsock(), but luckily LinuxKit has an\n\t// implementation.  We still need go-winio to convert port to GUID.\n\tsvcGUID, err := hvsock.GUIDFromString(winio.VsockServiceID(port).String())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not parse Hyper-V service GUID: %w\", err)\n\t}\n\taddr := hvsock.Addr{\n\t\tVMID:      vmGUID,\n\t\tServiceID: svcGUID,\n\t}\n\n\t// Normally dial times out after 30 seconds, but we want to time out faster.\n\tch := make(chan struct {\n\t\tconn net.Conn\n\t\terr  error\n\t}, 1)\n\tgo func() {\n\t\tconn, err := hvsock.Dial(addr)\n\t\tch <- struct {\n\t\t\tconn net.Conn\n\t\t\terr  error\n\t\t}{conn: conn, err: err}\n\t}()\n\n\tselect {\n\tcase result := <-ch:\n\t\tif result.err != nil {\n\t\t\treturn nil, fmt.Errorf(\"could not dial Hyper-V socket: %w\", result.err)\n\t\t}\n\t\treturn result.conn, nil\n\tcase <-time.After(time.Second):\n\t\treturn nil, fmt.Errorf(\"timed out dialing Hyper-V socket\")\n\t}\n}\n\n// Listen on the given Windows named pipe endpoint.\nfunc Listen(ctx context.Context, endpoint string) (net.Listener, error) {\n\tconst prefix = \"npipe://\"\n\n\tif !strings.HasPrefix(endpoint, prefix) {\n\t\treturn nil, fmt.Errorf(\"endpoint %s does not start with protocol %s\", endpoint, prefix)\n\t}\n\n\t// Configure pipe in MessageMode to support Docker's half-close semantics\n\t// - Enables zero-byte writes as EOF signals (CloseWrite)\n\t// - Crucial for stdin stream termination in interactive containers\n\tpipeConfig := &winio.PipeConfig{MessageMode: true}\n\n\tlistener, err := winio.ListenPipe(endpoint[len(prefix):], pipeConfig)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not listen on %s: %w\", endpoint, err)\n\t}\n\n\treturn listener, nil\n}\n\n// ParseBindString parses a HostConfig.Binds entry, returning the (<host-src> or\n// <volume-name>), <container-dest>, and (optional) <options>.  Additionally, it\n// also returns a boolean indicating if the first argument is a host path.\nfunc ParseBindString(input string) (string, string, string, bool) {\n\t// Windows names can be one of a few things:\n\t// C:\\foo\\bar                   colon is possible after the drive letter\n\t// \\\\?\\C:\\foo\\bar               colon is possible after the drive letter\n\t// \\\\server\\share\\foo           no colons are allowed\n\t// \\\\.\\pipe\\foo                 no colons are allowed\n\t// Luckily, we only have Linux dockerd, so we only have to worry about\n\t// Windows-style paths (that may contain colons) in the first part.\n\n\t// pathPattern is a RE for the first two options above.\n\tpathPattern := regexp.MustCompile(`^(?:\\\\\\\\\\?\\\\)?.:[^:]*`)\n\tmatch := pathPattern.FindString(input)\n\tif match == \"\" {\n\t\t// The first part is a volume name, a pipe, or other non-path thing.\n\t\tfirstIndex := strings.Index(input, \":\")\n\t\tlastIndex := strings.LastIndex(input, \":\")\n\t\tif firstIndex == lastIndex {\n\t\t\treturn input[:firstIndex], input[firstIndex+1:], \"\", false\n\t\t}\n\t\treturn input[:firstIndex], input[firstIndex+1 : lastIndex], input[lastIndex+1:], false\n\t} else {\n\t\t// The first part is a path.\n\t\trest := input[len(match)+1:]\n\t\tindex := strings.LastIndex(rest, \":\")\n\t\tif index > -1 {\n\t\t\treturn match, rest[:index], rest[index+1:], true\n\t\t}\n\t\treturn match, rest, \"\", true\n\t}\n}\n\nfunc isSlash(input string, indices ...int) bool {\n\tfor _, i := range indices {\n\t\tif len(input) <= i || (input[i] != '/' && input[i] != '\\\\') {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc IsAbsolutePath(input string) bool {\n\tif len(input) > 2 && input[1] == ':' && isSlash(input, 2) {\n\t\t// C:\\\n\t\treturn true\n\t}\n\tif len(input) > 6 && isSlash(input, 0, 1, 3) && input[2] == '?' && input[5] == ':' {\n\t\t// \\\\?\\C:\\\n\t\treturn true\n\t}\n\treturn false\n}\n\n// TranslatePathFromClient converts a client path to a path that can be used by\n// the docker daemon.\nfunc TranslatePathFromClient(ctx context.Context, windowsPath string) (string, error) {\n\t// TODO: See if we can do something faster than shelling out.\n\tcmd := exec.CommandContext(ctx, \"wsl\", \"--distribution\", \"rancher-desktop\", \"--exec\", \"/bin/wslpath\", \"-a\", \"-u\", windowsPath)\n\toutput, err := cmd.Output()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error getting WSL path: %w\", err)\n\t}\n\n\treturn strings.TrimSpace(string(output)), nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/serve_windows_test.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestParseBindString(t *testing.T) {\n\tt.Parallel()\n\tcases := map[string]struct {\n\t\thost      string\n\t\tcontainer string\n\t\toptions   string\n\t\tisPath    bool\n\t}{\n\t\t\"host:container\":            {\"host\", \"container\", \"\", false},\n\t\t\"host:container:rw\":         {\"host\", \"container\", \"rw\", false},\n\t\t`C:\\Windows:/host`:          {\"C:\\\\Windows\", \"/host\", \"\", true},\n\t\t`C:\\Windows:/host:ro`:       {\"C:\\\\Windows\", \"/host\", \"ro\", true},\n\t\t`\\\\?\\c:\\windows:/z`:         {`\\\\?\\c:\\windows`, \"/z\", \"\", true},\n\t\t`\\\\server\\share:/share`:     {`\\\\server\\share`, \"/share\", \"\", false},\n\t\t`\\\\.\\pipe\\foo:/pipe:foo:ro`: {`\\\\.\\pipe\\foo`, \"/pipe:foo\", \"ro\", false},\n\t}\n\n\tfor input, expected := range cases {\n\t\tt.Run(input, func(t *testing.T) {\n\t\t\thost, container, options, isPath := ParseBindString(input)\n\t\t\tassert.Equal(t, expected.host, host)\n\t\t\tassert.Equal(t, expected.container, container)\n\t\t\tassert.Equal(t, expected.options, options)\n\t\t\tassert.Equal(t, expected.isPath, isPath)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/vsock_linux.go",
    "content": "package platform\n\n// This file contains extensions to vsock handling on Linux.  This is derived\n// from github.com/linuxkit/virtsock/pkg/vsock.\n\n/**\n * Copyright 2016-2017 The authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/linuxkit/virtsock/pkg/vsock\"\n\t\"github.com/pkg/errors\"\n\t\"golang.org/x/sys/unix\"\n)\n\n// Convert a generic unix.Sockaddr to a Addr\nfunc sockaddrToVsock(sa unix.Sockaddr) *vsock.Addr {\n\tif vmAddr, ok := sa.(*unix.SockaddrVM); ok {\n\t\treturn &vsock.Addr{CID: vmAddr.CID, Port: vmAddr.Port}\n\t}\n\treturn nil\n}\n\n// ListenVsockNonBlocking returns a net.Listener which can accept connections on the given port, returning non-blocking connections.\nfunc ListenVsockNonBlocking(cid, port uint32) (net.Listener, error) {\n\tfd, err := syscall.Socket(unix.AF_VSOCK, syscall.SOCK_STREAM|syscall.SOCK_CLOEXEC, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsa := &unix.SockaddrVM{CID: cid, Port: port}\n\tif err = unix.Bind(fd, sa); err != nil {\n\t\treturn nil, errors.Wrapf(err, \"bind() to %08x.%08x failed\", cid, port)\n\t}\n\n\terr = syscall.Listen(fd, syscall.SOMAXCONN)\n\tif err != nil {\n\t\treturn nil, errors.Wrapf(err, \"listen() on %08x.%08x failed\", cid, port)\n\t}\n\treturn &vsockListener{fd, vsock.Addr{CID: cid, Port: port}}, nil\n}\n\ntype vsockListener struct {\n\tfd    int\n\tlocal vsock.Addr\n}\n\n// Accept accepts an incoming call and returns the new connection.\nfunc (v *vsockListener) Accept() (net.Conn, error) {\n\tfd, sa, err := unix.Accept4(v.fd, unix.SOCK_NONBLOCK)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error accept()ing connection: %w\", err)\n\t}\n\treturn newVsockConn(uintptr(fd), &v.local, sockaddrToVsock(sa)), nil\n}\n\n// Close closes the listening connection\nfunc (v *vsockListener) Close() error {\n\t// Note this won't cause the Accept to unblock.\n\treturn unix.Close(v.fd)\n}\n\n// Addr returns the address listened to by the Listener\nfunc (v *vsockListener) Addr() net.Addr {\n\treturn v.local\n}\n\n// a wrapper around FileConn which supports CloseRead and CloseWrite\ntype vsockConn struct {\n\tvsock  *os.File\n\tfd     uintptr\n\tlocal  *vsock.Addr\n\tremote *vsock.Addr\n}\n\nfunc newVsockConn(fd uintptr, local, remote *vsock.Addr) *vsockConn {\n\tsocketFile := os.NewFile(fd, fmt.Sprintf(\"vsock:%d\", fd))\n\treturn &vsockConn{vsock: socketFile, fd: fd, local: local, remote: remote}\n}\n\n// LocalAddr returns the local address of a connection\nfunc (v *vsockConn) LocalAddr() net.Addr {\n\treturn v.local\n}\n\n// RemoteAddr returns the remote address of a connection\nfunc (v *vsockConn) RemoteAddr() net.Addr {\n\treturn v.remote\n}\n\n// Close closes the connection\nfunc (v *vsockConn) Close() error {\n\treturn v.vsock.Close()\n}\n\n// CloseRead shuts down the reading side of a vsock connection\nfunc (v *vsockConn) CloseRead() error {\n\treturn syscall.Shutdown(int(v.fd), syscall.SHUT_RD)\n}\n\n// CloseWrite shuts down the writing side of a vsock connection\nfunc (v *vsockConn) CloseWrite() error {\n\treturn syscall.Shutdown(int(v.fd), syscall.SHUT_WR)\n}\n\n// Read reads data from the connection\nfunc (v *vsockConn) Read(buf []byte) (int, error) {\n\treturn v.vsock.Read(buf)\n}\n\n// Write writes data over the connection\nfunc (v *vsockConn) Write(buf []byte) (int, error) {\n\treturn v.vsock.Write(buf)\n}\n\n// SetDeadline sets the read and write deadlines associated with the connection\nfunc (v *vsockConn) SetDeadline(t time.Time) error {\n\treturn nil // FIXME\n}\n\n// SetReadDeadline sets the deadline for future Read calls.\nfunc (v *vsockConn) SetReadDeadline(t time.Time) error {\n\treturn nil // FIXME\n}\n\n// SetWriteDeadline sets the deadline for future Write calls\nfunc (v *vsockConn) SetWriteDeadline(t time.Time) error {\n\treturn nil // FIXME\n}\n\n// File duplicates the underlying socket descriptor and returns it.\nfunc (v *vsockConn) File() (*os.File, error) {\n\t// This is equivalent to dup(2) but creates the new fd with CLOEXEC already set.\n\tr0, _, e1 := syscall.Syscall(syscall.SYS_FCNTL, v.vsock.Fd(), syscall.F_DUPFD_CLOEXEC, 0)\n\tif e1 != 0 {\n\t\treturn nil, os.NewSyscallError(\"fcntl\", e1)\n\t}\n\treturn os.NewFile(r0, v.vsock.Name()), nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/platform/wsl_mountpoint_linux.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage platform\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst (\n\t// mountPointField is the zero-indexed field number inf /proc/self/mountinfo\n\t// that contains the mount point.\n\tmountPointField = 4\n)\n\n// Get the WSL mount point; typically, this is /mnt/wsl.\n// If we fail to find one, we will use /mnt/wsl instead.\nfunc GetWSLMountPoint() (string, error) {\n\tbuf, err := os.ReadFile(\"/proc/self/mountinfo\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error reading mounts: %w\", err)\n\t}\n\tfor _, line := range strings.Split(string(buf), \"\\n\") {\n\t\tif !strings.Contains(line, \" - tmpfs \") {\n\t\t\t// Skip the line if the filesystem type isn't \"tmpfs\"\n\t\t\tcontinue\n\t\t}\n\t\tfields := strings.Split(line, \" \")\n\t\tif len(fields) > mountPointField {\n\t\t\tif strings.HasSuffix(fields[mountPointField], \"/wsl\") {\n\t\t\t\treturn fields[mountPointField], nil\n\t\t\t}\n\t\t}\n\t}\n\tlogrus.Warnf(\"Could not find WSL mount root, falling back to /mnt/wsl\")\n\treturn \"/mnt/wsl\", nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/serve.go",
    "content": "//go:build linux || windows\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage dockerproxy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"regexp\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/Masterminds/semver\"\n\t\"github.com/sirupsen/logrus\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/models\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/util\"\n)\n\n// requestContextKeyType is a type defined for the context key to be unique.\ntype requestContextKeyType struct{}\n\n// RequestContextValue contains things we attach to incoming requests\ntype RequestContextValue map[interface{}]interface{}\n\n// requestContext is the context key for requestContextValue\nvar requestContext = requestContextKeyType{}\n\ntype containerInspectResponseBody struct {\n\tID string `json:\"Id\"`\n}\n\nconst dockerAPIVersion = \"v1.41.0\"\n\n// Serve up the docker proxy at the given endpoint, using the given function to\n// create a connection to the real dockerd.\nfunc Serve(ctx context.Context, endpoint string, dialer func(ctx context.Context) (net.Conn, error)) error {\n\tlistener, err := platform.Listen(ctx, endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttermch := make(chan os.Signal, 1)\n\tsignal.Notify(termch, os.Interrupt)\n\tgo func() {\n\t\t<-termch\n\t\tsignal.Stop(termch)\n\t\terr := listener.Close()\n\t\tif err != nil {\n\t\t\tlogrus.WithError(err).Error(\"Error closing listener on interrupt\")\n\t\t}\n\t}()\n\n\tlogWriter := logrus.StandardLogger().Writer()\n\tdefer logWriter.Close()\n\tmunger := newRequestMunger()\n\tproxy := &util.ReverseProxy{\n\t\tDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {\n\t\t\treturn dialer(ctx)\n\t\t},\n\t\tDirector: func(req *http.Request) {\n\t\t\tlogrus.WithField(\"request\", req).\n\t\t\t\tWithField(\"headers\", req.Header).\n\t\t\t\tWithField(\"url\", req.URL).\n\t\t\t\tDebug(\"got proxy request\")\n\t\t\t// The incoming URL is relative (to the root of the server); we need\n\t\t\t// to add scheme and host (\"http://proxy.invalid/\") to it.\n\t\t\treq.URL.Scheme = \"http\"\n\t\t\treq.URL.Host = \"proxy.invalid\"\n\n\t\t\toriginalReq := *req\n\t\t\toriginalURL := *req.URL\n\t\t\toriginalReq.URL = &originalURL\n\t\t\terr := munger.MungeRequest(req, dialer)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).\n\t\t\t\t\tWithField(\"original request\", originalReq).\n\t\t\t\t\tWithField(\"modified request\", req).\n\t\t\t\t\tError(\"could not munge request\")\n\t\t\t}\n\t\t},\n\t\tModifyResponse: func(resp *http.Response) error {\n\t\t\tlogEntry := logrus.WithField(\"response\", resp)\n\t\t\tdefer func() { logEntry.Debug(\"got backend response\") }()\n\n\t\t\t// Check the API version response, and if there is one, make sure\n\t\t\t// it's not newer than the API version we support.\n\t\t\tbackendVersion, err := semver.NewVersion(resp.Header.Get(\"API-Version\"))\n\t\t\tif err == nil {\n\t\t\t\tlogEntry = logEntry.WithField(\"backend version\", backendVersion)\n\t\t\t\tif backendVersion.GreaterThan(&dockerSpec.Info.Version) {\n\t\t\t\t\toverrideVersion := fmt.Sprintf(\"v%s\", dockerSpec.Info.Version.Original())\n\t\t\t\t\tresp.Header.Set(\"API-Version\", overrideVersion)\n\t\t\t\t\tlogEntry = logEntry.WithField(\"override version\", overrideVersion)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr = munger.MungeResponse(resp, dialer)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t\tErrorLog: log.New(logWriter, \"\", 0),\n\t}\n\n\tserver := &http.Server{\n\t\tReadHeaderTimeout: time.Minute,\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {\n\t\t\tctx := context.WithValue(req.Context(), requestContext, &RequestContextValue{})\n\t\t\tnewReq := req.WithContext(ctx)\n\t\t\tproxy.ServeHTTP(w, newReq)\n\t\t}),\n\t}\n\n\tlogrus.WithField(\"endpoint\", endpoint).Info(\"Listening\")\n\n\terr = server.Serve(listener)\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"serve exited with error\")\n\t}\n\n\treturn nil\n}\n\n// requestMunger is used to modify the incoming http.Request as required.\ntype requestMunger struct {\n\t// apiDetectPattern is used to detect the API version request path prefix.\n\tapiDetectPattern *regexp.Regexp\n\n\tsync.RWMutex\n}\n\n// newRequestMunger initializes a new requestMunger.\nfunc newRequestMunger() *requestMunger {\n\treturn &requestMunger{\n\t\tapiDetectPattern: regexp.MustCompile(`^/v[0-9.]+/`),\n\t}\n}\n\nfunc (m *requestMunger) getRequestPath(req *http.Request) string {\n\t// Strip the version string at the start of the request, if it exists.\n\trequestPath := req.URL.Path\n\tmatch := m.apiDetectPattern.FindStringIndex(requestPath)\n\tlogrus.WithFields(logrus.Fields{\n\t\t\"request path\": requestPath,\n\t\t\"matcher\":      m.apiDetectPattern,\n\t\t\"match\":        match,\n\t}).Debug(\"getting request path\")\n\tif match != nil {\n\t\treturn requestPath[match[1]-1:]\n\t}\n\treturn requestPath\n}\n\n// MungeRequest modifies a given request in-place.\nfunc (m *requestMunger) MungeRequest(req *http.Request, dialer func(ctx context.Context) (net.Conn, error)) error {\n\trequestPath := m.getRequestPath(req)\n\tlogEntry := logrus.WithFields(logrus.Fields{\n\t\t\"method\": req.Method,\n\t\t\"path\":   requestPath,\n\t\t\"phase\":  \"request\",\n\t})\n\tmungerMapping.RLock()\n\tmapping, ok := mungerMapping.mungers[req.Method]\n\tmungerMapping.RUnlock()\n\tif !ok {\n\t\tlogEntry.Debug(\"no munger with method\")\n\t\treturn nil\n\t}\n\tmunger, templates := mapping.getRequestMunger(requestPath)\n\tif munger == nil {\n\t\tlogEntry.Debug(\"request munger not found\")\n\t\treturn nil\n\t}\n\n\t// ensure id is always the long container id\n\tid, ok := templates[\"id\"]\n\tif ok {\n\t\tinspect, err := m.CanonicalizeContainerID(req, id, dialer)\n\t\tif err != nil {\n\t\t\tlogEntry.WithField(\"id\", id).WithError(err).Error(\"unable to resolve container id\")\n\t\t} else {\n\t\t\ttemplates[\"id\"] = inspect.ID\n\t\t}\n\t}\n\n\tcontextValue, _ := req.Context().Value(requestContext).(*RequestContextValue)\n\tlogEntry.Debug(\"calling request munger\")\n\terr := munger(req, contextValue, templates)\n\tif err != nil {\n\t\tlogEntry.WithField(\"munger\", munger).WithError(err).Error(\"munger failed\")\n\t\treturn fmt.Errorf(\"munger failed for %s: %w\", requestPath, err)\n\t}\n\treturn nil\n}\n\nfunc (m *requestMunger) MungeResponse(resp *http.Response, dialer func(ctx context.Context) (net.Conn, error)) error {\n\trequestPath := m.getRequestPath(resp.Request)\n\tlogEntry := logrus.WithFields(logrus.Fields{\n\t\t\"method\": resp.Request.Method,\n\t\t\"path\":   requestPath,\n\t\t\"phase\":  \"response\",\n\t})\n\tmungerMapping.RLock()\n\tmapping, ok := mungerMapping.mungers[resp.Request.Method]\n\tmungerMapping.RUnlock()\n\tif !ok {\n\t\tlogEntry.Debug(\"no munger with method\")\n\t\treturn nil\n\t}\n\tmunger, templates := mapping.getResponseMunger(requestPath)\n\tif munger == nil {\n\t\tlogEntry.Debug(\"request munger not found\")\n\t\treturn nil\n\t}\n\n\t// ensure id is always the long container id\n\tid, ok := templates[\"id\"]\n\tif ok {\n\t\tinspect, err := m.CanonicalizeContainerID(resp.Request, id, dialer)\n\t\tif err != nil {\n\t\t\tlogEntry.WithField(\"id\", id).WithError(err).Error(\"unable to resolve container id\")\n\t\t} else {\n\t\t\ttemplates[\"id\"] = inspect.ID\n\t\t}\n\t}\n\n\tcontextValue, _ := resp.Request.Context().Value(requestContext).(*RequestContextValue)\n\tlogEntry.Debug(\"calling response munger\")\n\terr := munger(resp, contextValue, templates)\n\tif err != nil {\n\t\tlogEntry.WithField(\"munger\", munger).WithError(err).Error(\"munger failed\")\n\t\treturn fmt.Errorf(\"munger failed for %s: %w\", requestPath, err)\n\t}\n\treturn nil\n}\n\n/*\nCanonicalizeContainerID makes a request upstream to inspect and resolve the full id of the container\nwe use the provided id path template variable to make an upstream request to the docker engine api to inspect the container.\nFortunately it supports both id or name as the container identifier.\nThe ID returned will be the full long container id that is used to lookup in docker-binds.json.\n*/\nfunc (m *requestMunger) CanonicalizeContainerID(req *http.Request, id string, dialer func(ctx context.Context) (net.Conn, error)) (*containerInspectResponseBody, error) {\n\t// url for inspecting container\n\tinspectURL, err := req.URL.Parse(fmt.Sprintf(\"/%s/containers/%s/json\", dockerAPIVersion, id))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := &http.Client{\n\t\tTransport: &http.Transport{\n\t\t\tDial: func(string, string) (net.Conn, error) {\n\t\t\t\treturn dialer(req.Context())\n\t\t\t},\n\t\t},\n\t}\n\n\t// make the inspect request\n\tinspectRequest, err := http.NewRequestWithContext(req.Context(), \"GET\", inspectURL.String(), http.NoBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinspectResponse, err := client.Do(inspectRequest)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer inspectResponse.Body.Close()\n\n\t// parse response as json\n\tbody := containerInspectResponseBody{}\n\tbuf, err := io.ReadAll(inspectResponse.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not read request body: %w\", err)\n\t}\n\n\terr = json.Unmarshal(buf, &body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not unmarshal request body: %w\", err)\n\t}\n\n\treturn &body, nil\n}\n\n// dockerSpec contains information about the embedded OpenAPI specification for\n// docker.\nvar dockerSpec struct {\n\tInfo struct {\n\t\tVersion semver.Version\n\t}\n}\n\n// requestMungerFunc is a munger for an incoming request; it also receives an\n// arbitrary mapping that can be reused in the response munger, as well as a\n// mapping of any path templating patterns that were matched.\ntype requestMungerFunc func(*http.Request, *RequestContextValue, map[string]string) error\n\n// responseMungerFunc is a munger for an outgoing response; it also receives an\n// arbitrary mapping that was initially passed to the matching request munger,\n// as well as a mapping of any path templating patterns that were matched.\ntype responseMungerFunc func(*http.Response, *RequestContextValue, map[string]string) error\n\n// mungerMethodMapping is a helper structure to find a munger given an API path,\n// specialized for a given HTTP method (GET, POST, etc.).\n// This should only be written to during init(), at which point it's protected\n// by the lock on mungerMapping.\ntype mungerMethodMapping struct {\n\t// requests that are simple (have no path templating)\n\trequests map[string]requestMungerFunc\n\t// requestPatterns are requests that involve path templating\n\trequestPatterns map[*regexp.Regexp]requestMungerFunc\n\t// responses that are simple (have no path templating)\n\tresponses map[string]responseMungerFunc\n\t// responsePatterns are responses that involve path templating\n\tresponsePatterns map[*regexp.Regexp]responseMungerFunc\n}\n\n// getRequestMunger gets the munger to use for this request, as well as the\n// path templating elements (if relevant for the munger).\nfunc (m *mungerMethodMapping) getRequestMunger(apiPath string) (requestMungerFunc, map[string]string) {\n\tif munger, ok := m.requests[apiPath]; ok {\n\t\treturn munger, nil\n\t}\n\tfor pattern, munger := range m.requestPatterns {\n\t\tmatches := pattern.FindStringSubmatch(apiPath)\n\t\tif matches != nil {\n\t\t\tnames := pattern.SubexpNames()\n\t\t\tresults := make(map[string]string)\n\t\t\tfor i, name := range names {\n\t\t\t\tresults[name] = matches[i]\n\t\t\t}\n\t\t\treturn munger, results\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (m *mungerMethodMapping) getResponseMunger(apiPath string) (responseMungerFunc, map[string]string) {\n\tif munger, ok := m.responses[apiPath]; ok {\n\t\treturn munger, nil\n\t}\n\tfor pattern, munger := range m.responsePatterns {\n\t\tmatches := pattern.FindStringSubmatch(apiPath)\n\t\tif matches != nil {\n\t\t\tnames := pattern.SubexpNames()\n\t\t\tresults := make(map[string]string)\n\t\t\tfor i, name := range names {\n\t\t\t\tresults[name] = matches[i]\n\t\t\t}\n\t\t\treturn munger, results\n\t\t}\n\t}\n\treturn nil, nil\n}\n\n// mungerMapping contains mungers that will handle particular API endpoints.\nvar mungerMapping struct {\n\tsync.RWMutex\n\tmungers map[string]*mungerMethodMapping\n}\n\n// convertPattern converts an API path to a regular expression pattern for\n// matching URLs with path templating; if there are no path templates, this\n// returns nil.  The returned pattern always matches the whole string.\nfunc convertPattern(apiPath string) *regexp.Regexp {\n\tmatches := regexp.MustCompile(`{[^}/]+}`).FindAllStringIndex(apiPath, -1)\n\tif len(matches) < 1 {\n\t\treturn nil\n\t}\n\tlastEnd := 0\n\tpattern := `\\A`\n\tfor _, match := range matches {\n\t\tpattern += regexp.QuoteMeta(apiPath[lastEnd:match[0]])\n\t\tpattern += fmt.Sprintf(`(?P<%s>[^/]+)`, apiPath[match[0]+1:match[1]-1])\n\t\tlastEnd = match[1]\n\t}\n\tpattern += regexp.QuoteMeta(apiPath[lastEnd:]) + `\\z`\n\treturn regexp.MustCompile(pattern)\n}\n\n// Helper method to get a munger method mapping, or created one if it doesn't\n// exist.\n// This should be called with the mungerMapping lock held.\nfunc getMungerMethodMapping(method string) *mungerMethodMapping {\n\tmapping, ok := mungerMapping.mungers[method]\n\tif !ok {\n\t\tmapping = &mungerMethodMapping{\n\t\t\trequests:         make(map[string]requestMungerFunc),\n\t\t\trequestPatterns:  make(map[*regexp.Regexp]requestMungerFunc),\n\t\t\tresponses:        make(map[string]responseMungerFunc),\n\t\t\tresponsePatterns: make(map[*regexp.Regexp]responseMungerFunc),\n\t\t}\n\t\tmungerMapping.mungers[method] = mapping\n\t}\n\treturn mapping\n}\n\nfunc RegisterRequestMunger(method, apiPath string, munger requestMungerFunc) {\n\tmungerMapping.Lock()\n\tdefer mungerMapping.Unlock()\n\n\tmapping := getMungerMethodMapping(method)\n\tif pattern := convertPattern(apiPath); pattern == nil {\n\t\tmapping.requests[apiPath] = munger\n\t} else {\n\t\tmapping.requestPatterns[pattern] = munger\n\t}\n}\n\nfunc RegisterResponseMunger(method, apiPath string, munger responseMungerFunc) {\n\tmungerMapping.Lock()\n\tdefer mungerMapping.Unlock()\n\n\tmapping := getMungerMethodMapping(method)\n\tif pattern := convertPattern(apiPath); pattern == nil {\n\t\tmapping.responses[apiPath] = munger\n\t} else {\n\t\tmapping.responsePatterns[pattern] = munger\n\t}\n}\n\nfunc init() {\n\tmungerMapping.mungers = make(map[string]*mungerMethodMapping)\n\terr := json.Unmarshal(models.SwaggerJSON, &dockerSpec)\n\tif err != nil {\n\t\tpanic(\"could not parse embedded spec version\")\n\t}\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/start.go",
    "content": "//go:build linux\n\n/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage dockerproxy\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/linuxkit/virtsock/pkg/vsock\"\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/unix\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/platform\"\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/dockerproxy/util\"\n)\n\nconst (\n\t// defaultProxyEndpoint is the path on which dockerd should listen on,\n\t// relative to the WSL mount point.\n\tdefaultProxyEndpoint = \"rancher-desktop/run/docker.sock\"\n\t// socketExistTimeout is the time to wait for the docker socket to exist\n\tsocketExistTimeout = 30 * time.Second\n\t// fileExistSleep is interval to wait while waiting for a file to exist.\n\tfileExistSleep = 500 * time.Millisecond\n)\n\n// waitForFileToExist will block until the given path exists.  If the given\n// timeout is reached, an error will be returned.\nfunc waitForFileToExist(filePath string, timeout time.Duration) error {\n\ttimer := time.After(timeout)\n\tready := make(chan struct{})\n\texpired := false\n\n\tgo func() {\n\t\tdefer close(ready)\n\t\t// We just do polling here, since inotify / fanotify both have fairly\n\t\t// low limits on the concurrent number of watchers.\n\t\tfor !expired {\n\t\t\t_, err := os.Lstat(filePath)\n\t\t\tif err == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttime.Sleep(fileExistSleep)\n\t\t}\n\t}()\n\n\tselect {\n\tcase <-ready:\n\t\treturn nil\n\tcase <-timer:\n\t\texpired = true\n\t\treturn fmt.Errorf(\"timed out waiting for %s to exist\", filePath)\n\t}\n}\n\nfunc GetDefaultProxyEndpoint() (string, error) {\n\tmountPoint, err := platform.GetWSLMountPoint()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn path.Join(mountPoint, defaultProxyEndpoint), nil\n}\n\n// Start the dockerd process within this WSL distribution on the given vsock\n// port as well as the unix socket at the given path.  All other arguments are\n// passed to dockerd as-is.\n//\n// This function returns after dockerd has exited.\nfunc Start(ctx context.Context, port uint32, dockerSocket string, args []string) error {\n\tdockerd, err := exec.LookPath(\"dockerd\")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not find dockerd: %w\", err)\n\t}\n\n\t// We have dockerd listen on the given docker socket, so that it can be\n\t// used from other distributions (though we still need to do path\n\t// path translation on top).\n\n\terr = os.MkdirAll(path.Dir(dockerSocket), 0o755)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not set up docker socket: %w\", err)\n\t}\n\n\targs = append(args,\n\t\tfmt.Sprintf(\"--host=unix://%s\", dockerSocket),\n\t\t\"--host=unix:///var/run/docker.sock.raw\",\n\t\t\"--host=unix:///var/run/docker.sock\")\n\tcmd := exec.CommandContext(ctx, dockerd, args...)\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not start dockerd: %w\", err)\n\t}\n\n\tdefer func() {\n\t\tif proc := cmd.Process; proc != nil {\n\t\t\terr := proc.Signal(unix.SIGTERM)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"could not kill docker: %s\\n\", err)\n\t\t\t}\n\t\t}\n\t}()\n\n\t// Wait for the docker socket to exist...\n\terr = waitForFileToExist(dockerSocket, socketExistTimeout)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor {\n\t\terr := listenOnVsock(ctx, port, dockerSocket)\n\t\tif err != nil {\n\t\t\tlogrus.Fatalf(\"docker-proxy: error listening on vsock: %s\", err)\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc listenOnVsock(ctx context.Context, port uint32, dockerSocket string) error {\n\tlistener, err := platform.ListenVsockNonBlocking(vsock.CIDAny, port)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not listen on vsock port %08x: %w\", port, err)\n\t}\n\tdefer listener.Close()\n\tlogrus.Infof(\"docker-proxy: listening on vsock port %08x\", port)\n\n\tsigch := make(chan os.Signal, 1)\n\tsignal.Notify(sigch, unix.SIGTERM)\n\tgo func() {\n\t\t<-sigch\n\t\tlistener.Close()\n\t}()\n\n\tfor {\n\t\tconn, err := listener.Accept()\n\t\tif err != nil {\n\t\t\tlogrus.Errorf(\"docker-proxy: error accepting client connection: %s\", err)\n\t\t\tif errors.Is(err, unix.EINVAL) {\n\t\t\t\t// This does not recover; return and re-listen\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tgo handleConnection(ctx, conn, dockerSocket)\n\t}\n}\n\n// handleConnection handles piping the connection from the client to the docker\n// socket.\nfunc handleConnection(ctx context.Context, conn net.Conn, dockerPath string) {\n\tdialer := net.Dialer{}\n\tdockerConn, err := dialer.DialContext(ctx, \"unix\", dockerPath)\n\tif err != nil {\n\t\tlogrus.Errorf(\"could not connect to docker: %s\", err)\n\t\treturn\n\t}\n\tdefer dockerConn.Close()\n\n\t// Cast both client and docker connections to HalfReadWriteCloser for further handling.\n\txConn, ok := conn.(util.HalfReadWriteCloser)\n\tif !ok {\n\t\tlogrus.Errorf(\"client connection does not implement HalfReadWriteCloser\")\n\t\treturn\n\t}\n\n\txDockerConn, ok := dockerConn.(util.HalfReadWriteCloser)\n\tif !ok {\n\t\tlogrus.Errorf(\"docker connection does not implement HalfReadWriteCloser\")\n\t\treturn\n\t}\n\n\t// Pipe data between the client and Docker, ensuring bidirectional data flow.\n\tif err := util.Pipe(xConn, xDockerConn); err != nil {\n\t\tlogrus.Errorf(\"error forwarding data between client and docker: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/swagger-configuration.yaml",
    "content": "# Copyright © 2021 SUSE LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n# This is a go-swagger configuration file to generate the models for moby from\n# its OpenAPI specification.\n\nlayout:\n  models:\n  - name: definition\n    source: asset:model\n    target: \"{{ joinFilePath .Target .ModelPackage }}\"\n    file_name: \"{{ pascalize .Name | snakize }}.go\"\n  application:\n  - name: embedded_spec\n    source: asset:swaggerJsonEmbed\n    target: \"{{ joinFilePath .Target (toPackagePath .ServerPackage) }}\"\n    file_name: \"embedded_spec.go\"\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/util/pipe.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage util\n\nimport (\n\t\"fmt\"\n\t\"io\"\n)\n\nfunc Pipe(c1, c2 HalfReadWriteCloser) error {\n\tioCopy := func(reader io.Reader, writer io.Writer) <-chan error {\n\t\tch := make(chan error)\n\t\tgo func() {\n\t\t\t_, err := io.Copy(writer, reader)\n\t\t\tch <- err\n\t\t}()\n\t\treturn ch\n\t}\n\n\tch1 := ioCopy(c1, c2)\n\tch2 := ioCopy(c2, c1)\n\tfor range 2 {\n\t\tselect {\n\t\tcase err := <-ch1:\n\t\t\tcwErr := c2.CloseWrite()\n\t\t\tif cwErr != nil {\n\t\t\t\treturn fmt.Errorf(\"error closing write end of c2: %w\", cwErr)\n\t\t\t}\n\t\t\tif err != nil && err != io.EOF {\n\t\t\t\treturn err\n\t\t\t}\n\t\tcase err := <-ch2:\n\t\t\tcwErr := c1.CloseWrite()\n\t\t\tif cwErr != nil {\n\t\t\t\treturn fmt.Errorf(\"error closing write end of c1: %w\", cwErr)\n\t\t\t}\n\t\t\tif err != nil && err != io.EOF {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype HalfReadWriteCloser interface {\n\t// CloseWrite closes the write-side of the connection.\n\tCloseWrite() error\n\t// Write is a passthrough to the underlying connection.\n\tio.ReadWriteCloser\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/util/pipe_test.go",
    "content": "/*\nCopyright © 2021 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage util\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// bidirectionalHalfClosePipe is a testing utility that simulates a bidirectional pipe\n// with the ability to half-close connections. It's designed to mimic scenarios\n// like interactive command-line operations where a client can send data and\n// then half-close the connection while waiting for a response.\ntype bidirectionalHalfClosePipe struct {\n\tr io.ReadCloser\n\tw io.WriteCloser\n}\n\n// newBidirectionalHalfClosePipe creates two interconnected bidirectional pipe endpoints.\n//\n// The function returns two bidirectionalHalfClosePipe instances that are connected\n// such that what is written to one's write endpoint can be read from the other's\n// read endpoint, and vice versa.\n//\n// Returns:\n//   - h1: First bidirectional pipe endpoint\n//   - h2: Second bidirectional pipe endpoint\nfunc newBidirectionalHalfClosePipe() (h1, h2 *bidirectionalHalfClosePipe) {\n\tpr1, pw1 := io.Pipe()\n\tpr2, pw2 := io.Pipe()\n\n\th1 = &bidirectionalHalfClosePipe{\n\t\tr: pr1, w: pw2,\n\t}\n\n\th2 = &bidirectionalHalfClosePipe{\n\t\tr: pr2, w: pw1,\n\t}\n\treturn\n}\n\nfunc (h *bidirectionalHalfClosePipe) CloseWrite() error {\n\treturn h.w.Close()\n}\n\nfunc (h *bidirectionalHalfClosePipe) Close() error {\n\twErr := h.w.Close()\n\trErr := h.r.Close()\n\n\tif wErr != nil {\n\t\treturn wErr\n\t}\n\treturn rErr\n}\n\nfunc (h *bidirectionalHalfClosePipe) Read(p []byte) (n int, err error) {\n\treturn h.r.Read(p)\n}\n\nfunc (h *bidirectionalHalfClosePipe) Write(p []byte) (n int, err error) {\n\treturn h.w.Write(p)\n}\n\n// TestPipe verifies the functionality of the bidirectional pipe utility.\n//\n// The test simulates a scenario similar to interactive command execution,\n// such as a docker run -i command, which requires bidirectional communication.\n// This test case mimics scenarios like:\n// - Sending input to a Docker container via stdin\n// - Half-closing the input stream\n// - Receiving output from the container\n//\n// The test steps are:\n// 1. A client sends data\n// 2. The client half-closes the connection\n// 3. The server reads the data\n// 4. The server sends a return response\n// 5. The server half-closes the connection\n//\n// This approach is particularly relevant for interactive Docker runs where\n// the client needs to send input and then wait for the container's response,\n// while maintaining the ability to close streams independently.\nfunc TestPipe(t *testing.T) {\n\th1a, h1b := newBidirectionalHalfClosePipe()\n\th2a, h2b := newBidirectionalHalfClosePipe()\n\tvar wg sync.WaitGroup\n\twg.Add(2)\n\n\t// Goroutine simulating the client-side operation\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tdataToSend := bytes.NewBufferString(\"some data\")\n\t\t_, err := h1a.Write(dataToSend.Bytes())\n\t\tassert.NoError(t, err)\n\t\th1a.CloseWrite()\n\n\t\toutput, err := io.ReadAll(h1a)\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, output, \"return data\")\n\t}()\n\n\t// Goroutine simulating the server-side operation\n\tgo func() {\n\t\tdefer wg.Done()\n\t\toutput, err := io.ReadAll(h2b)\n\t\tassert.NoError(t, err)\n\t\tassert.EqualValues(t, output, \"some data\")\n\n\t\tdataToSend := bytes.NewBufferString(\"return data\")\n\t\t_, err = h2b.Write(dataToSend.Bytes())\n\t\tassert.NoError(t, err)\n\n\t\th2b.CloseWrite()\n\t}()\n\n\terr := Pipe(h1b, h2a)\n\tassert.NoError(t, err)\n\twg.Wait()\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/util/reverse_proxy.go",
    "content": "package util\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"io\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\thostHeaderValue = \"api.moby.localhost\"\n\ttargetProtocol  = \"http://\"\n\t// The amount of time between flushes for a flushWriter\n\tflushInterval = 100 * time.Millisecond\n)\n\n// ReverseProxy is a custom reverse proxy specifically designed for Rancher Desktop's\n// Docker API communication. Unlike the standard library's ReverseProxy, this\n// implementation provides explicit support for half-close connections and\n// HTTP protocol upgrades required by the Docker API.\n//\n// Key design features:\n// - Handles HTTP protocol upgrades (WebSocket-like connections)\n// - Supports half-close TCP connections\n// - Provides hooks for request/response modification\n// - Designed for specific Docker API interaction requirements\ntype ReverseProxy struct {\n\t// Dial provides a custom connection establishment method\n\tDial func(network, addr string) (net.Conn, error)\n\n\t// DialContext provides a custom dial function with context support.\n\t// It overrides Dial if both are set.\n\tDialContext func(ctx context.Context, _, _ string) (net.Conn, error)\n\n\t// Director allows modification of the outgoing request before forwarding\n\tDirector func(*http.Request)\n\n\t// ModifyResponse enables post-processing of the backend response\n\tModifyResponse func(*http.Response) error\n\n\t// ErrorLog defines an optional logger for recording errors encountered\n\t// during request proxying. If not provided, the standard logger from\n\t// the log package is used instead.\n\tErrorLog *log.Logger\n}\n\n// ServeHTTP implements the http.Handler interface, routing incoming\n// HTTP requests through the custom reverse proxy\nfunc (proxy ReverseProxy) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n\tproxy.forwardRequest(rw, r)\n}\n\n// forwardRequest is the core method that handles request proxying,\n// with special handling for Docker API-specific requirements.\n//\n// Primary responsibilities:\n// - Establish backend connection\n// - Forward request to backend\n// - Handle response streaming\n// - Support protocol upgrades\n// - Ensure proper connection management\nfunc (proxy *ReverseProxy) forwardRequest(w http.ResponseWriter, r *http.Request) {\n\t// Early check to ensure the ResponseWriter supports http.Flusher.\n\t// This allows immediate error feedback to the client if the required\n\t// functionality is not available, rather than failing later during streaming.\n\tflusher, ok := w.(http.Flusher)\n\tif !ok {\n\t\tproxy.sendError(w, \"expected http.ResponseWriter to be an http.Flusher\", http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// Leverage the original request's context as the base\n\tctx := r.Context()\n\n\t// Create a new context with cancellation to ensure we can stop the flush\n\t// The context will be canceled when the request is done or if needed earlier\n\tctx, cancel := context.WithCancel(ctx)\n\tdefer cancel()\n\n\tvar backendConn net.Conn\n\tvar err error\n\tif proxy.DialContext != nil {\n\t\t// Establish a connection to the backend using a custom DialContext if provided\n\t\tbackendConn, err = proxy.DialContext(ctx, \"\", \"\")\n\t} else {\n\t\t// Fallback to the custom Dial method if DialContext is not set\n\t\tbackendConn, err = proxy.Dial(\"\", \"\")\n\t}\n\tif err != nil {\n\t\tproxy.sendError(w, \"failed to connect to the backend: \"+err.Error(), http.StatusBadGateway)\n\t\treturn\n\t}\n\tdefer backendConn.Close()\n\n\t// Create a new HTTP request with the same headers\n\turl := targetProtocol + hostHeaderValue + r.RequestURI\n\tnewReq, err := http.NewRequestWithContext(ctx, r.Method, url, r.Body)\n\tif err != nil {\n\t\tproxy.sendError(w, \"failed to create a request for the backend: \"+err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tnewReq.Header = r.Header\n\n\t// Director function\n\t// Allows complete customization of the outgoing request\n\tif proxy.Director != nil {\n\t\tproxy.Director(newReq)\n\t}\n\t// Prevent automatic connection closure\n\tnewReq.Close = false\n\n\t// Forward the modified request to the backend\n\tif err = newReq.Write(backendConn); err != nil {\n\t\tproxy.sendError(w, \"failed to forward the request to the backend: \"+err.Error(), http.StatusBadGateway)\n\t\treturn\n\t}\n\n\t// Read the response from the backend\n\tbufferedReader := bufio.NewReader(backendConn)\n\tbackendResponse, err := http.ReadResponse(bufferedReader, newReq)\n\tif err != nil {\n\t\tproxy.sendError(w, \"failed to read the response from the backend: \"+err.Error(), http.StatusBadGateway)\n\t\treturn\n\t}\n\tdefer backendResponse.Body.Close()\n\t// ModifyResponse function\n\t// Allows post-processing of the backend response\n\tif proxy.ModifyResponse != nil {\n\t\terr := proxy.ModifyResponse(backendResponse)\n\t\tif err != nil {\n\t\t\tproxy.sendError(w, \"failed to modify the response from the backend: \"+err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Propagate backend response headers to the client\n\tfor key, values := range backendResponse.Header {\n\t\tfor _, value := range values {\n\t\t\tw.Header().Add(key, value)\n\t\t}\n\t}\n\n\t// Write the response status code and headers and flush it immediately\n\tw.WriteHeader(backendResponse.StatusCode)\n\tflusher.Flush()\n\n\t// Check if the response has a status code of 101 (Switching Protocols)\n\tif backendResponse.StatusCode == http.StatusSwitchingProtocols {\n\t\t// When reading the response, the buffered reader may have consumed part of the body\n\t\t// beyond the headers. We need to recover these overread bytes and ensure they are\n\t\t// sent to the client immediately after hijacking the connection.\n\t\tbufferedBytesLen := bufferedReader.Buffered()\n\t\tpendingResponseBytes, err := bufferedReader.Peek(bufferedBytesLen)\n\t\tif err != nil {\n\t\t\tproxy.logf(\"failed to peek for buffered bytes: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tproxy.handleUpgradedConnection(w, backendConn, pendingResponseBytes)\n\t\treturn\n\t}\n\n\t// Stream the response body back to the client\n\t// flushedWriter is a critical component for supporting\n\t// long-running, streaming connections like \"docker log -f\"\n\tfw := newFlushedWriter(ctx, w)\n\t_, err = io.Copy(fw, backendResponse.Body)\n\tfw.stopFlushing()\n\tif err != nil {\n\t\tproxy.logf(\"failed to stream the response body to the client: %v\", err)\n\t}\n}\n\n// handleUpgradedConnection manages HTTP protocol upgrades (e.g., WebSocket),\n// specifically tailored for Docker API's hijacking mechanism.\n//\n// This method:\n// - Hijacks the existing connection\n// - Handling any buffered data that was overread during the initial response parsing\n// - Enables bidirectional communication after protocol upgrade\nfunc (proxy *ReverseProxy) handleUpgradedConnection(w http.ResponseWriter, backendConn net.Conn, pendingResponseBytes []byte) {\n\t// Cast writer to safely hijack the connection\n\thijacker, ok := w.(http.Hijacker)\n\tif !ok {\n\t\tproxy.logf(\"client response writer does not support http.Hijacker\")\n\t\treturn\n\t}\n\n\t// Hijack attempts to take control of the underlying connection\n\t// Returns:\n\t// - clientConn: The raw network connection\n\t// - bufferedClientConn: A buffered reader/writer for any pending data\n\tclientConn, bufferedClientConn, err := hijacker.Hijack()\n\tif err != nil {\n\t\tproxy.logf(\"cannot hijack client connection: %v\", err)\n\t\treturn\n\t}\n\tdefer clientConn.Close()\n\n\t// Flush any buffered data in the writer to ensure no data is lost\n\tif bufferedClientConn.Writer.Buffered() > 0 {\n\t\tif err := bufferedClientConn.Flush(); err != nil {\n\t\t\tproxy.logf(\"failed to flush client connection: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// Process any data already buffered in the reader before full duplex communication\n\t// This prevents losing any data that might have been read but not yet processed\n\tif bufferedLen := bufferedClientConn.Reader.Buffered(); bufferedLen > 0 {\n\t\tbufferedData := make([]byte, bufferedLen)\n\t\t_, err := bufferedClientConn.Read(bufferedData)\n\t\tif err != nil {\n\t\t\tproxy.logf(\"failed to read buffered data from the client: %v\", err)\n\t\t\treturn\n\t\t}\n\t\t_, err = backendConn.Write(bufferedData)\n\t\tif err != nil {\n\t\t\tproxy.logf(\"failed to write buffered data to the backend: %v\", err)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// After hijacking the connection, send any pending bytes that were overread by the buffered reader\n\t// during the initial response parsing. This ensures no data is lost during the protocol upgrade.\n\t_, err = clientConn.Write(pendingResponseBytes)\n\tif err != nil {\n\t\tproxy.logf(\"failed to write pending response data the client: %v\", err)\n\t\treturn\n\t}\n\n\t// Cast backend and client connections to HalfReadWriteCloser\n\tvar halfCloserBackendConn HalfReadWriteCloser\n\tvar halfCloserClientConn HalfReadWriteCloser\n\tif halfCloser, ok := backendConn.(HalfReadWriteCloser); !ok {\n\t\tproxy.logf(\"backend connection does not implement HalfReadCloseWriter\")\n\t\treturn\n\t} else {\n\t\thalfCloserBackendConn = halfCloser\n\t}\n\tif halfCloser, ok := clientConn.(HalfReadWriteCloser); !ok {\n\t\tproxy.logf(\"client connection does not implement HalfReadCloseWriter\")\n\t\treturn\n\t} else {\n\t\thalfCloserClientConn = halfCloser\n\t}\n\n\t// Establish a bidirectional pipe between client and backend connections\n\t// This allows full-duplex communication with support for half-closes\n\t// Critical for Docker API's stream-based communication model\n\tif err := Pipe(halfCloserClientConn, halfCloserBackendConn); err != nil {\n\t\tproxy.logf(\"piping client to backend failed: %v\", err)\n\t}\n}\n\nfunc (proxy *ReverseProxy) sendError(w http.ResponseWriter, msg string, statusCode int) {\n\tproxy.logf(\"%s\", msg)\n\thttp.Error(w, msg, statusCode)\n}\n\nfunc (proxy *ReverseProxy) logf(format string, args ...any) {\n\tlogger := proxy.ErrorLog\n\tif logger == nil {\n\t\tlogger = log.Default()\n\t}\n\tlogger.Printf(format, args...)\n}\n\n// flushedWriter wraps an io.Writer with periodic flushing capability.\n// It ensures that data is periodically flushed to the underlying writer.\ntype flushedWriter struct {\n\tw      io.Writer          // Underlying writer to which data is written.\n\tmu     sync.Mutex         // Mutex to protect concurrent access to the writer and dirty flag.\n\tctx    context.Context    // Context to control the lifecycle of the periodic flusher.\n\tcancel context.CancelFunc // Cancels the periodic flusher context.\n\tdirty  bool               // Flag indicating whether the writer may have unflushed data.\n}\n\n// NewFlushedWriter creates and initializes a new flushedWriter instance.\n// The provided writer w must implement both io.Writer and http.Flusher interfaces.\n// If w does not implement http.Flusher, the writer will work but no periodic\n// flushing will be performed. It is the caller's responsibility to ensure\n// that w implements http.Flusher before instantiation if periodic flushing\n// is required.\nfunc newFlushedWriter(ctx context.Context, w io.Writer) *flushedWriter {\n\tflushCtx, flushCancel := context.WithCancel(ctx)\n\tfw := &flushedWriter{\n\t\tw:      w,\n\t\tctx:    flushCtx,\n\t\tcancel: flushCancel,\n\t}\n\n\t// periodicFlusher runs a loop that periodically flushes the writer\n\tperiodicFlusher := func(flusher http.Flusher) {\n\t\tticker := time.NewTicker(flushInterval)\n\t\tdefer ticker.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-fw.ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\t\t\t\tfw.mu.Lock()\n\t\t\t\tif fw.dirty {\n\t\t\t\t\tflusher.Flush()\n\t\t\t\t\tfw.dirty = false\n\t\t\t\t}\n\t\t\t\tfw.mu.Unlock()\n\t\t\t}\n\t\t}\n\t}\n\n\t// Type assert the writer to http.Flusher\n\tif flusher, ok := w.(http.Flusher); ok {\n\t\t// Start periodic flushing in a goroutine\n\t\tgo periodicFlusher(flusher)\n\t}\n\n\treturn fw\n}\n\n// Write implements io.Writer and protects the underlying writer with a mutex\nfunc (fw *flushedWriter) Write(p []byte) (n int, err error) {\n\tfw.mu.Lock()\n\tdefer fw.mu.Unlock()\n\n\tn, err = fw.w.Write(p)\n\tif n > 0 {\n\t\tfw.dirty = true\n\t}\n\treturn n, err\n}\n\n// stopFlushing stops the periodic flusher and clears any pending flush state.\n// It should be called after streaming completes to avoid concurrent flushes on close.\nfunc (fw *flushedWriter) stopFlushing() {\n\tfw.mu.Lock()\n\tfw.dirty = false\n\tfw.cancel()\n\tfw.mu.Unlock()\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/dockerproxy/util/reverse_proxy_test.go",
    "content": "/*\nCopyright © 2026 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n    http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage util\n\nimport (\n\t\"io\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFlushedWriterPeriodicFlush(t *testing.T) {\n\tt.Parallel()\n\n\tflusher := newRecordingFlusher()\n\twriter := newFlushedWriter(t.Context(), flusher)\n\n\t_, err := writer.Write([]byte(\"data\"))\n\tassert.NoError(t, err)\n\n\tselect {\n\tcase <-flusher.flushCh:\n\tcase <-time.After(flushInterval * flushTestTimeoutMultiplier):\n\t\tt.Fatal(\"expected periodic flush\")\n\t}\n}\n\nfunc TestFlushedWriterStopFlushingSuppressesPeriodicFlush(t *testing.T) {\n\tt.Parallel()\n\n\tflusher := newRecordingFlusher()\n\twriter := newFlushedWriter(t.Context(), flusher)\n\n\t_, err := writer.Write([]byte(\"data\"))\n\tassert.NoError(t, err)\n\n\twriter.mu.Lock()\n\twriter.dirty = true\n\twriter.mu.Unlock()\n\n\twriter.stopFlushing()\n\n\tassert.False(t, writer.dirty)\n\tselect {\n\tcase <-writer.ctx.Done():\n\tdefault:\n\t\tt.Fatal(\"expected flush context to be canceled\")\n\t}\n}\n\nconst flushTestTimeoutMultiplier = 5\n\ntype recordingFlusher struct {\n\tio.Writer\n\tflushCh   chan struct{}\n\tcloseOnce sync.Once\n}\n\nfunc newRecordingFlusher() *recordingFlusher {\n\treturn &recordingFlusher{\n\t\tWriter:  io.Discard,\n\t\tflushCh: make(chan struct{}),\n\t}\n}\n\nfunc (writer *recordingFlusher) Flush() {\n\twriter.closeOnce.Do(func() {\n\t\tclose(writer.flushCh)\n\t})\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/integration/docker_linux.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage integration\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"slices\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\nconst (\n\tpluginDirsKey = \"cliPluginsExtraDirs\"\n\tcredsStoreKey = \"credsStore\"\n\n\t//nolint:gosec // This is not a credential, it's a file name.\n\t// The file name of the docker Windows credential helper.\n\tdockerCredentialWinCredExe = \"wincred.exe\"\n)\n\n// UpdateDockerConfig configures docker CLI to load plugins from the directory\n// given. It also sets the credential helper to wincred.exe.\nfunc UpdateDockerConfig(ctx context.Context, homeDir, pluginPath string, enabled bool) error {\n\tconfigPath := filepath.Join(homeDir, \".docker\", \"config.json\")\n\tconfig := make(map[string]any)\n\n\tconfigBytes, err := os.ReadFile(configPath)\n\tif errors.Is(err, os.ErrNotExist) {\n\t\t// If the config file does not exist, start with empty map.\n\t\tif !enabled {\n\t\t\treturn nil\n\t\t}\n\t} else if err != nil {\n\t\treturn fmt.Errorf(\"could not read docker CLI configuration: %w\", err)\n\t} else {\n\t\tif err = json.Unmarshal(configBytes, &config); err != nil {\n\t\t\treturn fmt.Errorf(\"could not parse docker CLI configuration: %w\", err)\n\t\t}\n\t}\n\n\treplaceCredsStore := true\n\tif credsStoreRaw, ok := config[credsStoreKey]; ok {\n\t\tif credsStore, ok := credsStoreRaw.(string); ok {\n\t\t\treplaceCredsStore = !isCredHelperWorking(ctx, credsStore)\n\t\t}\n\t}\n\tif replaceCredsStore {\n\t\tconfig[credsStoreKey] = dockerCredentialWinCredExe\n\t}\n\n\tvar dirs []string\n\n\tif dirsRaw, ok := config[pluginDirsKey]; ok {\n\t\tif dirsAny, ok := dirsRaw.([]any); ok {\n\t\t\tfor _, item := range dirsAny {\n\t\t\t\tif dir, ok := item.(string); ok {\n\t\t\t\t\tdirs = append(dirs, dir)\n\t\t\t\t} else {\n\t\t\t\t\treturn fmt.Errorf(\"failed to update docker CLI configuration: %q has non-string item %v\", pluginDirsKey, item)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\treturn fmt.Errorf(\"failed to update docker CLI configuration: %q is not a string array\", pluginDirsKey)\n\t\t}\n\t\tindex := slices.Index(dirs, pluginPath)\n\t\tif enabled {\n\t\t\tif index >= 0 {\n\t\t\t\t// Config file already contains the plugin path; nothing to do.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdirs = append([]string{pluginPath}, dirs...)\n\t\t} else {\n\t\t\tif index < 0 {\n\t\t\t\t// Config does not contain the plugin path; nothing to do.\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tdirs = slices.Delete(dirs, index, index+1)\n\t\t}\n\t} else {\n\t\tif !enabled {\n\t\t\t// The key does not exist, and we don't want it; nothing to do.\n\t\t\treturn nil\n\t\t}\n\t\t// The key does not exist; add it.\n\t\tdirs = []string{pluginPath}\n\t}\n\tif len(dirs) > 0 {\n\t\tconfig[pluginDirsKey] = dirs\n\t} else {\n\t\tdelete(config, pluginDirsKey)\n\t}\n\n\tif configBytes, err = json.Marshal(config); err != nil {\n\t\treturn fmt.Errorf(\"failed to serialize updated docker CLI configuration: %w\", err)\n\t}\n\n\tif err = os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {\n\t\treturn fmt.Errorf(\"failed to update docker CLI configuration: could not create parent: %w\", err)\n\t}\n\n\tif err = os.WriteFile(configPath, configBytes, 0o644); err != nil {\n\t\treturn fmt.Errorf(\"failed to update docker CLI configuration: %w\", err)\n\t}\n\n\treturn nil\n}\n\n// isCredHelperWorking verifies that the credential helper can be called, and doesn't need to be replaced.\nfunc isCredHelperWorking(ctx context.Context, credsStore string) bool {\n\t// The proprietary \"desktop\" helper is always replaced with the default helper.\n\tif credsStore == \"\" || credsStore == \"desktop\" || credsStore == \"desktop.exe\" {\n\t\treturn false\n\t}\n\tcredHelper := fmt.Sprintf(\"docker-credential-%s\", credsStore)\n\treturn exec.CommandContext(ctx, credHelper, \"list\").Run() == nil\n}\n\n// RemoveObsoletePluginSymlinks removes symlinks in the docker CLI plugin\n// directory which are children of the given directory.\nfunc RemoveObsoletePluginSymlinks(homeDir, binPath string) error {\n\tpluginDir := path.Join(homeDir, \".docker\", \"cli-plugins\")\n\tentries, err := os.ReadDir(pluginDir)\n\tif err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\t// If the plugin directory does not exist, there is nothing to do.\n\t\t\tlogrus.Debugf(\"Docker CLI plugins directory %q does not exist\", pluginDir)\n\t\t\treturn nil\n\t\t}\n\t\treturn fmt.Errorf(\"failed to enumerate docker CLI plugins: %w\", err)\n\t}\n\tfor _, entry := range entries {\n\t\tif entry.Type()&os.ModeSymlink != os.ModeSymlink {\n\t\t\t// entry is not a symlink; ignore it.\n\t\t\tlogrus.Debugf(\"Plugin %q is not a symlink\", entry.Name())\n\t\t\tcontinue\n\t\t}\n\t\tentryPath := path.Join(pluginDir, entry.Name())\n\t\ttarget, err := os.Readlink(entryPath)\n\t\tif err != nil {\n\t\t\tlogrus.Debugf(\"Error reading plugin symlink %q: %v\", entryPath, err)\n\t\t} else if filepath.Dir(target) == binPath {\n\t\t\t// Remove the symlink, ignoring any errors.\n\t\t\t_ = os.Remove(entryPath)\n\t\t} else {\n\t\t\tlogrus.Debugf(\"Plugin symlink %q does not start with %q\", target, binPath)\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/integration/docker_linux_test.go",
    "content": "/*\nCopyright © 2024 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage integration_test\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/integration\"\n)\n\nfunc TestUpdateDockerConfig(t *testing.T) {\n\tt.Parallel()\n\tt.Run(\"create config file\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tpluginPath := t.TempDir()\n\n\t\tassert.NoError(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, true))\n\n\t\tbytes, err := os.ReadFile(path.Join(homeDir, \".docker\", \"config.json\"))\n\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\tvar config map[string]any\n\t\trequire.NoError(t, json.Unmarshal(bytes, &config))\n\n\t\tvalue := config[\"cliPluginsExtraDirs\"]\n\t\trequire.Contains(t, config, \"cliPluginsExtraDirs\")\n\t\trequire.Contains(t, value, pluginPath, \"did not contain plugin path\")\n\t\tcredStore := config[\"credsStore\"]\n\t\trequire.Equal(t, credStore, \"wincred.exe\")\n\t})\n\tt.Run(\"update config file\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tpluginPath := t.TempDir()\n\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\t\texistingContents := []byte(`{\"credsStore\": \"nothing\"}`)\n\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\n\t\trequire.NoError(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, true))\n\n\t\tbytes, err := os.ReadFile(path.Join(homeDir, \".docker\", \"config.json\"))\n\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\tvar config map[string]any\n\t\trequire.NoError(t, json.Unmarshal(bytes, &config))\n\n\t\tassert.Subset(t, config, map[string]any{\"cliPluginsExtraDirs\": []any{pluginPath}})\n\t\tassert.Subset(t, config, map[string]any{\"credsStore\": \"wincred.exe\"})\n\t})\n\tt.Run(\"do not add multiple instances\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tpluginPath := t.TempDir()\n\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\n\t\texpected := []any{\"1\", pluginPath, \"2\"}\n\t\tconfig := map[string]any{\"cliPluginsExtraDirs\": expected}\n\t\texistingContents, err := json.Marshal(config)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\t\tconfig = make(map[string]any)\n\n\t\trequire.NoError(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, true))\n\n\t\tbytes, err := os.ReadFile(configPath)\n\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\trequire.NoError(t, json.Unmarshal(bytes, &config))\n\n\t\tassert.Subset(t, config, map[string]any{\"cliPluginsExtraDirs\": expected})\n\t})\n\tt.Run(\"remove existing instances\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tpluginPath := t.TempDir()\n\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\n\t\tconfig := map[string]any{\"cliPluginsExtraDirs\": []any{\"1\", pluginPath, \"2\"}}\n\t\texistingContents, err := json.Marshal(config)\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\t\tconfig = make(map[string]any)\n\n\t\trequire.NoError(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, false))\n\n\t\tbytes, err := os.ReadFile(configPath)\n\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\trequire.NoError(t, json.Unmarshal(bytes, &config))\n\n\t\tassert.Subset(t, config, map[string]any{\"cliPluginsExtraDirs\": []any{\"1\", \"2\"}})\n\t})\n\tt.Run(\"do not modify invalid file\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tt.Run(\"file is not JSON\", func(t *testing.T) {\n\t\t\thomeDir := t.TempDir()\n\t\t\tpluginPath := t.TempDir()\n\t\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\t\t\texistingContents := []byte(`this is not JSON`)\n\t\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\n\t\t\tassert.Error(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, true))\n\n\t\t\tbytes, err := os.ReadFile(configPath)\n\t\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\t\tassert.Equal(t, existingContents, bytes, \"docker CLI config was changed\")\n\t\t})\n\t\tt.Run(\"file contains invalid plugin dirs\", func(t *testing.T) {\n\t\t\thomeDir := t.TempDir()\n\t\t\tpluginPath := t.TempDir()\n\t\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\n\t\t\tconfig := map[string]any{\"cliPluginsExtraDirs\": 500}\n\t\t\texistingContents, err := json.MarshalIndent(config, \" \\t \", \"  \\n\\r  \")\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\n\t\t\trequire.Error(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, false))\n\n\t\t\tbytes, err := os.ReadFile(configPath)\n\t\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\t\t// Since we should not have modified the file at all, the file should\n\t\t\t// still be byte-identical.\n\t\t\tassert.Equal(t, existingContents, bytes, \"docker CLI config was modified\")\n\t\t})\n\t\tt.Run(\"file contains non-string plugin dirs items\", func(t *testing.T) {})\n\t\thomeDir := t.TempDir()\n\t\tpluginPath := t.TempDir()\n\t\tconfigPath := path.Join(homeDir, \".docker\", \"config.json\")\n\t\trequire.NoError(t, os.MkdirAll(filepath.Dir(configPath), 0o755))\n\n\t\titems := []any{1, true, map[string]any{\"hello\": \"world\"}}\n\t\tconfig := map[string]any{\"cliPluginsExtraDirs\": items}\n\t\texistingContents, err := json.MarshalIndent(config, \" \\t \", \"  \\n\\r  \")\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, os.WriteFile(configPath, existingContents, 0o644))\n\n\t\trequire.Error(t, integration.UpdateDockerConfig(t.Context(), homeDir, pluginPath, false))\n\n\t\tbytes, err := os.ReadFile(configPath)\n\t\trequire.NoError(t, err, \"error reading docker CLI config\")\n\t\t// Since we should not have modified the file at all, the file should\n\t\t// still be byte-identical.\n\t\tassert.Equal(t, existingContents, bytes, \"docker CLI config was modified\")\n\t})\n}\n\nfunc TestRemoveObsoletePluginSymlinks(t *testing.T) {\n\tt.Run(\"plugin directory does not exist\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tbinPath := t.TempDir()\n\t\tassert.NoError(t, integration.RemoveObsoletePluginSymlinks(homeDir, binPath))\n\t})\n\tt.Run(\"leaves non-symlink plugins\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tbinPath := t.TempDir()\n\t\tpluginDir := path.Join(homeDir, \".docker\", \"cli-plugins\")\n\t\tassert.NoError(t, os.MkdirAll(pluginDir, 0o755))\n\t\tpluginPath := path.Join(pluginDir, \"docker-plugin\")\n\t\tassert.NoError(t, os.WriteFile(pluginPath, []byte{}, 0o755))\n\t\tassert.NoError(t, integration.RemoveObsoletePluginSymlinks(homeDir, binPath))\n\t\tcontents, err := os.ReadFile(pluginPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Empty(t, contents)\n\t})\n\tt.Run(\"leaves foreign symlinks\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tbinPath := t.TempDir()\n\t\tpluginDir := path.Join(homeDir, \".docker\", \"cli-plugins\")\n\t\tassert.NoError(t, os.MkdirAll(pluginDir, 0o755))\n\t\tpluginPath := path.Join(pluginDir, \"docker-plugin\")\n\t\tassert.NoError(t, os.Symlink(\"/usr/bin/true\", pluginPath))\n\t\tassert.NoError(t, integration.RemoveObsoletePluginSymlinks(homeDir, binPath))\n\t\tsymlinkTarget, err := os.Readlink(pluginPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, \"/usr/bin/true\", symlinkTarget)\n\t})\n\tt.Run(\"leaves self-referential symlinks\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tbinPath := t.TempDir()\n\t\tpluginDir := path.Join(homeDir, \".docker\", \"cli-plugins\")\n\t\tassert.NoError(t, os.MkdirAll(pluginDir, 0o755))\n\t\tpluginPath := path.Join(pluginDir, \"docker-plugin\")\n\t\tassert.NoError(t, os.Symlink(pluginPath, pluginPath))\n\t\tassert.NoError(t, integration.RemoveObsoletePluginSymlinks(homeDir, binPath))\n\t\tsymlinkTarget, err := os.Readlink(pluginPath)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, pluginPath, symlinkTarget)\n\t})\n\tt.Run(\"removes symlinks\", func(t *testing.T) {\n\t\thomeDir := t.TempDir()\n\t\tbinPath := t.TempDir()\n\t\tpluginDir := path.Join(homeDir, \".docker\", \"cli-plugins\")\n\t\tassert.NoError(t, os.MkdirAll(pluginDir, 0o755))\n\t\tpluginPath := path.Join(pluginDir, \"docker-plugin\")\n\t\ttargetPath := path.Join(binPath, \"does-not-exist\")\n\t\tassert.NoError(t, os.Symlink(targetPath, pluginPath))\n\t\tassert.NoError(t, integration.RemoveObsoletePluginSymlinks(homeDir, binPath))\n\t\t_, err := os.Readlink(pluginPath)\n\t\tassert.ErrorIs(t, err, os.ErrNotExist)\n\t})\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/integration/integration.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package integration manages the marker file to indicate if a WSL distribution\n// is being integrated with Rancher Desktop.\npackage integration\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n)\n\nconst (\n\tmarkerPath                = \"/.rancher-desktop-integration\"\n\tmarkerContents            = \"This file is used to mark Rancher Desktop WSL integration.\\n\"\n\tintegrationFilePermission = 0o644\n)\n\n// Set the current distribution as being integrated with Rancher Desktop.\nfunc Set() error {\n\treturn os.WriteFile(markerPath, []byte(markerContents), integrationFilePermission)\n}\n\n// Delete any markers claiming the current distribution is integrated with\n// Rancher Desktop.\nfunc Delete() error {\n\tif err := os.Remove(markerPath); err != nil {\n\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Check if the current distribution is being integrated with Rancher Desktop;\n// prints, on stdout, either \"true\", \"false\", or an error message.\nfunc Show() error {\n\tif _, err := os.Stat(markerPath); err == nil {\n\t\tfmt.Println(\"true\")\n\t} else if errors.Is(err, os.ErrNotExist) {\n\t\tfmt.Println(\"false\")\n\t} else {\n\t\tfmt.Printf(\"%s\\n\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/process/imports_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport \"golang.org/x/sys/windows\"\n\nvar (\n\tkernel32Dll           = windows.NewLazySystemDLL(\"kernel32.dll\")\n\topenProcess           = kernel32Dll.NewProc(\"OpenProcess\")\n\tattachConsole         = kernel32Dll.NewProc(\"AttachConsole\")\n\tfreeConsole           = kernel32Dll.NewProc(\"FreeConsole\")\n\tsetConsoleCtrlHandler = kernel32Dll.NewProc(\"SetConsoleCtrlHandler\")\n)\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/process/kill_others_linux.go",
    "content": "package process\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strconv\"\n\t\"syscall\"\n\n\t\"github.com/sirupsen/logrus\"\n)\n\n// KillOthers will kill any other processes with the executable.\nfunc KillOthers(args ...string) error {\n\tselfPid := fmt.Sprintf(\"%d\", os.Getpid())\n\tselfFile, err := os.Readlink(\"/proc/self/exe\")\n\tif err != nil {\n\t\tlogrus.WithError(err).Error(\"could not read /proc/self/exe\")\n\t\treturn err\n\t}\n\t// We compare the arguments against /proc/*/cmdline, which contains a null-\n\t// separated list of arguments.  Convert it a byte array here so we can do\n\t// a bytes.Compare later.\n\tvar argsBytes []byte\n\tfor _, arg := range args {\n\t\targsBytes = append(argsBytes, []byte(arg)...)\n\t\targsBytes = append(argsBytes, byte(0))\n\t}\n\tvar pids []int\n\t// Read /proc, ignoring errors - any entries we _could_ read are returned.\n\tprocs, _ := os.ReadDir(\"/proc\")\n\tfor _, proc := range procs {\n\t\tif !proc.IsDir() || proc.Name() == selfPid || proc.Name() == \"self\" {\n\t\t\tcontinue\n\t\t}\n\t\tprocFile, err := os.Readlink(path.Join(\"/proc\", proc.Name(), \"exe\"))\n\t\tif err != nil {\n\t\t\t// pid died, or we don't have permissions, or it's not a pid.\n\t\t\tif !errors.Is(err, os.ErrNotExist) {\n\t\t\t\tlogrus.WithError(err).WithField(\"pid\", proc.Name()).Debug(\"could not read exe\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif selfFile != procFile {\n\t\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\t\"pid\":                 proc.Name(),\n\t\t\t\t\"expected executable\": selfFile,\n\t\t\t\t\"executable\":          procFile,\n\t\t\t}).Trace(\"pid has different executable\")\n\t\t\tcontinue\n\t\t}\n\t\tprocCmd, err := os.ReadFile(path.Join(\"/proc\", proc.Name(), \"cmdline\"))\n\t\tif err != nil {\n\t\t\t// pid died, or we don't have permissions, or it's not a pid.\n\t\t\tlogrus.WithError(err).WithField(\"pid\", proc.Name()).Debug(\"could not read command line\")\n\t\t\tcontinue\n\t\t}\n\t\tprocArgs := bytes.SplitN(procCmd, []byte{0}, 2)\n\t\tif len(procArgs) < 2 {\n\t\t\tlogrus.WithField(\"pid\", proc.Name()).Trace(\"pid has no args\")\n\t\t\tcontinue\n\t\t} else if !bytes.HasPrefix(procArgs[1], argsBytes) {\n\t\t\t// pid args are not the expected args\n\t\t\tlogrus.WithFields(logrus.Fields{\n\t\t\t\t\"pid\":           proc.Name(),\n\t\t\t\t\"expected args\": string(argsBytes),\n\t\t\t\t\"actual args\":   string(procArgs[1]),\n\t\t\t}).Trace(\"pid has incorrect arguments\")\n\t\t\tcontinue\n\t\t}\n\t\tpid, err := strconv.Atoi(proc.Name())\n\t\tif err == nil {\n\t\t\tpids = append(pids, pid)\n\t\t}\n\t}\n\tfor _, pid := range pids {\n\t\tlogrus.WithField(\"pid\", pid).Debug(\"Attempting to kill pid\")\n\t\tproc, err := os.FindProcess(pid)\n\t\tif err == nil {\n\t\t\terr = proc.Signal(syscall.SIGTERM)\n\t\t\tif err != nil {\n\t\t\t\tlogrus.WithError(err).WithField(\"pid\", pid).Info(\"could not kill process; ignoring.\")\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/process/kill_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n//nolint:stylecheck // Win32 constant\nconst (\n\tATTACH_PARENT_PROCESS = math.MaxUint32\n)\n\nfunc Kill(pid int) error {\n\tif pid < 1 {\n\t\treturn fmt.Errorf(\"cannot kill process: invalid pid: %d\", pid)\n\t}\n\n\t// Try to re-attach to the default console\n\tdefer func() {\n\t\t_, _, _ = freeConsole.Call()\n\t\t_, _, _ = attachConsole.Call(ATTACH_PARENT_PROCESS)\n\t}()\n\n\t// Detach from the current console; if this fails (and we stay attached to the\n\t// current console), AttachConsole() will fail later, so we don't need to\n\t// check the return value.\n\t_, _, _ = freeConsole.Call()\n\n\trv, _, err := attachConsole.Call(uintptr(pid))\n\tif rv == 0 {\n\t\treturn fmt.Errorf(\"failed to attach to console: %w\", err)\n\t}\n\t// Prevent _this_ process from being affected by Ctrl+C (so we exit cleanly).\n\t// Ignore any errors if this fails.\n\t_, _, _ = setConsoleCtrlHandler.Call(uintptr(unsafe.Pointer(nil)), 1)\n\n\terr = windows.GenerateConsoleCtrlEvent(windows.CTRL_C_EVENT, 0)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to generate Ctrl+C: %w\", err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/process/run_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os/exec\"\n)\n\nfunc Launch(ctx context.Context, executable string, args ...string) error {\n\terr := exec.CommandContext(ctx, executable, args...).Start()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to start %s: %w\", executable, err)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/process/wait_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage process\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// WaitPid waits for the process with the given PID to exit before returning.\nfunc WaitPid(pid uint32) error {\n\tlogEntry := logrus.WithField(\"pid\", pid)\n\tlogEntry.Trace(\"trying to wait for process\")\n\thProcRaw, _, err := openProcess.Call(\n\t\twindows.SYNCHRONIZE,\n\t\t0,\n\t\tuintptr(pid),\n\t)\n\tif hProcRaw == 0 {\n\t\treturn fmt.Errorf(\"could not get handle to process %d: %w\", pid, err)\n\t}\n\thProc := windows.Handle(hProcRaw)\n\tdefer func() { _ = windows.CloseHandle(hProc) }()\n\n\tlogEntry.Trace(\"waiting for process\")\n\tresult, err := windows.WaitForSingleObject(hProc, windows.INFINITE)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to wait for process %d: %w\", pid, err)\n\t}\n\tlogEntry.WithField(\"result\", result).Trace(\"finished waiting for process\")\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/version/version.go",
    "content": "package version\n\nvar Version = \"0.0.0\"\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/wsl-utils/doc.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package wslutils retrieves information about WSL.\npackage wslutils\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/wsl-utils/install_windows.go",
    "content": "package wslutils\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os/exec\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// UpdateWSL runs wsl.exe to update the WSL kernel.  This assumes that WSL had\n// already been installed.  This may request elevation.\nfunc UpdateWSL(ctx context.Context, log *logrus.Entry) error {\n\t// Similar to InstallWSL, the best we have so far is just to spawn wsl.exe and\n\t// hope it does the right thing.  We can technically fetch the .msixbundle\n\t// from https://api.github.com/repos/Microsoft/WSL/releases/latest and install\n\t// it with PackageManager.AddPackageAsync but that isn't really useful.\n\tnewRunnerFunc := NewWSLRunner\n\tif f := ctx.Value(&kWSLExeOverride); f != nil {\n\t\tnewRunnerFunc = f.(func() WSLRunner)\n\t}\n\t// WSL install hangs if we set stdout; don't set that here.\n\trunner := newRunnerFunc().WithStderr(log.WriterLevel(logrus.InfoLevel))\n\terr := runner.Run(ctx, \"--update\")\n\tif err != nil {\n\t\t// Since we're running from Windows Installer, `wsl --update` will fail\n\t\t// because the Windows Installer database is locked (by us).  It will,\n\t\t// however, succeed the next time we use WSL.  It seems to return\n\t\t// STATUS_CONTROL_C_EXIT in this case, so catch that error code and\n\t\t// ignore it.  Unfortunately, this means we can't check that the update\n\t\t// has succeeded (because it hasn't, yet).\n\t\tvar exitError *exec.ExitError\n\t\tif errors.As(err, &exitError) {\n\t\t\tif exitError.ExitCode() == int(windows.STATUS_CONTROL_C_EXIT) {\n\t\t\t\tlog.WithError(err).Trace(\"wsl --update exited with error as expected (ignoring)\")\n\t\t\t} else {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/wsl-utils/run_windows.go",
    "content": "package wslutils\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\n// WSLRunner is an interface to describe running wsl.exe\ntype WSLRunner interface {\n\t// WithStdout causes the command to emit stdout to the given writer, instead\n\t// of os.Stdout.\n\tWithStdout(io.Writer) WSLRunner\n\t// WithStdErr causes the command to emit stderr to the given writer, instead\n\t// of os.Stderr.\n\tWithStderr(io.Writer) WSLRunner\n\t// Run the command and return any errors.\n\tRun(ctx context.Context, args ...string) error\n}\n\ntype wslRunnerImpl struct {\n\tstdout io.Writer\n\tstderr io.Writer\n\trunFn  func(context.Context, ...string) error\n}\n\nfunc NewWSLRunner() WSLRunner {\n\tresult := &wslRunnerImpl{}\n\tresult.runFn = result.run\n\treturn result\n}\n\nfunc (r *wslRunnerImpl) WithStdout(w io.Writer) WSLRunner {\n\tr.stdout = w\n\treturn r\n}\n\nfunc (r *wslRunnerImpl) WithStderr(w io.Writer) WSLRunner {\n\tr.stderr = w\n\treturn r\n}\n\nfunc (r *wslRunnerImpl) Run(ctx context.Context, args ...string) error {\n\treturn r.runFn(ctx, args...)\n}\n\nfunc (r *wslRunnerImpl) run(ctx context.Context, args ...string) error {\n\tsystemDir, err := windows.GetSystemDirectory()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get system directory: %w\", err)\n\t}\n\twslPath := filepath.Join(systemDir, \"wsl.exe\")\n\tcmd := exec.CommandContext(ctx, wslPath, args...)\n\tcmd.Env = append(cmd.Env, os.Environ()...)\n\tcmd.Env = append(cmd.Env, \"WSL_UTF8=1\")\n\tcmd.Stdout = r.stdout\n\tcmd.Stderr = r.stderr\n\tcmd.SysProcAttr = &windows.SysProcAttr{HideWindow: true}\n\terr = cmd.Run()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/wsl-utils/version_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage wslutils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// WSLInfo describes the current (online) WSL installation.\ntype WSLInfo struct {\n\tInstalled      bool           `json:\"installed\"`       // Whether WSL is considered to be installed.\n\tInbox          bool           `json:\"inbox\"`           // Whether WSL was shipped in-box or from the MS Store/MSIX\n\tVersion        PackageVersion `json:\"version\"`         // Installed WSL version (only for store version)\n\tKernelVersion  PackageVersion `json:\"kernel_version\"`  // Installed WSL kernel version\n\tHasKernel      bool           `json:\"has_kernel\"`      // Whether WSL has a kernel installed\n\tOutdatedKernel bool           `json:\"outdated_kernel\"` // Whether the WSL kernel is too old\n}\n\nfunc (i WSLInfo) String() string {\n\tvar parts []string\n\tif i.Installed {\n\t\tparts = append(parts, \"installed\")\n\t}\n\tif i.Inbox {\n\t\tparts = append(parts, \"inbox\")\n\t}\n\tif i.HasKernel {\n\t\tparts = append(parts, \"has-kernel\")\n\t}\n\tif i.OutdatedKernel {\n\t\tparts = append(parts, \"outdated-kernel\")\n\t}\n\tif len(parts) == 0 {\n\t\tparts = append(parts, \"not-installed\")\n\t}\n\treturn fmt.Sprintf(\"Version=%s kernel=%s (%s)\", i.Version, i.KernelVersion, strings.Join(parts, \", \"))\n}\n\nconst (\n\t// kMsiUpgradeCode is the upgrade code for the WSL kernel (for in-box WSL2)\n\tkMsiUpgradeCode = \"{1C3DB5B6-65A5-4EBC-A5B9-2F2D6F665F48}\"\n\t// Number of characters in a GUID string, including spaces\n\tguidLength = 39\n\t// wslExitNotInstalled is the exit code from `wsl --status` when WSL is not\n\t// installed.\n\twslExitNotInstalled = 50\n\t// wslExitNoKernel is the exit code from `wsl --status` when the in-box WSL is\n\t// installed, but the kernel is missing.\n\twslExitNoKernel = 0xFFFFFE44\n\t// wslExitVersion is the expected exit code from `wsl --version`.\n\twslExitVersion = 128\n)\n\n//nolint:stylecheck // Win32 constants\nconst (\n\tINSTALLPROPERTY_VERSIONSTRING = \"VersionString\"\n)\n\nvar (\n\tdllMsi                 = windows.NewLazySystemDLL(\"msi.dll\")\n\tmsiEnumRelatedProducts = dllMsi.NewProc(\"MsiEnumRelatedProductsW\")\n\tmsiGetProductInfo      = dllMsi.NewProc(\"MsiGetProductInfoW\")\n\n\t// kWSLExeOverride is a context key to override how we run wsl.exe for\n\t// testing.\n\tkWSLExeOverride = &struct{}{}\n\t// kUpgradeCodeOverride is a context key to override the MSI file to look for.\n\tkUpgradeCodeOverride = &struct{}{}\n\t// MinimumKernelVersion is the minimum WSL kernel version required to not be\n\t// considered outdated.\n\tMinimumKernelVersion = PackageVersion{Major: 5, Minor: 0}\n)\n\n// errorFromWin32 wraps a Win32 return value into an error, with a message in\n// the form of: {msg}: {rv}\nfunc errorFromWin32(msg string, rv uintptr) error {\n\treturn fmt.Errorf(\"%s: %w\", msg, windows.Errno(rv))\n}\n\n// PackageVersion corresponds to the PACKAGE_VERSION structure.\ntype PackageVersion struct {\n\tRevision uint16 `json:\"revision\"`\n\tBuild    uint16 `json:\"build\"`\n\tMinor    uint16 `json:\"minor\"`\n\tMajor    uint16 `json:\"major\"`\n}\n\nfunc (v PackageVersion) String() string {\n\treturn fmt.Sprintf(\"%d.%d.%d.%d\", v.Major, v.Minor, v.Build, v.Revision)\n}\n\nfunc (v *PackageVersion) UnmarshalText(text []byte) error {\n\texpr := regexp.MustCompile(`\\s*(\\d+)[.,](\\d+)[.,](\\d+)(?:[.,](\\d+))?`)\n\tgroups := expr.FindStringSubmatch(string(text))\n\tif groups == nil {\n\t\treturn fmt.Errorf(\"could not parse version %q\", string(text))\n\t}\n\tvar allErrors []error\n\tif part, err := strconv.ParseUint(groups[1], 10, 16); err == nil {\n\t\tv.Major = uint16(part)\n\t} else {\n\t\terr = fmt.Errorf(\"version %q has invalid major part: %w\", string(text), err)\n\t\tallErrors = append(allErrors, err)\n\t}\n\tif part, err := strconv.ParseUint(groups[2], 10, 16); err == nil {\n\t\tv.Minor = uint16(part)\n\t} else {\n\t\terr = fmt.Errorf(\"version %q has invalid minor part: %w\", string(text), err)\n\t\tallErrors = append(allErrors, err)\n\t}\n\tif part, err := strconv.ParseUint(groups[3], 10, 16); err == nil {\n\t\tv.Build = uint16(part)\n\t} else {\n\t\terr = fmt.Errorf(\"version %q has invalid build part: %w\", string(text), err)\n\t\tallErrors = append(allErrors, err)\n\t}\n\tif groups[4] != \"\" {\n\t\tif part, err := strconv.ParseUint(groups[4], 10, 16); err == nil {\n\t\t\tv.Revision = uint16(part)\n\t\t} else {\n\t\t\terr = fmt.Errorf(\"version %q has invalid revision part: %w\", string(text), err)\n\t\t\tallErrors = append(allErrors, err)\n\t\t}\n\t}\n\tif len(allErrors) > 0 {\n\t\treturn errors.Join(allErrors...)\n\t}\n\treturn nil\n}\n\n// Less returns true if this version is lower (i.e. older) than the other.\nfunc (v PackageVersion) Less(other PackageVersion) bool {\n\tswitch {\n\tcase v.Major != other.Major:\n\t\treturn v.Major < other.Major\n\tcase v.Minor != other.Minor:\n\t\treturn v.Minor < other.Minor\n\tcase v.Build != other.Build:\n\t\treturn v.Build < other.Build\n\tcase v.Revision != other.Revision:\n\t\treturn v.Revision < other.Revision\n\t}\n\treturn false\n}\n\n// Get the component versions by asking the CLI.  Returns the WSL\n// version, followed by the kernel version.\nfunc getVersionFromCLI(ctx context.Context, log *logrus.Entry) (*PackageVersion, *PackageVersion, error) {\n\tnewRunnerFunc := NewWSLRunner\n\tif f := ctx.Value(&kWSLExeOverride); f != nil {\n\t\tnewRunnerFunc = f.(func() WSLRunner)\n\t}\n\toutput := &bytes.Buffer{}\n\terr := newRunnerFunc().WithStdout(output).WithStderr(os.Stderr).Run(ctx, \"--version\")\n\tvar exitError *exec.ExitError\n\tif errors.As(err, &exitError) && exitError.ExitCode() == wslExitVersion {\n\t\t// wsl --version is expected to return non-nil\n\t} else if err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"error running wsl --version: %w\", err)\n\t}\n\tlog.WithField(\"raw\", output.String()).Trace(\"wsl --version output\")\n\texpr := regexp.MustCompile(`\\s+\\d+[.,]\\d+[.,]\\d+(?:[.,]\\d+)?`)\n\tvar errorList []error\n\tvar version, wslVersion, kernelVersion PackageVersion\n\ti := 0\n\tfor _, line := range strings.Split(output.String(), \"\\n\") {\n\t\tline = strings.TrimSpace(line)\n\t\tmatchedString := expr.FindString(line)\n\t\tif matchedString == \"\" {\n\t\t\tlog.WithField(\"line\", line).Trace(\"line does not contain version string\")\n\t\t\tcontinue\n\t\t}\n\t\tlog.WithField(\"line\", line).WithField(\"version\", matchedString).Trace(\"found version string\")\n\t\tif err = version.UnmarshalText([]byte(matchedString)); err != nil {\n\t\t\terrorList = append(errorList, err)\n\t\t} else {\n\t\t\tswitch i {\n\t\t\tcase 0:\n\t\t\t\twslVersion = version\n\t\t\tcase 1:\n\t\t\t\tkernelVersion = version\n\t\t\t}\n\t\t\ti++\n\t\t\tif i > 1 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tif len(errorList) > 0 {\n\t\treturn nil, nil, fmt.Errorf(\"error getting WSL version from CLI: %w\", errors.Join(errorList...))\n\t}\n\tlog.WithFields(logrus.Fields{\"wsl\": wslVersion, \"kernel\": kernelVersion}).Trace(\"got version from CLI\")\n\treturn &wslVersion, &kernelVersion, nil\n}\n\n// getInboxWSLInfo checks if the \"in-box\" version of WSL is installed, returning\n// whether it's installed, and the version of the kernel installed (if any)\nfunc getInboxWSLInfo(ctx context.Context, log *logrus.Entry) (bool, *PackageVersion, error) {\n\tvar allErrors []error\n\n\t// Check if the core is installed\n\tcoreInstalled := false\n\tnewRunnerFunc := NewWSLRunner\n\tif f := ctx.Value(&kWSLExeOverride); f != nil {\n\t\tnewRunnerFunc = f.(func() WSLRunner)\n\t}\n\toutput := &bytes.Buffer{}\n\terr := newRunnerFunc().WithStdout(output).WithStderr(os.Stderr).Run(ctx, \"--status\")\n\tvar exitErr *exec.ExitError\n\tif errors.As(err, &exitErr) && exitErr.ExitCode() == wslExitNotInstalled {\n\t\t// When WSL is not installed, we seem to get exit code 50\n\t} else if errors.As(err, &exitErr) && exitErr.ExitCode() == wslExitNoKernel {\n\t\t// When WSL is installed but the kernel is missing, we seem to get exit code\n\t\t// -444 (\"The WSL 2 kernel file is not found...\")\n\t\tcoreInstalled = true\n\t\treturn coreInstalled, nil, errors.Join(allErrors...)\n\t} else if err != nil {\n\t\tlog.WithError(err).Trace(\"wsl.exe --status exited\")\n\t\tallErrors = append(allErrors, err)\n\t} else {\n\t\tlines := strings.Split(strings.TrimSpace(output.String()), \"\\n\")\n\t\tif len(lines) > 0 {\n\t\t\tcoreInstalled = true\n\t\t} else {\n\t\t\tallErrors = append(allErrors, fmt.Errorf(\"no output from wsl --status\"))\n\t\t}\n\t}\n\n\t// Check if the kernel is installed.\n\tvar kernelVersion *PackageVersion\n\tupgradeCodeString := kMsiUpgradeCode\n\tif v := ctx.Value(&kUpgradeCodeOverride); v != nil {\n\t\tupgradeCodeString = v.(string)\n\t}\n\tupgradeCode, err := windows.UTF16PtrFromString(upgradeCodeString)\n\tif err != nil {\n\t\tallErrors = append(allErrors, err)\n\t} else {\n\t\tproductCode := make([]uint16, guidLength)\n\n\t\trv, _, _ := msiEnumRelatedProducts.Call(\n\t\t\tuintptr(unsafe.Pointer(upgradeCode)),\n\t\t\tuintptr(0),\n\t\t\tuintptr(0),\n\t\t\tuintptr(unsafe.Pointer(unsafe.SliceData(productCode))),\n\t\t)\n\t\tswitch rv {\n\t\tcase uintptr(windows.ERROR_SUCCESS):\n\t\t\tkernelVersion, err = getMSIVersion(productCode, log)\n\t\t\tif err != nil {\n\t\t\t\tallErrors = append(allErrors, fmt.Errorf(\"error getting kernel version: %w\", err))\n\t\t\t}\n\t\tcase uintptr(windows.ERROR_NO_MORE_ITEMS):\n\t\t\t// kernel is not installed\n\t\tdefault:\n\t\t\terr = errorFromWin32(\"error querying Windows Installer database\", rv)\n\t\t\tallErrors = append(allErrors, err)\n\t\t}\n\t}\n\n\treturn coreInstalled, kernelVersion, errors.Join(allErrors...)\n}\n\n// Get the version of an installed MSI package, given its product code.\nfunc getMSIVersion(productCode []uint16, log *logrus.Entry) (*PackageVersion, error) {\n\tversion := PackageVersion{}\n\tversionStringWide, err := windows.UTF16PtrFromString(INSTALLPROPERTY_VERSIONSTRING)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbufSize := 0\n\tvar wideBuf []uint16\n\trv, _, _ := msiGetProductInfo.Call(\n\t\tuintptr(unsafe.Pointer(unsafe.SliceData(productCode))),\n\t\tuintptr(unsafe.Pointer(versionStringWide)),\n\t\tuintptr(unsafe.Pointer(nil)),\n\t\tuintptr(unsafe.Pointer(&bufSize)),\n\t)\n\tswitch rv {\n\tcase uintptr(windows.ERROR_SUCCESS):\n\t\tlog.WithFields(logrus.Fields{\"bufSize\": bufSize}).Trace(\"unexpected success, assuming needs more data\")\n\t\tfallthrough\n\tcase uintptr(windows.ERROR_MORE_DATA):\n\t\twideBuf = make([]uint16, bufSize+1) // Add space for null terminator\n\t\tbufSize = len(wideBuf)\n\tcase uintptr(windows.ERROR_BAD_CONFIGURATION):\n\t\terr = errorFromWin32(\"Windows Installer configuration data is corrupt\", rv)\n\t\treturn nil, err\n\tdefault:\n\t\treturn nil, errorFromWin32(\"failed to get WSL kernel MSI version\", rv)\n\t}\n\n\trv, _, _ = msiGetProductInfo.Call(\n\t\tuintptr(unsafe.Pointer(unsafe.SliceData(productCode))),\n\t\tuintptr(unsafe.Pointer(versionStringWide)),\n\t\tuintptr(unsafe.Pointer(unsafe.SliceData(wideBuf))),\n\t\tuintptr(unsafe.Pointer(&bufSize)),\n\t)\n\tswitch rv {\n\tcase uintptr(windows.ERROR_SUCCESS):\n\t\tversionString := windows.UTF16ToString(wideBuf[:bufSize])\n\t\tif err := version.UnmarshalText([]byte(versionString)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn &version, nil\n\tcase uintptr(windows.ERROR_MORE_DATA):\n\t\treturn nil, errorFromWin32(\"allocated buffer was too small\", rv)\n\tcase uintptr(windows.ERROR_BAD_CONFIGURATION):\n\t\terr = errorFromWin32(\"Windows Installer configuration data is corrupt\", rv)\n\t\treturn nil, err\n\tdefault:\n\t\treturn nil, errorFromWin32(\"failed to get WSL kernel MSI version\", rv)\n\t}\n}\n\nfunc GetWSLInfo(ctx context.Context, log *logrus.Entry) (*WSLInfo, error) {\n\twslVersion, kernelVersion, err := getVersionFromCLI(ctx, log)\n\tif err == nil {\n\t\treturn &WSLInfo{\n\t\t\tInstalled:      true,\n\t\t\tInbox:          false,\n\t\t\tVersion:        *wslVersion,\n\t\t\tKernelVersion:  *kernelVersion,\n\t\t\tHasKernel:      PackageVersion{}.Less(*kernelVersion),\n\t\t\tOutdatedKernel: kernelVersion.Less(MinimumKernelVersion),\n\t\t}, nil\n\t}\n\tlog.WithError(err).Trace(\"Could not get version from `wsl --version`, trying inbox versions...\")\n\n\thasWSL, kernelVersion, err := getInboxWSLInfo(ctx, log)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif kernelVersion == nil {\n\t\tkernelVersion = &PackageVersion{}\n\t}\n\thasKernel := PackageVersion{}.Less(*kernelVersion)\n\treturn &WSLInfo{\n\t\tInstalled:      hasWSL && hasKernel,\n\t\tInbox:          hasWSL,\n\t\tHasKernel:      hasKernel,\n\t\tKernelVersion:  *kernelVersion,\n\t\tOutdatedKernel: kernelVersion.Less(MinimumKernelVersion),\n\t}, nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/pkg/wsl-utils/version_windows_test.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage wslutils\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/sirupsen/logrus/hooks/test\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc TestPackageVersion(t *testing.T) {\n\tt.Run(\"UnmarshalText\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tt.Run(\"three-part\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"1.2.3\"))\n\t\t\tassert.NoError(t, err, \"failed to unmarshal 1.2.3\")\n\t\t\tassert.Equal(t, PackageVersion{Major: 1, Minor: 2, Build: 3}, v)\n\t\t})\n\t\tt.Run(\"four-part\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"1234.5678.8765.4321\"))\n\t\t\tassert.NoError(t, err, \"failed to unmarshal 1234.5678.8765.4321\")\n\t\t\tassert.Equal(t, PackageVersion{Major: 1234, Minor: 5678, Build: 8765, Revision: 4321}, v)\n\t\t})\n\t\tt.Run(\"comma\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"1,2,3\"))\n\t\t\tassert.NoError(t, err, \"failed to unmarshal 1,2,3\")\n\t\t\tassert.Equal(t, PackageVersion{Major: 1, Minor: 2, Build: 3}, v)\n\t\t})\n\t\tt.Run(\"space\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\" 4.5.6\"))\n\t\t\tassert.NoError(t, err, \"failed to unmarshal <space>4.5.6\")\n\t\t\tassert.Equal(t, PackageVersion{Major: 4, Minor: 5, Build: 6}, v)\n\t\t})\n\t\tt.Run(\"invalid\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"12345\"))\n\t\t\tassert.ErrorContains(t, err, `could not parse version \"12345\"`)\n\t\t})\n\t\tt.Run(\"negative\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"-1.-2.-3.-4\"))\n\t\t\tassert.ErrorContains(t, err, `could not parse version \"-1.-2.-3.-4\"`)\n\t\t})\n\t\tt.Run(\"too-large\", func(t *testing.T) {\n\t\t\tv := PackageVersion{}\n\t\t\terr := v.UnmarshalText([]byte(\"65537.65537.65537.65537\"))\n\t\t\tif assert.Error(t, err) {\n\t\t\t\tassert.ErrorContains(t, err, `version \"65537.65537.65537.65537\" has invalid major part`)\n\t\t\t\tassert.ErrorContains(t, err, `version \"65537.65537.65537.65537\" has invalid minor part`)\n\t\t\t\tassert.ErrorContains(t, err, `version \"65537.65537.65537.65537\" has invalid build part`)\n\t\t\t\tassert.ErrorContains(t, err, `version \"65537.65537.65537.65537\" has invalid revision part`)\n\t\t\t}\n\t\t})\n\t})\n\tt.Run(\"Less\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tcases := []struct {\n\t\t\tL      string\n\t\t\tR      string\n\t\t\texpect bool\n\t\t}{\n\t\t\t{L: \"0.0.0\", R: \"0.0.0\", expect: false},\n\t\t\t{L: \"0.0.0\", R: \"0.0.1\", expect: true},\n\t\t\t{L: \"0.0.2\", R: \"0.0.1\", expect: false},\n\t\t\t{L: \"0.0.0\", R: \"0.1.0\", expect: true},\n\t\t\t{L: \"0.2.0\", R: \"0.1.0\", expect: false},\n\t\t\t{L: \"0.0.0\", R: \"1.0.0\", expect: true},\n\t\t\t{L: \"2.0.0\", R: \"1.0.0\", expect: false},\n\t\t\t{L: \"0.0.1\", R: \"0.1.0\", expect: true},\n\t\t\t{L: \"0.0.1\", R: \"1.0.0\", expect: true},\n\t\t\t{L: \"0.1.0\", R: \"0.0.1\", expect: false},\n\t\t\t{L: \"1.0.0\", R: \"0.0.1\", expect: false},\n\t\t}\n\t\tfor _, input := range cases {\n\t\t\tt.Run(fmt.Sprintf(\"%s<%s=%v\", input.L, input.R, input.expect), func(t *testing.T) {\n\t\t\t\tvar left, right PackageVersion\n\t\t\t\tassert.NoError(t, left.UnmarshalText([]byte(input.L)))\n\t\t\t\tassert.NoError(t, right.UnmarshalText([]byte(input.R)))\n\t\t\t\tassert.Equal(t, input.expect, left.Less(right))\n\t\t\t})\n\t\t}\n\t})\n}\n\n// TestWithExitCode is a dummy test function to let us exit with a given exit\n// code.  See TestIsInboxWSLInstalled/not_installed.\nfunc TestWithExitCode(t *testing.T) {\n\tcodeStr := os.Getenv(\"TEST_EXIT_CODE_VALUE\")\n\tcode, err := strconv.ParseInt(codeStr, 10, 8)\n\tif err != nil {\n\t\treturn\n\t}\n\tos.Exit(int(code))\n}\n\n// mockRun overrides the WSL runner to use the given function.\nfunc mockRun(ctx context.Context, fn func(context.Context, ...string) error) (context.Context, *wslRunnerImpl) {\n\trunner := &wslRunnerImpl{\n\t\tstdout: io.Discard,\n\t\tstderr: io.Discard,\n\t\trunFn:  fn,\n\t}\n\treturn context.WithValue(ctx, &kWSLExeOverride, func() WSLRunner { return runner }), runner\n}\n\n// runPowerShell runs the given command with PowerShell, returning standard output.\nfunc runPowerShell(ctx context.Context, command string) (*bytes.Buffer, error) {\n\tstdout := &bytes.Buffer{}\n\tstderr := &bytes.Buffer{}\n\tcmd := exec.CommandContext(ctx, \"powershell.exe\",\n\t\t\"-NoLogo\", \"-NoProfile\", \"-NonInteractive\", \"-Command\", command)\n\tcmd.Stdout = stdout\n\tcmd.Stderr = stderr\n\tif err := cmd.Run(); err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to run command %q: stdout=%s stderr=%s\", command, stdout, stderr)\n\t}\n\treturn stdout, nil\n}\n\nfunc TestGetVersionFromCLI(t *testing.T) {\n\toutputs := map[string]struct {\n\t\tlines  []string\n\t\twsl    string\n\t\tkernel string\n\t}{\n\t\t\"english\": {\n\t\t\tlines:  []string{\"WSL Version: 2.0.9.0\", \"Kernel version: 5.15.133.1-1\", \"WSLg version: 1.0.59\", \"... stuff\"},\n\t\t\twsl:    \"2.0.9.0\",\n\t\t\tkernel: \"5.15.133.1-1\",\n\t\t},\n\t\t\"commas\": {\n\t\t\tlines:  []string{\"Text 2,0,9,0\", \"Ignored 5,15,133,1-1\", \"more,ignored,text\"},\n\t\t\twsl:    \"2.0.9.0\",\n\t\t\tkernel: \"5.15.133.1-1\",\n\t\t},\n\t\t\"incomplete\": {\n\t\t\tlines:  []string{\"W 2.0.9.0\", \"no kernel version listed\"},\n\t\t\twsl:    \"2.0.9.0\",\n\t\t\tkernel: \"0.0.0\",\n\t\t},\n\t}\n\tlogger := logrus.New()\n\tlogger.SetOutput(io.Discard)\n\n\tfor name, input := range outputs {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tctx, cancel := context.WithCancel(context.Background())\n\t\t\tt.Cleanup(cancel)\n\t\t\tvar runner *wslRunnerImpl\n\t\t\tctx, runner = mockRun(ctx, func(ctx context.Context, s ...string) error {\n\t\t\t\tassert.ElementsMatch(t, []string{\"--version\"}, s)\n\t\t\t\tfor _, line := range input.lines {\n\t\t\t\t\t_, err := io.WriteString(runner.stdout, line+\"\\r\\n\")\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tvar expectedWSL, expectedKernel PackageVersion\n\t\t\twsl, kernel, err := getVersionFromCLI(ctx, logrus.NewEntry(logger))\n\t\t\tassert.NoError(t, err)\n\t\t\tassert.NoError(t, expectedWSL.UnmarshalText([]byte(input.wsl)))\n\t\t\tassert.NoError(t, expectedKernel.UnmarshalText([]byte(input.kernel)))\n\t\t\tassert.Equal(t, &expectedWSL, wsl)\n\t\t\tassert.Equal(t, &expectedKernel, kernel)\n\t\t})\n\t}\n}\n\nfunc TestGetInboxWSLInfo(t *testing.T) {\n\tlogger := logrus.New()\n\tlogger.SetOutput(io.Discard)\n\n\tt.Run(\"not installed\", func(t *testing.T) {\n\t\tctx, _ := mockRun(context.Background(), func(ctx context.Context, args ...string) error {\n\t\t\tassert.EqualValues(t, []string{\"--status\"}, args)\n\t\t\t// We want to mock an executable that exits with `wslExitNotInstalled`.\n\t\t\t// We do this by running ourselves, but using the TestWithExitCode\n\t\t\t// function above to return a fixed value passed through the\n\t\t\t// environment.\n\t\t\tcmd := exec.CommandContext(ctx, os.Args[0], \"-test.run\", \"^TestWithExitCode$\")\n\t\t\tcmd.Env = append(cmd.Env, fmt.Sprintf(\"TEST_EXIT_CODE_VALUE=%d\", wslExitNotInstalled))\n\t\t\treturn cmd.Run()\n\t\t})\n\t\t// Use a random GUID here\n\t\tctx = context.WithValue(ctx, &kUpgradeCodeOverride, \"{60486CC7-CD7A-4514-9E88-7F21E8A81679}\")\n\t\thasWSL, kernelVersion, err := getInboxWSLInfo(ctx, logrus.NewEntry(logger))\n\t\tassert.NoError(t, err)\n\t\tassert.False(t, hasWSL, \"WSL should not be installed\")\n\t\tif !assert.Nil(t, kernelVersion, \"kernel should not be installed\") {\n\t\t\tassert.False(t, PackageVersion{}.Less(*kernelVersion), \"kernel should not be installed\")\n\t\t}\n\t})\n\tt.Run(\"installed without kernel\", func(t *testing.T) {\n\t\tvar ctx context.Context\n\t\tvar runner *wslRunnerImpl\n\t\tctx, runner = mockRun(context.Background(), func(ctx context.Context, args ...string) error {\n\t\t\tassert.EqualValues(t, []string{\"--status\"}, args)\n\t\t\t// When WSL (inbox) is installed but no kernel, `wsl --status`\n\t\t\t// returns with exit code 0.\n\t\t\tfor _, line := range []string{\n\t\t\t\t\"Default Version: 2\",\n\t\t\t\t\"\",\n\t\t\t\t\"... Something about updates...\",\n\t\t\t\t\"The WSL 2 kernel file is not found. To update or restore the kernel please run 'wsl --update'.\",\n\t\t\t\t\"\",\n\t\t\t} {\n\t\t\t\t_, err := io.WriteString(runner.stdout, line+\"\\r\\n\")\n\t\t\t\tassert.NoError(t, err)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t\t// Use a random GUID here\n\t\tctx = context.WithValue(ctx, &kUpgradeCodeOverride, \"{0C32EDDD-2674-4F32-B415-B715AF90BE74}\")\n\t\thasWSL, kernelVersion, err := getInboxWSLInfo(ctx, logrus.NewEntry(logger))\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, hasWSL, \"WSL should be installed\")\n\t\tif !assert.Nil(t, kernelVersion, \"kernel should not be installed\") {\n\t\t\tassert.False(t, PackageVersion{}.Less(*kernelVersion), \"kernel should not be installed\")\n\t\t}\n\t})\n\tt.Run(\"installed with kernel\", func(t *testing.T) {\n\t\tvar ctx context.Context\n\t\tvar runner *wslRunnerImpl\n\t\tctx, runner = mockRun(context.Background(), func(ctx context.Context, args ...string) error {\n\t\t\tassert.EqualValues(t, []string{\"--status\"}, args)\n\t\t\tio.WriteString(runner.stdout, \"Hello world\\r\\n\")\n\t\t\treturn nil\n\t\t})\n\t\t// Use the upgrade code for \"Windows Subsystem for Linux\", which is the\n\t\t// version installed from the MS Store.\n\t\tctx = context.WithValue(ctx, &kUpgradeCodeOverride, \"{6D5B792B-1EDC-4DE9-8EAD-201B820F8E82}\")\n\t\thasWSL, kernelVersion, err := getInboxWSLInfo(ctx, logrus.NewEntry(logger))\n\t\tassert.NoError(t, err)\n\t\tassert.True(t, hasWSL, \"WSL should be installed\")\n\t\tif assert.NotNil(t, kernelVersion, \"kernel should be installed\") {\n\t\t\tassert.True(t, PackageVersion{}.Less(*kernelVersion), \"kernel should be installed\")\n\t\t}\n\t})\n}\n\nfunc TestGetMSIVersion(t *testing.T) {\n\tctx, cancel := context.WithCancel(context.Background())\n\tt.Cleanup(cancel)\n\t// Find a random installed MSI (via PowerShell), then check that we can get\n\t// the same version number.\n\tcommandLine := strings.NewReplacer(\"\\r\", \" \", \"\\n\", \" \", \"\\t\", \" \").Replace(`\n\t\tGet-CimInstance -ClassName Win32_Product -Property IdentifyingNumber, Version\n\t\t| Select-Object -First 1\n\t\t| ConvertTo-JSON\n\t`)\n\tstdout, err := runPowerShell(ctx, commandLine)\n\tif err != nil {\n\t\tt.Skipf(\"Skipping test, failed to get any installed product: %s\", err)\n\t}\n\tresult := struct {\n\t\tIdentifyingNumber string\n\t\tVersion           *PackageVersion\n\t}{}\n\trequire.NoError(t, json.Unmarshal(stdout.Bytes(), &result), \"Failed to get product info\")\n\trequire.NotNil(t, result.Version, \"Failed to get product version\")\n\n\t// Now we have a product code to test again; actually run the function under test.\n\tlogger, _ := test.NewNullLogger()\n\tproductCode, err := windows.UTF16FromString(result.IdentifyingNumber)\n\trequire.NoError(t, err, \"Failed to convert product code\")\n\tactualVersion, err := getMSIVersion(productCode, logrus.NewEntry(logger))\n\trequire.NoError(t, err, \"Failed to get product version\")\n\tassert.Equal(t, result.Version, actualVersion, \"Unexpected version\")\n}\n"
  },
  {
    "path": "src/go/wsl-helper/wix/check_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\twslutils \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/wsl-utils\"\n)\n\nconst (\n\t// Windows Installer property that is set if WSL was detected to be installed.\n\tPropWSLInstalled = \"WSLINSTALLED\"\n\t// Windows Installer property that is set if the WSL kernel needs to be upgraded.\n\tPropWSLKernelOutdated = \"WSLKERNELOUTDATED\"\n)\n\n// DetectWSLImpl checks if WSL is installed; it outputs results by setting\n// Windows Installer properties.\nfunc DetectWSLImpl(hInstall MSIHANDLE) uint32 {\n\tctx := context.Background()\n\n\twriter := &msiWriter{hInstall: hInstall}\n\tlog := logrus.NewEntry(&logrus.Logger{\n\t\tOut:       writer,\n\t\tFormatter: &logrus.TextFormatter{},\n\t\tHooks:     make(logrus.LevelHooks),\n\t\tLevel:     logrus.TraceLevel,\n\t})\n\n\tlog.Info(\"Checking if WSL is installed...\")\n\tinfo, err := wslutils.GetWSLInfo(ctx, log)\n\tif err != nil {\n\t\tlog.WithError(err).Error(\"Failed to get WSL info\")\n\t\treturn 1\n\t}\n\tlog.Infof(\"WSL install state: %+v\", info)\n\n\tif info.Installed {\n\t\tif err = setProperty(hInstall, PropWSLInstalled, \"1\"); err != nil {\n\t\t\tlog.WithError(err).Errorf(\"failed to set property %s\", PropWSLInstalled)\n\t\t}\n\t\tif info.OutdatedKernel {\n\t\t\tif err = setProperty(hInstall, PropWSLKernelOutdated, \"1\"); err != nil {\n\t\t\t\tlog.WithError(err).Errorf(\"failed to set property %s\", PropWSLKernelOutdated)\n\t\t\t}\n\t\t}\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "src/go/wsl-helper/wix/doc.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// Package main implements a Windows Installer custom action DLL.  That is, it\n// exports functions using CGO that will be called by Windows Installer.\npackage main\n"
  },
  {
    "path": "src/go/wsl-helper/wix/helpers_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"unsafe\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\ntype messageType uintptr\n\n//nolint:stylecheck // Win32 constants\nconst (\n\tINSTALLMESSAGE_INFO        messageType = 0x04000000\n\tINSTALLMESSAGE_ACTIONSTART messageType = 0x08000000\n)\n\nfunc submitMessage(hInstall MSIHANDLE, message messageType, data []string) error {\n\trecord, _, _ := msiCreateRecord.Call(uintptr(len(data) - 1))\n\tif record == 0 {\n\t\treturn fmt.Errorf(\"failed to create record\")\n\t}\n\tdefer func() { _, _, _ = msiCloseHandle.Call(record) }()\n\tfor i, item := range data {\n\t\tbuf, err := windows.UTF16PtrFromString(item)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, _, _ = msiRecordSetStringW.Call(record, uintptr(i), uintptr(unsafe.Pointer(buf)))\n\t}\n\t_, _, _ = msiProcessMessage.Call(uintptr(hInstall), uintptr(message), record)\n\treturn nil\n}\n\n// msiWriter is an io.Writer that emits to Windows Installer's logging.\ntype msiWriter struct {\n\thInstall MSIHANDLE\n}\n\nfunc (w *msiWriter) Write(message []byte) (int, error) {\n\t// We always set up a record where *0 is just \"[1]\" to avoid issues if\n\t// the message contains formatting; this is analogous to calling\n\t// `Sprintf(\"%s\", ...)``\n\tdata := []string{\"[1]\", strings.TrimRight(string(message), \"\\r\\n\")}\n\terr := submitMessage(w.hInstall, INSTALLMESSAGE_INFO, data)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(message), nil\n}\n\n// setProperty sets a Windows Installer property to the given value.\nfunc setProperty(hInstall MSIHANDLE, name, value string) error {\n\tnameBuf, err := windows.UTF16PtrFromString(name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode property name %q: %w\", name, err)\n\t}\n\tvalueBuf, err := windows.UTF16PtrFromString(value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encode property value %q: %w\", value, err)\n\t}\n\t_, _, _ = msiSetPropertyW.Call(\n\t\tuintptr(hInstall),\n\t\tuintptr(unsafe.Pointer(nameBuf)),\n\t\tuintptr(unsafe.Pointer(valueBuf)),\n\t)\n\treturn nil\n}\n"
  },
  {
    "path": "src/go/wsl-helper/wix/imports_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport \"golang.org/x/sys/windows\"\n\ntype MSIHANDLE uint32\n\nvar (\n\tdllMsi              = windows.NewLazySystemDLL(\"msi.dll\")\n\tmsiCloseHandle      = dllMsi.NewProc(\"MsiCloseHandle\")\n\tmsiCreateRecord     = dllMsi.NewProc(\"MsiCreateRecord\")\n\tmsiRecordSetStringW = dllMsi.NewProc(\"MsiRecordSetStringW\")\n\tmsiProcessMessage   = dllMsi.NewProc(\"MsiProcessMessage\")\n\tmsiSetPropertyW     = dllMsi.NewProc(\"MsiSetPropertyW\")\n)\n"
  },
  {
    "path": "src/go/wsl-helper/wix/install_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport (\n\t\"context\"\n\n\t\"github.com/sirupsen/logrus\"\n\n\twslutils \"github.com/rancher-sandbox/rancher-desktop/src/go/wsl-helper/pkg/wsl-utils\"\n)\n\nfunc setupLogger(hInstall MSIHANDLE) *logrus.Entry {\n\treturn logrus.NewEntry(&logrus.Logger{\n\t\tOut:       &msiWriter{hInstall: hInstall},\n\t\tFormatter: &logrus.TextFormatter{},\n\t\tHooks:     make(logrus.LevelHooks),\n\t\tLevel:     logrus.TraceLevel,\n\t})\n}\n\n// UpdateWSLImpl updates the previously installed WSL.\n// This needs to be run as the user, and may request elevation.\nfunc UpdateWSLImpl(hInstall MSIHANDLE) uint32 {\n\tctx := context.Background()\n\tlog := setupLogger(hInstall)\n\n\tlog.Info(\"Updating WSL...\")\n\terr := submitMessage(hInstall, INSTALLMESSAGE_ACTIONSTART, []string{\n\t\t\"\", \"UpdateWSL\", \"Updating Windows Subsystem for Linux...\", \"<unused>\",\n\t})\n\tif err != nil {\n\t\tlog.WithError(err).Info(\"Failed to update progress\")\n\t}\n\tif err := wslutils.UpdateWSL(ctx, log); err != nil {\n\t\tlog.WithError(err).Error(\"Updating WSL failed\")\n\t\treturn 1\n\t}\n\n\tlog.Info(\"WSL successfully updated.\")\n\treturn 0\n}\n"
  },
  {
    "path": "src/go/wsl-helper/wix/main_windows.go",
    "content": "/*\nCopyright © 2023 SUSE LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n\thttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\npackage main\n\nimport \"C\"\n\nfunc main() {\n\t// DllMain is not used\n}\n\n// DetectWSL is a wrapper around DetectWSLImpl; this is the stub to be exported\n// in the DLL.  This only exists to limit cgo to this file so that editing on a\n// machine that requires cross compilation can avoid needing a cross cgo\n// toolchain.\n//\n//export DetectWSL\nfunc DetectWSL(hInstall C.ulong) C.ulong {\n\treturn C.ulong(DetectWSLImpl(MSIHANDLE(hInstall)))\n}\n\n// UpdateWSL is a wrapper around UpdateWSLImpl; this is the stub to be exported\n// in the DLL.  This only exists to limit cgo to this file so that editing on a\n// machine that requires cross compilation can avoid needing a cross cgo\n// toolchain.\n//\n//export UpdateWSL\nfunc UpdateWSL(hInstall C.ulong) C.ulong {\n\treturn C.ulong(UpdateWSLImpl(MSIHANDLE(hInstall)))\n}\n"
  },
  {
    "path": "src/sudo-prompt/build-sudo-prompt",
    "content": "#!/usr/bin/env bash\n\n# shellcheck disable=SC2164 # Use 'cd ... || exit' or 'cd ... || return' in case cd fails.\nREPO=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../..\"; pwd)\n\n# The APP name must be \"Rancher Desktop.app\" because this name is used in the dialog as\n# \"Rancher Desktop wants to make changes.\"\nRESOURCES=\"${REPO}/resources\"\nAPP=\"${RESOURCES}/darwin/internal/Rancher Desktop.app\"\nCONTENTS=\"${APP}/Contents\"\n\nrm -rf \"$APP\"\nmkdir -p \"$(dirname \"$APP\")\"\nosacompile -o \"$APP\" sudo-prompt.applescript\n\n# Don't put the script into ${CONTENTS}/MacOS/ because that breaks signing the applet\ncp sudo-prompt-script \"${CONTENTS}/Resources/Scripts/\"\nsips -s format icns \"${RESOURCES}/icons/mac-icon.png\" --out \"${CONTENTS}/Resources/applet.icns\"\n\nplutil -replace CFBundleName -string \"Rancher Desktop Password Prompt\" \"${CONTENTS}/Info.plist\"\n"
  },
  {
    "path": "src/sudo-prompt/sudo-prompt-script",
    "content": "#!/bin/bash\n# This script is executed by the applet with root permissions.\n# The caller will have created a temporary directory containing just the\n# `sudo-prompt-command` shell script. This script will add the `code`,\n# `stdout` and `stderr` files. The caller will delete this directory\n# again after reading the files.\n\n# Set sudo timestamp for subsequent sudo calls if tty_tickets are disabled:\n/bin/mkdir -p /var/db/sudo/$USER > /dev/null 2>&1\n/usr/bin/touch /var/db/sudo/$USER > /dev/null 2>&1\n# AppleScript's \"do shell script\" may alter stdout line-endings.\n# It may also set stdout to stderr if there was a non-zero return code and no stderr.\n# We therefore prefer to redirect output streams and capture return code manually:\n/bin/bash sudo-prompt-command 1>stdout 2>stderr\n/bin/echo $? > code\n# Correct ownership of stdout, stderr and code so that user can delete them:\n/usr/sbin/chown $USER stdout stderr code\n# Always return 0 so that AppleScript does not show error dialog:\nexit 0\n"
  },
  {
    "path": "src/sudo-prompt/sudo-prompt.applescript",
    "content": "set appletPath to POSIX path of (path to me)\nif appletPath ends with \".app/\" then\n\tset appletPath to appletPath & \"Contents/Resources/Scripts\"\nelse\n\tset appletPath to do shell script \"dirname \" & quoted form of appletPath\nend if\nset promptScript to appletPath & \"/sudo-prompt-script\"\ndo shell script (quoted form of promptScript) with administrator privileges\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2024\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Node\",\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"react\",\n    \"lib\": [\n      \"ESNext\",\n      \"ESNext.AsyncIterable\",\n      \"DOM\",\n      \"DOM.Iterable\",\n    ],\n    \"esModuleInterop\": true,\n    \"allowJs\": true,\n    \"sourceMap\": true,\n    \"strict\": true,\n    \"experimentalDecorators\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@pkg/*\": [\n        \"pkg/rancher-desktop/*\"\n      ],\n      \"@shell/*\": [\n        \"./node_modules/@rancher/shell/*\"\n      ]\n    },\n    \"typeRoots\": [\n      \"./node_modules\",\n      \"./node_modules/@types\",\n      \"./node_modules/@rancher/shell/types\"\n    ],\n    \"types\": [\n      \"@types/node\",\n      \"@types/jest\"\n    ]\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"dist\",\n    \"scripts/lib/installer-*.tsx\",\n    \"scripts/lib/installer-win32-gen.tsx\"\n  ],\n  \"include\": [\n    \"**/*\",\n    \"**/*.ts\",\n    \"**/*.d.ts\",\n    \"**/*.tsx\",\n    \"**/*.vue\",\n    \".eslintrc.js\"\n  ]\n}\n"
  }
]