From 613116ab139c9f415a7e54e5669fc3afee09e48a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 20:51:45 +0000 Subject: [PATCH 1/5] Initial plan for issue From f745c25d717152b83e073e6de7ef0ba4a59fe9df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 21:09:41 +0000 Subject: [PATCH 2/5] Add Windows support for retina-shell Co-authored-by: nddq <28567936+nddq@users.noreply.github.com> --- Makefile | 16 ++++++ cli/cmd/shell.go | 52 ++++++++++++++++++- shell/Dockerfile.windows | 52 +++++++++++++++++++ shell/manifests.go | 30 +++++++++-- shell/manifests_test.go | 49 ++++++++++++++++++ shell/shell.go | 15 ++++++ shell/validation.go | 20 ++++++-- shell/validation_test.go | 106 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 331 insertions(+), 9 deletions(-) create mode 100644 shell/Dockerfile.windows create mode 100644 shell/validation_test.go diff --git a/Makefile b/Makefile index 80ccaf3b37..d522af7e1d 100644 --- a/Makefile +++ b/Makefile @@ -296,6 +296,22 @@ retina-shell-image: TAG=$(RETINA_PLATFORM_TAG) \ CONTEXT_DIR=$(REPO_ROOT) +retina-shell-image-win: + for year in $(WINDOWS_YEARS); do \ + tag=$(TAG)-windows-ltsc$$year-amd64; \ + echo "Building retina-shell Windows image with tag $$tag"; \ + set -e ; \ + $(MAKE) container-$(CONTAINER_BUILDER) \ + PLATFORM=windows/amd64 \ + DOCKERFILE=shell/Dockerfile.windows \ + REGISTRY=$(IMAGE_REGISTRY) \ + IMAGE=$(RETINA_SHELL_IMAGE) \ + OS_VERSION=ltsc$$year \ + VERSION=$(TAG) \ + TAG=$$tag \ + CONTEXT_DIR=$(REPO_ROOT); \ + done + kubectl-retina-image: echo "Building for $(PLATFORM)" set -e ; \ diff --git a/cli/cmd/shell.go b/cli/cmd/shell.go index b388c720eb..1b7c57114c 100644 --- a/cli/cmd/shell.go +++ b/cli/cmd/shell.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "fmt" "os" @@ -12,6 +13,7 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/templates" @@ -22,6 +24,7 @@ var ( matchVersionFlags *cmdutil.MatchVersionFlags retinaShellImageRepo string retinaShellImageVersion string + windowsImageTag string mountHostFilesystem bool allowHostFilesystemWrite bool hostPID bool @@ -38,6 +41,9 @@ var ( defaultTimeout = 30 * time.Second + // Default Windows image tag suffix + defaultWindowsImageTag = "windows-ltsc2022-amd64" + errMissingRequiredRetinaShellImageVersionArg = errors.New("missing required --retina-shell-image-version") errUnsupportedResourceType = errors.New("unsupported resource type") ) @@ -57,6 +63,10 @@ var shellCmd = &cobra.Command{ CLI flags (--retina-shell-image-repo and --retina-shell-image-version) or environment variables (RETINA_SHELL_IMAGE_REPO and RETINA_SHELL_IMAGE_VERSION). CLI flags take precedence over env vars. + + For Windows nodes, the shell image will automatically use the Windows variant with the + specified Windows image tag suffix (--windows-image-tag). Windows support requires a + Windows node with HostProcess containers enabled. `), Example: templates.Examples(` @@ -75,6 +85,9 @@ var shellCmd = &cobra.Command{ # start a shell in a node, with NET_RAW and NET_ADMIN capabilities # (required for iptables and tcpdump) kubectl retina shell node001 --capabilities NET_RAW,NET_ADMIN + + # start a shell in a Windows node + kubectl retina shell win-node001 `), Args: cobra.ExactArgs(1), RunE: func(_ *cobra.Command, args []string) error { @@ -106,6 +119,13 @@ var shellCmd = &cobra.Command{ return fmt.Errorf("error constructing REST config: %w", err) } + // Create Kubernetes clientset to determine node OS + clientset, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return fmt.Errorf("error creating clientset: %w", err) + } + + // Create a generic config for now, we'll update image based on node OS config := shell.Config{ RestConfig: restConfig, RetinaShellImage: fmt.Sprintf("%s:%s", retinaShellImageRepo, retinaShellImageVersion), @@ -123,10 +143,34 @@ var shellCmd = &cobra.Command{ switch obj := info.Object.(type) { case *v1.Node: - podDebugNamespace := namespace nodeName := obj.Name + podDebugNamespace := namespace + + // Get the OS and update the image if it's Windows + nodeOS := obj.Labels["kubernetes.io/os"] + if nodeOS == "windows" { + // For Windows, use the Windows-specific image tag + windowsImageVersion := fmt.Sprintf("%s-%s", retinaShellImageVersion, windowsImageTag) + config.RetinaShellImage = fmt.Sprintf("%s:%s", retinaShellImageRepo, windowsImageVersion) + fmt.Printf("Using Windows shell image: %s\n", config.RetinaShellImage) + } + return shell.RunInNode(config, nodeName, podDebugNamespace) case *v1.Pod: + // For pods, we need to get the node OS based on the pod's node + ctx := context.Background() + nodeOS, err := shell.GetNodeOS(ctx, clientset, obj.Spec.NodeName) + if err != nil { + return fmt.Errorf("error getting node OS: %w", err) + } + + if nodeOS == "windows" { + // For Windows, use the Windows-specific image tag + windowsImageVersion := fmt.Sprintf("%s-%s", retinaShellImageVersion, windowsImageTag) + config.RetinaShellImage = fmt.Sprintf("%s:%s", retinaShellImageRepo, windowsImageVersion) + fmt.Printf("Using Windows shell image: %s\n", config.RetinaShellImage) + } + return shell.RunInPod(config, obj.Namespace, obj.Name) default: gvk := obj.GetObjectKind().GroupVersionKind() @@ -154,9 +198,15 @@ func init() { retinaShellImageVersion = envVersion } } + if !cmd.Flags().Changed("windows-image-tag") { + if envWindowsTag := os.Getenv("RETINA_SHELL_WINDOWS_IMAGE_TAG"); envWindowsTag != "" { + windowsImageTag = envWindowsTag + } + } } shellCmd.Flags().StringVar(&retinaShellImageRepo, "retina-shell-image-repo", defaultRetinaShellImageRepo, "The container registry repository for the image to use for the shell container") shellCmd.Flags().StringVar(&retinaShellImageVersion, "retina-shell-image-version", defaultRetinaShellImageVersion, "The version (tag) of the image to use for the shell container") + shellCmd.Flags().StringVar(&windowsImageTag, "windows-image-tag", defaultWindowsImageTag, "The tag suffix to use for Windows shell images (e.g., 'windows-ltsc2022-amd64')") shellCmd.Flags().BoolVarP(&mountHostFilesystem, "mount-host-filesystem", "m", false, "Mount the host filesystem to /host. Applies only to nodes, not pods.") shellCmd.Flags().BoolVarP(&allowHostFilesystemWrite, "allow-host-filesystem-write", "w", false, "Allow write access to the host filesystem. Implies --mount-host-filesystem. Applies only to nodes, not pods.") diff --git a/shell/Dockerfile.windows b/shell/Dockerfile.windows new file mode 100644 index 0000000000..ab8c7d0196 --- /dev/null +++ b/shell/Dockerfile.windows @@ -0,0 +1,52 @@ +ARG WINDOWS_VERSION=ltsc2022 + +# Using a smaller Windows Server Core base image +FROM mcr.microsoft.com/windows/servercore:${WINDOWS_VERSION} + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# Install common network troubleshooting tools +# - Netstat, ping, ipconfig, etc. are built into Windows +# - NirSoft WinDump (tcpdump equivalent for Windows) +# - Portqry - port scanner + +# Download and install NirSoft WinDump +RUN mkdir -p C:\tools; \ + Invoke-WebRequest -Uri "https://www.nirsoft.net/utils/windump_setup.exe" -OutFile "C:\tools\windump_setup.exe" -UseBasicParsing; \ + Start-Process -FilePath "C:\tools\windump_setup.exe" -ArgumentList "/S" -Wait; \ + Remove-Item -Path "C:\tools\windump_setup.exe" -Force + +# Download and install PortQry +RUN Invoke-WebRequest -Uri "https://download.microsoft.com/download/3/b/5/3b51a025-7522-4686-87a6-47aa8ea68141/PortQryV2.exe" -OutFile "C:\Windows\System32\PortQry.exe" -UseBasicParsing + +# Download and extract Nmap network scanner +RUN Invoke-WebRequest -Uri "https://nmap.org/dist/nmap-7.92-win32.zip" -OutFile "C:\tools\nmap.zip" -UseBasicParsing; \ + Expand-Archive -Path "C:\tools\nmap.zip" -DestinationPath "C:\tools" -Force; \ + Move-Item -Path "C:\tools\nmap-*\*" -Destination "C:\tools\" -Force; \ + $env:PATH = "C:\tools;$env:PATH" + +# Create a convenience script to display available tools +RUN Set-Content -Path "C:\tools\show-tools.cmd" -Value @" +@echo off +echo. +echo Available Network Troubleshooting Tools: +echo - ipconfig : Show network configuration +echo - netstat : Show network connections +echo - ping : Test connectivity +echo - tracert : Trace route +echo - nslookup : DNS lookup +echo - route : Show/manipulate routing table +echo - netsh : Network shell for configuration +echo - nmap : Network discovery and security auditing +echo - portqry : Port scanner +echo - windump : Packet analyzer (tcpdump for Windows) +echo. +"@ + +# Set PATH to include tools +ENV PATH="C:\tools;C:\Windows\System32;C:\Windows;C:\Windows\System32\WindowsPowerShell\v1.0" + +WORKDIR C:\ + +# Default entry point +CMD ["cmd.exe"] \ No newline at end of file diff --git a/shell/manifests.go b/shell/manifests.go index 72a5df0989..f148f4e57d 100644 --- a/shell/manifests.go +++ b/shell/manifests.go @@ -35,7 +35,7 @@ func hostNetworkPodForNodeDebug(config Config, debugPodNamespace, nodeName strin RestartPolicy: v1.RestartPolicyNever, Tolerations: []v1.Toleration{{Operator: v1.TolerationOpExists}}, HostNetwork: true, - HostPID: config.HostPID, + HostPID: config.HostPID && config.NodeOS != "windows", // HostPID is not applicable for Windows Containers: []v1.Container{ { Name: "retina-shell", @@ -53,18 +53,42 @@ func hostNetworkPodForNodeDebug(config Config, debugPodNamespace, nodeName strin }, } + // Add Windows specific settings if needed + if config.NodeOS == "windows" { + // Use hostProcess container for Windows (equivalent to privileged on Linux) + trueValue := true + falseValue := false + runAsUserName := "NT AUTHORITY\\SYSTEM" + pod.Spec.SecurityContext = &v1.PodSecurityContext{ + WindowsOptions: &v1.WindowsSecurityContextOptions{ + HostProcess: &trueValue, + RunAsUserName: &runAsUserName, + }, + RunAsNonRoot: &falseValue, + } + } + + // Mount host filesystem if requested (different paths for Windows vs Linux) if config.MountHostFilesystem || config.AllowHostFilesystemWrite { + hostPath := "/" + mountPath := "/host" + + if config.NodeOS == "windows" { + hostPath = "C:\\" + mountPath = "C:\\host" + } + pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{ Name: "host-filesystem", VolumeSource: v1.VolumeSource{ HostPath: &v1.HostPathVolumeSource{ - Path: "/", + Path: hostPath, }, }, }) pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, v1.VolumeMount{ Name: "host-filesystem", - MountPath: "/host", + MountPath: mountPath, ReadOnly: !config.AllowHostFilesystemWrite, }) } diff --git a/shell/manifests_test.go b/shell/manifests_test.go index 840d7d2e49..c7793f7ce9 100644 --- a/shell/manifests_test.go +++ b/shell/manifests_test.go @@ -9,6 +9,7 @@ import ( ) const testRetinaImage = "retina-shell:v0.0.1" +const testRetinaWindowsImage = "retina-shell:v0.0.1-windows-ltsc2022-amd64" func TestEphemeralContainerForPodDebug(t *testing.T) { ec := ephemeralContainerForPodDebug(Config{RetinaShellImage: testRetinaImage}) @@ -89,3 +90,51 @@ func TestHostNetworkPodForNodeDebugWithMountHostFilesystemWithWriteAccess(t *tes assert.Equal(t, "/host", pod.Spec.Containers[0].VolumeMounts[0].MountPath) assert.False(t, pod.Spec.Containers[0].VolumeMounts[0].ReadOnly) } + +// Test Windows node support +func TestHostNetworkPodForWindowsNodeDebug(t *testing.T) { + config := Config{ + RetinaShellImage: testRetinaWindowsImage, + NodeOS: "windows", + } + pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") + + // Verify Windows specific configurations + assert.NotNil(t, pod.Spec.SecurityContext) + assert.NotNil(t, pod.Spec.SecurityContext.WindowsOptions) + assert.NotNil(t, pod.Spec.SecurityContext.WindowsOptions.HostProcess) + assert.True(t, *pod.Spec.SecurityContext.WindowsOptions.HostProcess) + assert.Equal(t, "NT AUTHORITY\\SYSTEM", *pod.Spec.SecurityContext.WindowsOptions.RunAsUserName) + + // HostPID should be false for Windows regardless of setting + assert.False(t, pod.Spec.HostPID) +} + +func TestHostNetworkPodForWindowsNodeDebugWithMountHostFilesystem(t *testing.T) { + config := Config{ + RetinaShellImage: testRetinaWindowsImage, + NodeOS: "windows", + MountHostFilesystem: true, + } + pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") + + assert.Len(t, pod.Spec.Volumes, 1) + assert.Equal(t, "host-filesystem", pod.Spec.Volumes[0].Name) + assert.Equal(t, "C:\\", pod.Spec.Volumes[0].VolumeSource.HostPath.Path) + + assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) + assert.Equal(t, "host-filesystem", pod.Spec.Containers[0].VolumeMounts[0].Name) + assert.Equal(t, "C:\\host", pod.Spec.Containers[0].VolumeMounts[0].MountPath) +} + +func TestHostNetworkPodForWindowsNodeWithHostPID(t *testing.T) { + config := Config{ + RetinaShellImage: testRetinaWindowsImage, + NodeOS: "windows", + HostPID: true, // Should be ignored for Windows + } + pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") + + // HostPID should be false for Windows regardless of the setting + assert.False(t, pod.Spec.HostPID) +} diff --git a/shell/shell.go b/shell/shell.go index c85a07338c..7b98f86683 100644 --- a/shell/shell.go +++ b/shell/shell.go @@ -18,6 +18,7 @@ type Config struct { HostPID bool Capabilities []string Timeout time.Duration + NodeOS string // "linux" or "windows" // Host filesystem access applies only to nodes, not pods. MountHostFilesystem bool @@ -45,6 +46,13 @@ func RunInPod(config Config, podNamespace, podName string) error { return fmt.Errorf("error validating operating system for node %s: %w", pod.Spec.NodeName, err) } + // Get the node OS for the pod's node + nodeOS, err := GetNodeOS(ctx, clientset, pod.Spec.NodeName) + if err != nil { + return fmt.Errorf("error getting OS for node %s: %w", pod.Spec.NodeName, err) + } + config.NodeOS = nodeOS + fmt.Printf("Starting ephemeral container in pod %s/%s\n", podNamespace, podName) ephemeralContainer := ephemeralContainerForPodDebug(config) pod.Spec.EphemeralContainers = append(pod.Spec.EphemeralContainers, ephemeralContainer) @@ -77,6 +85,13 @@ func RunInNode(config Config, nodeName, debugPodNamespace string) error { return fmt.Errorf("error validating operating system for node %s: %w", nodeName, err) } + // Get the node OS + nodeOS, err := GetNodeOS(ctx, clientset, nodeName) + if err != nil { + return fmt.Errorf("error getting OS for node %s: %w", nodeName, err) + } + config.NodeOS = nodeOS + pod := hostNetworkPodForNodeDebug(config, debugPodNamespace, nodeName) fmt.Printf("Starting host networking pod %s/%s on node %s\n", debugPodNamespace, pod.Name, nodeName) diff --git a/shell/validation.go b/shell/validation.go index fa97f8b15e..358536ffb1 100644 --- a/shell/validation.go +++ b/shell/validation.go @@ -9,18 +9,28 @@ import ( "k8s.io/client-go/kubernetes" ) -var errUnsupportedOperatingSystem = errors.New("unsupported OS (retina-shell requires Linux)") +var errUnsupportedOperatingSystem = errors.New("unsupported OS (retina-shell requires Linux or Windows)") -func validateOperatingSystemSupportedForNode(ctx context.Context, clientset *kubernetes.Clientset, nodeName string) error { +// GetNodeOS retrieves the operating system of a node from its labels +func GetNodeOS(ctx context.Context, clientset kubernetes.Interface, nodeName string) (string, error) { node, err := clientset.CoreV1(). Nodes(). Get(ctx, nodeName, metav1.GetOptions{}) if err != nil { - return fmt.Errorf("error retrieving node %s: %w", nodeName, err) + return "", fmt.Errorf("error retrieving node %s: %w", nodeName, err) } - osLabel := node.Labels["kubernetes.io/os"] - if osLabel != "linux" { // Only Linux supported for now. + return node.Labels["kubernetes.io/os"], nil +} + +func validateOperatingSystemSupportedForNode(ctx context.Context, clientset kubernetes.Interface, nodeName string) error { + os, err := GetNodeOS(ctx, clientset, nodeName) + if err != nil { + return err + } + + // Support both Linux and Windows + if os != "linux" && os != "windows" { return errUnsupportedOperatingSystem } diff --git a/shell/validation_test.go b/shell/validation_test.go new file mode 100644 index 0000000000..36bd22dc4f --- /dev/null +++ b/shell/validation_test.go @@ -0,0 +1,106 @@ +package shell + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" +) + +// Extended interface for testing that combines both interfaces +type clientsetInterface interface { + kubernetes.Interface +} + +func TestValidateOperatingSystemSupportedForNode(t *testing.T) { + tests := []struct { + name string + osLabel string + wantError bool + }{ + { + name: "linux node", + osLabel: "linux", + wantError: false, + }, + { + name: "windows node", + osLabel: "windows", + wantError: false, + }, + { + name: "unsupported OS", + osLabel: "darwin", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a fake client with a node that has the specified OS label + clientset := fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Labels: map[string]string{ + "kubernetes.io/os": tt.osLabel, + }, + }, + }) + + // Use the clientset as kubernetes.Interface + var cs clientsetInterface = clientset + + err := validateOperatingSystemSupportedForNode(context.Background(), cs, "test-node") + if tt.wantError { + assert.Error(t, err, "Expected error for OS %s", tt.osLabel) + assert.Equal(t, errUnsupportedOperatingSystem, err) + } else { + assert.NoError(t, err, "Expected no error for OS %s", tt.osLabel) + } + }) + } +} + +func TestGetNodeOS(t *testing.T) { + tests := []struct { + name string + osLabel string + wantOS string + }{ + { + name: "linux node", + osLabel: "linux", + wantOS: "linux", + }, + { + name: "windows node", + osLabel: "windows", + wantOS: "windows", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a fake client with a node that has the specified OS label + clientset := fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Labels: map[string]string{ + "kubernetes.io/os": tt.osLabel, + }, + }, + }) + + // Use the clientset as kubernetes.Interface + var cs clientsetInterface = clientset + + os, err := GetNodeOS(context.Background(), cs, "test-node") + assert.NoError(t, err) + assert.Equal(t, tt.wantOS, os) + }) + } +} \ No newline at end of file From 3bce1e732efe9f1a9662ab6e279152935a2129c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 21:37:05 +0000 Subject: [PATCH 3/5] Update documentation for Windows support in retina-shell Co-authored-by: rbtr <2940321+rbtr@users.noreply.github.com> --- docs/06-Troubleshooting/shell.md | 49 +++++++++++++++++++++++++++++++- shell/README.md | 8 ++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/06-Troubleshooting/shell.md b/docs/06-Troubleshooting/shell.md index 3bf5adbb1d..3b5c342143 100644 --- a/docs/06-Troubleshooting/shell.md +++ b/docs/06-Troubleshooting/shell.md @@ -14,6 +14,9 @@ kubectl retina shell aks-nodepool1-15232018-vmss000001 # To start a shell inside a pod (pod network namespace): kubectl retina shell -n kube-system pods/coredns-d459997b4-7cpzx + +# To start a shell in a Windows node: +kubectl retina shell win-node-001 ``` Check connectivity using `ping`: @@ -146,6 +149,43 @@ root [ / ]# chroot /host systemctl status | head -n 2 **If `systemctl` shows an error "Failed to connect to bus: No data available", check that the `retina shell` command has `--host-pid` set and that you have chroot'd to /host.** +## Windows Support + +Retina shell supports Windows nodes by automatically detecting the node OS and using a Windows container image with appropriate networking tools. + +### Windows Tools and Commands + +When using a Windows node, you'll have access to these networking tools: + +- `ipconfig`: Show network configuration +- `netstat`: Show network connections +- `ping`: Test connectivity +- `tracert`: Trace route to destination +- `nslookup`: DNS lookup +- `route`: Show/manipulate routing table +- `netsh`: Network shell for configuration +- `nmap`: Network discovery and security auditing +- `portqry`: Port scanner +- `windump`: Packet analyzer (tcpdump for Windows) + +### Windows Example + +```bash +# Start a shell in a Windows node +kubectl retina shell win-node-001 + +# You can specify a specific Windows image tag variant +kubectl retina shell win-node-001 --windows-image-tag windows-ltsc2019-amd64 +``` + +### Windows Host Filesystem Access + +For Windows nodes, the host filesystem is mounted at `C:\host` when using `--mount-host-filesystem`: + +```bash +kubectl retina shell win-node-001 --mount-host-filesystem +``` + ## Troubleshooting ### Timeouts @@ -176,10 +216,17 @@ export RETINA_SHELL_IMAGE_VERSION=v0.0.1 # optional, if not set defaults to the kubectl retina shell node0001 # this will use the image "example.azurecr.io/retina/retina-shell:v0.0.1" ``` +For Windows images, you can also override the Windows image tag suffix: + +```bash +export RETINA_SHELL_WINDOWS_IMAGE_TAG="windows-ltsc2019-amd64" +kubectl retina shell win-node-001 # will use the Windows image with the specified tag suffix +``` + ## Limitations -* Windows nodes and pods are not yet supported. * `bpftool` and `bpftrace` are not supported. * The shell image links `iptables` commands to `iptables-nft`, even if the node itself links to `iptables-legacy`. * `nsenter` is not supported. * `ip netns` will not work without `chroot` to the host filesystem. +* On Windows, commands specific to Linux containers are not available (e.g., iptables, nft). diff --git a/shell/README.md b/shell/README.md index 1f3033814c..9042eb2a86 100644 --- a/shell/README.md +++ b/shell/README.md @@ -3,12 +3,14 @@ Retina CLI provides a command to launch an interactive shell in a node or pod for adhoc debugging. * The CLI command `kubectl retina shell` creates a pod with `HostNetwork=true` (for node debugging) or an ephemeral container in an existing pod (for pod debugging). -* The container runs an image built from the Dockerfile in this directory. The image is based on Azure Linux and includes commonly-used networking tools. +* For Linux nodes, the container runs an image built from the Dockerfile in this directory, based on Azure Linux and includes commonly-used networking tools. +* For Windows nodes, the container runs a Windows-based image with Windows networking utilities built from Dockerfile.windows. For testing, you can override the image used by `retina shell` either with CLI arguments (`--retina-shell-image-repo` and `--retina-shell-image-version`) or environment variables (`RETINA_SHELL_IMAGE_REPO` and `RETINA_SHELL_IMAGE_VERSION`). -Run `kubectl retina shell -h` for full documentation and examples. +For Windows nodes, you can specify the Windows image tag suffix with the `--windows-image-tag` flag or +the `RETINA_SHELL_WINDOWS_IMAGE_TAG` environment variable. -Currently only Linux is supported; Windows support will be added in the future. +Run `kubectl retina shell -h` for full documentation and examples. From b685638e79fe20dcc4c47ef6eb8af267280844e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 May 2025 23:35:03 +0000 Subject: [PATCH 4/5] Add GitHub Actions workflow job for retina-shell-win-images and run formatter Co-authored-by: nddq <28567936+nddq@users.noreply.github.com> --- .github/workflows/images.yaml | 53 +++++++++++++++++++ cli/cmd/root.go | 4 +- .../simplify-grafana-overwrite_test.go | 1 - .../dashboards/simplify-grafana_test.go | 1 - operator/cilium-crds/k8s/fakeresource.go | 3 +- pkg/k8s/watcher_linux.go | 4 +- pkg/plugin/hnsstats/vfp_counters_windows.go | 1 - pkg/plugin/linuxutil/types_linux.go | 6 ++- pkg/telemetry/telemetry.go | 1 + shell/manifests.go | 4 +- shell/manifests_test.go | 16 +++--- shell/validation_test.go | 2 +- .../framework/kubernetes/check-pod-status.go | 2 - .../framework/kubernetes/get-external-crd.go | 2 +- .../kubernetes/install-retina-helm.go | 2 +- .../framework/scaletest/add-shared-labels.go | 1 - .../framework/scaletest/add-unique-labels.go | 1 - .../scaletest/create-network-policies.go | 1 - .../scaletest/delete-and-re-add-labels.go | 1 - .../scaletest/get-publish-metrics.go | 5 -- .../scaletest/templates/networkpolicy.go | 34 ++++++------ .../framework/scaletest/validate-options.go | 1 - test/e2e/retina_perf_test.go | 8 ++- .../scenarios/perf/publish-perf-results.go | 2 +- test/multicloud/test/utils/utils_test.go | 2 +- 25 files changed, 97 insertions(+), 61 deletions(-) diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 18dfc3e7c2..0f5db8f1da 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -213,6 +213,58 @@ jobs: fi env: IS_MERGE_GROUP: ${{ github.event_name == 'merge_group' }} + + retina-shell-win-images: + name: Build Retina Shell Windows Images + runs-on: ubuntu-latest + + strategy: + matrix: + platform: ["windows"] + arch: ["amd64"] + year: ["2019", "2022"] + + steps: + - name: Checkout code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version-file: go.mod + - run: go version + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Az CLI login + uses: azure/login@v2 + if: ${{ github.event_name == 'merge_group' }} + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION }} + + - name: Build Images + shell: bash + run: | + set -euo pipefail + echo "TAG=$(make version)" >> $GITHUB_ENV + if [ "$IS_MERGE_GROUP" == "true" ]; then + az acr login -n ${{ vars.ACR_NAME }} + make retina-shell-image-win \ + IMAGE_NAMESPACE=${{ github.repository }} \ + PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ + IMAGE_REGISTRY=${{ vars.ACR_NAME }} \ + WINDOWS_YEARS=${{ matrix.year }} \ + BUILDX_ACTION=--push + else + make retina-shell-image-win \ + IMAGE_NAMESPACE=${{ github.repository }} \ + PLATFORM=${{ matrix.platform }}/${{ matrix.arch }} \ + WINDOWS_YEARS=${{ matrix.year }} + fi + env: + IS_MERGE_GROUP: ${{ github.event_name == 'merge_group' }} kubectl-retina-images: name: Build Kubectl Retina Images @@ -273,6 +325,7 @@ jobs: retina-win-images, operator-images, retina-shell-images, + retina-shell-win-images, kubectl-retina-images, ] diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 6413b4de89..f2cd588fd6 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -24,9 +24,9 @@ type Config struct { } var Retina = &cobra.Command{ - Use: "kubectl-retina", + Use: "kubectl-retina", Short: "A kubectl plugin for Retina", - Long: "A kubectl plugin for Retina\nRetina is an eBPF distributed networking observability tool for Kubernetes.", + Long: "A kubectl plugin for Retina\nRetina is an eBPF distributed networking observability tool for Kubernetes.", PersistentPreRun: func(*cobra.Command, []string) { var config Config file, _ := os.ReadFile(ClientConfigPath) diff --git a/deploy/testutils/grafana/dashboards/simplify-grafana-overwrite_test.go b/deploy/testutils/grafana/dashboards/simplify-grafana-overwrite_test.go index 499d6f51c8..797225f15b 100644 --- a/deploy/testutils/grafana/dashboards/simplify-grafana-overwrite_test.go +++ b/deploy/testutils/grafana/dashboards/simplify-grafana-overwrite_test.go @@ -11,7 +11,6 @@ import ( func TestOverwriteDashboards(t *testing.T) { // get all json's in various generation deploly folders files, err := filepath.Glob("../../../grafana-dashboards/*.json") - if err != nil { t.Fatal(err) } diff --git a/deploy/testutils/grafana/dashboards/simplify-grafana_test.go b/deploy/testutils/grafana/dashboards/simplify-grafana_test.go index 2c8e271d26..d969e14051 100644 --- a/deploy/testutils/grafana/dashboards/simplify-grafana_test.go +++ b/deploy/testutils/grafana/dashboards/simplify-grafana_test.go @@ -12,7 +12,6 @@ import ( func TestDashboardsAreSimplified(t *testing.T) { // get all json's in this folder files, err := filepath.Glob("../../../grafana-dashboards/*.json") - if err != nil { t.Fatal(err) } diff --git a/operator/cilium-crds/k8s/fakeresource.go b/operator/cilium-crds/k8s/fakeresource.go index c212ca61bb..c02a2286d1 100644 --- a/operator/cilium-crds/k8s/fakeresource.go +++ b/operator/cilium-crds/k8s/fakeresource.go @@ -8,8 +8,7 @@ import ( "github.com/cilium/cilium/pkg/k8s/resource" ) -type fakeresource[T k8sRuntime.Object] struct { -} +type fakeresource[T k8sRuntime.Object] struct{} func (f *fakeresource[T]) Events(ctx context.Context, opts ...resource.EventsOpt) <-chan resource.Event[T] { return make(<-chan resource.Event[T]) diff --git a/pkg/k8s/watcher_linux.go b/pkg/k8s/watcher_linux.go index e1a119b03c..0129a30f4f 100644 --- a/pkg/k8s/watcher_linux.go +++ b/pkg/k8s/watcher_linux.go @@ -23,9 +23,7 @@ func init() { } } -var ( - logger = logging.DefaultLogger.WithField(logfields.LogSubsys, "k8s-watcher") -) +var logger = logging.DefaultLogger.WithField(logfields.LogSubsys, "k8s-watcher") func Start(ctx context.Context, k *watchers.K8sWatcher) { logger.Info("Starting Kubernetes watcher") diff --git a/pkg/plugin/hnsstats/vfp_counters_windows.go b/pkg/plugin/hnsstats/vfp_counters_windows.go index 8bdcfefecb..339d32a20d 100644 --- a/pkg/plugin/hnsstats/vfp_counters_windows.go +++ b/pkg/plugin/hnsstats/vfp_counters_windows.go @@ -152,7 +152,6 @@ func getVfpPortCountersRaw(portGUID string) (string, error) { cmd := exec.Command("cmd", "/c", vfpCmd) out, err := cmd.Output() - if err != nil { return "", errors.Wrap(err, "errored while running vfpctrl /get-port-counter") } diff --git a/pkg/plugin/linuxutil/types_linux.go b/pkg/plugin/linuxutil/types_linux.go index e7c20764fc..4509c84f65 100644 --- a/pkg/plugin/linuxutil/types_linux.go +++ b/pkg/plugin/linuxutil/types_linux.go @@ -8,8 +8,10 @@ import ( "github.com/microsoft/retina/pkg/log" ) -const name = "linuxutil" -const defaultLimit = 2000 +const ( + name = "linuxutil" + defaultLimit = 2000 +) //go:generate go run go.uber.org/mock/mockgen@v0.4.0 -source=types_linux.go -destination=linuxutil_mock_generated_linux.go -package=linuxutil type linuxUtil struct { diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 0c91f32e67..61462cfc8a 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -193,6 +193,7 @@ func (t *TelemetryClient) heartbeat(ctx context.Context) { maps.Copy(props, t.profile.GetMemoryUsage()) t.TrackEvent("heartbeat", props) } + func metricsCardinality(gatherer prometheus.Gatherer) (int, error) { if gatherer == nil { return 0, fmt.Errorf("failed to get metrics Gatherer: %w", ErrorNilCombinedGatherer) diff --git a/shell/manifests.go b/shell/manifests.go index f148f4e57d..0a97f185f8 100644 --- a/shell/manifests.go +++ b/shell/manifests.go @@ -72,12 +72,12 @@ func hostNetworkPodForNodeDebug(config Config, debugPodNamespace, nodeName strin if config.MountHostFilesystem || config.AllowHostFilesystemWrite { hostPath := "/" mountPath := "/host" - + if config.NodeOS == "windows" { hostPath = "C:\\" mountPath = "C:\\host" } - + pod.Spec.Volumes = append(pod.Spec.Volumes, v1.Volume{ Name: "host-filesystem", VolumeSource: v1.VolumeSource{ diff --git a/shell/manifests_test.go b/shell/manifests_test.go index c7793f7ce9..d04c36f51a 100644 --- a/shell/manifests_test.go +++ b/shell/manifests_test.go @@ -8,8 +8,10 @@ import ( v1 "k8s.io/api/core/v1" ) -const testRetinaImage = "retina-shell:v0.0.1" -const testRetinaWindowsImage = "retina-shell:v0.0.1-windows-ltsc2022-amd64" +const ( + testRetinaImage = "retina-shell:v0.0.1" + testRetinaWindowsImage = "retina-shell:v0.0.1-windows-ltsc2022-amd64" +) func TestEphemeralContainerForPodDebug(t *testing.T) { ec := ephemeralContainerForPodDebug(Config{RetinaShellImage: testRetinaImage}) @@ -98,14 +100,14 @@ func TestHostNetworkPodForWindowsNodeDebug(t *testing.T) { NodeOS: "windows", } pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") - + // Verify Windows specific configurations assert.NotNil(t, pod.Spec.SecurityContext) assert.NotNil(t, pod.Spec.SecurityContext.WindowsOptions) assert.NotNil(t, pod.Spec.SecurityContext.WindowsOptions.HostProcess) assert.True(t, *pod.Spec.SecurityContext.WindowsOptions.HostProcess) assert.Equal(t, "NT AUTHORITY\\SYSTEM", *pod.Spec.SecurityContext.WindowsOptions.RunAsUserName) - + // HostPID should be false for Windows regardless of setting assert.False(t, pod.Spec.HostPID) } @@ -117,11 +119,11 @@ func TestHostNetworkPodForWindowsNodeDebugWithMountHostFilesystem(t *testing.T) MountHostFilesystem: true, } pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") - + assert.Len(t, pod.Spec.Volumes, 1) assert.Equal(t, "host-filesystem", pod.Spec.Volumes[0].Name) assert.Equal(t, "C:\\", pod.Spec.Volumes[0].VolumeSource.HostPath.Path) - + assert.Len(t, pod.Spec.Containers[0].VolumeMounts, 1) assert.Equal(t, "host-filesystem", pod.Spec.Containers[0].VolumeMounts[0].Name) assert.Equal(t, "C:\\host", pod.Spec.Containers[0].VolumeMounts[0].MountPath) @@ -134,7 +136,7 @@ func TestHostNetworkPodForWindowsNodeWithHostPID(t *testing.T) { HostPID: true, // Should be ignored for Windows } pod := hostNetworkPodForNodeDebug(config, "kube-system", "win-node0001") - + // HostPID should be false for Windows regardless of the setting assert.False(t, pod.Spec.HostPID) } diff --git a/shell/validation_test.go b/shell/validation_test.go index 36bd22dc4f..d280ae3fab 100644 --- a/shell/validation_test.go +++ b/shell/validation_test.go @@ -103,4 +103,4 @@ func TestGetNodeOS(t *testing.T) { assert.Equal(t, tt.wantOS, os) }) } -} \ No newline at end of file +} diff --git a/test/e2e/framework/kubernetes/check-pod-status.go b/test/e2e/framework/kubernetes/check-pod-status.go index 8c51152cd8..2926bcd7ef 100644 --- a/test/e2e/framework/kubernetes/check-pod-status.go +++ b/test/e2e/framework/kubernetes/check-pod-status.go @@ -37,7 +37,6 @@ func (w *WaitPodsReady) Prevalidate() error { // Primary step where test logic is executed // Returning an error will cause the test to fail func (w *WaitPodsReady) Run() error { - config, err := clientcmd.BuildConfigFromFlags("", w.KubeConfigFilePath) if err != nil { return fmt.Errorf("error building kubeconfig: %w", err) @@ -59,7 +58,6 @@ func (w *WaitPodsReady) Stop() error { } func WaitForPodReady(ctx context.Context, clientset *kubernetes.Clientset, namespace, labelSelector string) error { - printIterator := 0 conditionFunc := wait.ConditionWithContextFunc(func(context.Context) (bool, error) { defer func() { diff --git a/test/e2e/framework/kubernetes/get-external-crd.go b/test/e2e/framework/kubernetes/get-external-crd.go index 1564691362..6e80e226ab 100644 --- a/test/e2e/framework/kubernetes/get-external-crd.go +++ b/test/e2e/framework/kubernetes/get-external-crd.go @@ -55,7 +55,7 @@ func extractFileName(rawURL string) (string, error) { } func saveToFile(filename string, data []byte) error { - err := os.WriteFile(filename, data, 0644) + err := os.WriteFile(filename, data, 0o644) if err != nil { return fmt.Errorf("failed to write crd.yaml to /crds dir : %w", err) } diff --git a/test/e2e/framework/kubernetes/install-retina-helm.go b/test/e2e/framework/kubernetes/install-retina-helm.go index 5b03d65ccb..e9800cfd2f 100644 --- a/test/e2e/framework/kubernetes/install-retina-helm.go +++ b/test/e2e/framework/kubernetes/install-retina-helm.go @@ -68,7 +68,7 @@ func (i *InstallHelmChart) Run() error { return fmt.Errorf("image namespace is not set: %w", errEmpty) } - //Download necessary CRD's + // Download necessary CRD's err = downloadExternalCRDs(i.ChartPath) if err != nil { return fmt.Errorf("failed to load external crd's: %w", err) diff --git a/test/e2e/framework/scaletest/add-shared-labels.go b/test/e2e/framework/scaletest/add-shared-labels.go index 027f711994..7c44160d65 100644 --- a/test/e2e/framework/scaletest/add-shared-labels.go +++ b/test/e2e/framework/scaletest/add-shared-labels.go @@ -35,7 +35,6 @@ func (a *AddSharedLabelsToAllPods) Prevalidate() error { // Primary step where test logic is executed // Returning an error will cause the test to fail func (a *AddSharedLabelsToAllPods) Run() error { - if a.NumSharedLabelsPerPod < 1 { return nil } diff --git a/test/e2e/framework/scaletest/add-unique-labels.go b/test/e2e/framework/scaletest/add-unique-labels.go index e196caabb1..02e1b486df 100644 --- a/test/e2e/framework/scaletest/add-unique-labels.go +++ b/test/e2e/framework/scaletest/add-unique-labels.go @@ -27,7 +27,6 @@ func (a *AddUniqueLabelsToAllPods) Prevalidate() error { // Primary step where test logic is executed // Returning an error will cause the test to fail func (a *AddUniqueLabelsToAllPods) Run() error { - if a.NumUniqueLabelsPerPod < 1 { return nil } diff --git a/test/e2e/framework/scaletest/create-network-policies.go b/test/e2e/framework/scaletest/create-network-policies.go index 18b7a9b90a..0480070ea5 100644 --- a/test/e2e/framework/scaletest/create-network-policies.go +++ b/test/e2e/framework/scaletest/create-network-policies.go @@ -33,7 +33,6 @@ func (c *CreateNetworkPolicies) Prevalidate() error { // Primary step where test logic is executed // Returning an error will cause the test to fail func (c *CreateNetworkPolicies) Run() error { - config, err := clientcmd.BuildConfigFromFlags("", c.KubeConfigFilePath) if err != nil { return fmt.Errorf("error building kubeconfig: %w", err) diff --git a/test/e2e/framework/scaletest/delete-and-re-add-labels.go b/test/e2e/framework/scaletest/delete-and-re-add-labels.go index db215234e4..2ecead296f 100644 --- a/test/e2e/framework/scaletest/delete-and-re-add-labels.go +++ b/test/e2e/framework/scaletest/delete-and-re-add-labels.go @@ -118,7 +118,6 @@ func (d *DeleteAndReAddLabels) addLabels(ctx context.Context, clientset *kuberne } func (d *DeleteAndReAddLabels) deleteLabels(ctx context.Context, clientset *kubernetes.Clientset, pods *corev1.PodList, patch string) error { - for _, pod := range pods.Items { log.Println("Deleting label from Pod", pod.Name) diff --git a/test/e2e/framework/scaletest/get-publish-metrics.go b/test/e2e/framework/scaletest/get-publish-metrics.go index c64703f1f7..5f15c9276d 100644 --- a/test/e2e/framework/scaletest/get-publish-metrics.go +++ b/test/e2e/framework/scaletest/get-publish-metrics.go @@ -76,7 +76,6 @@ func (g *GetAndPublishMetrics) Run() error { g.errs = new(errgroup.Group) g.errs.Go(func() error { - t := time.NewTicker(defaultInterval) defer t.Stop() @@ -100,7 +99,6 @@ func (g *GetAndPublishMetrics) Run() error { } } - }) return nil @@ -135,7 +133,6 @@ func (g *GetAndPublishMetrics) Prevalidate() error { } func (g *GetAndPublishMetrics) getAndPublishMetrics() error { - ctx := context.TODO() labelSelector := labels.Set(g.Labels).String() @@ -161,7 +158,6 @@ func (g *GetAndPublishMetrics) getAndPublishMetrics() error { log.Println("Publishing metrics to AppInsights") for _, metric := range allMetrics { g.telemetryClient.TrackEvent("scale-test", metric) - } } @@ -192,7 +188,6 @@ func (g *GetAndPublishMetrics) getAndPublishMetrics() error { type metric map[string]string func (g *GetAndPublishMetrics) getPodsMetrics(ctx context.Context, labelSelector string) ([]metric, error) { - var pods *v1.PodList retrier := retry.Retrier{Attempts: defaultRetryAttempts, Delay: defaultRetryDelay} diff --git a/test/e2e/framework/scaletest/templates/networkpolicy.go b/test/e2e/framework/scaletest/templates/networkpolicy.go index 2eef3e3161..936767b3d7 100644 --- a/test/e2e/framework/scaletest/templates/networkpolicy.go +++ b/test/e2e/framework/scaletest/templates/networkpolicy.go @@ -5,23 +5,21 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -var ( - NetworkPolicy = netv1.NetworkPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: "NetworkPolicy", - APIVersion: "networking.k8s.io/v1", +var NetworkPolicy = netv1.NetworkPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: "NetworkPolicy", + APIVersion: "networking.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "template-network-policy", + }, + Spec: netv1.NetworkPolicySpec{ + PolicyTypes: []netv1.PolicyType{ + "Ingress", + "Egress", }, - ObjectMeta: metav1.ObjectMeta{ - Name: "template-network-policy", + PodSelector: metav1.LabelSelector{ + MatchLabels: map[string]string{}, }, - Spec: netv1.NetworkPolicySpec{ - PolicyTypes: []netv1.PolicyType{ - "Ingress", - "Egress", - }, - PodSelector: metav1.LabelSelector{ - MatchLabels: map[string]string{}, - }, - }, - } -) + }, +} diff --git a/test/e2e/framework/scaletest/validate-options.go b/test/e2e/framework/scaletest/validate-options.go index 0dafdd2b06..588bdc993b 100644 --- a/test/e2e/framework/scaletest/validate-options.go +++ b/test/e2e/framework/scaletest/validate-options.go @@ -37,7 +37,6 @@ func (po *ValidateAndPrintOptions) Prevalidate() error { // Returning an error will cause the test to fail func (po *ValidateAndPrintOptions) Run() error { - log.Printf("Starting to scale with folowing options: %+v", po.Options) return nil diff --git a/test/e2e/retina_perf_test.go b/test/e2e/retina_perf_test.go index 7346c64285..bee1d044d5 100644 --- a/test/e2e/retina_perf_test.go +++ b/test/e2e/retina_perf_test.go @@ -17,11 +17,9 @@ import ( "github.com/stretchr/testify/require" ) -var ( - // Add flags for the test - // retina-mode: basic, advanced, hubble - retinaMode = flag.String("retina-mode", "basic", "One of basic or advanced") -) +// Add flags for the test +// retina-mode: basic, advanced, hubble +var retinaMode = flag.String("retina-mode", "basic", "One of basic or advanced") func validateRetinaMode(t *testing.T) { switch *retinaMode { diff --git a/test/e2e/scenarios/perf/publish-perf-results.go b/test/e2e/scenarios/perf/publish-perf-results.go index a141098d5f..ab33161673 100644 --- a/test/e2e/scenarios/perf/publish-perf-results.go +++ b/test/e2e/scenarios/perf/publish-perf-results.go @@ -29,7 +29,7 @@ func (v *PublishPerfResults) Run() error { return nil } - resultsFile, err := os.OpenFile(v.ResultsFile, os.O_RDONLY, 0644) + resultsFile, err := os.OpenFile(v.ResultsFile, os.O_RDONLY, 0o644) if err != nil { return errors.Wrap(err, "failed to open results file") } diff --git a/test/multicloud/test/utils/utils_test.go b/test/multicloud/test/utils/utils_test.go index 1a5df25e8a..106b592625 100644 --- a/test/multicloud/test/utils/utils_test.go +++ b/test/multicloud/test/utils/utils_test.go @@ -159,7 +159,7 @@ func TestFetchSensitiveOutput(t *testing.T) { if err != nil { t.Fatalf("Failed to marshal mock state: %v", err) } - if err := os.WriteFile(stateFilePath, stateData, 0644); err != nil { + if err := os.WriteFile(stateFilePath, stateData, 0o644); err != nil { t.Fatalf("Failed to write mock state file: %v", err) } From 04890c2dbdcb67a7e79278575a5999a385314bc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 20 May 2025 15:08:26 +0000 Subject: [PATCH 5/5] Simplify Windows shell container to use just PowerShell Co-authored-by: nddq <28567936+nddq@users.noreply.github.com> --- shell/Dockerfile.windows | 41 +++------------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/shell/Dockerfile.windows b/shell/Dockerfile.windows index ab8c7d0196..347597d0db 100644 --- a/shell/Dockerfile.windows +++ b/shell/Dockerfile.windows @@ -5,46 +5,11 @@ FROM mcr.microsoft.com/windows/servercore:${WINDOWS_VERSION} SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] -# Install common network troubleshooting tools -# - Netstat, ping, ipconfig, etc. are built into Windows -# - NirSoft WinDump (tcpdump equivalent for Windows) -# - Portqry - port scanner - -# Download and install NirSoft WinDump -RUN mkdir -p C:\tools; \ - Invoke-WebRequest -Uri "https://www.nirsoft.net/utils/windump_setup.exe" -OutFile "C:\tools\windump_setup.exe" -UseBasicParsing; \ - Start-Process -FilePath "C:\tools\windump_setup.exe" -ArgumentList "/S" -Wait; \ - Remove-Item -Path "C:\tools\windump_setup.exe" -Force - -# Download and install PortQry -RUN Invoke-WebRequest -Uri "https://download.microsoft.com/download/3/b/5/3b51a025-7522-4686-87a6-47aa8ea68141/PortQryV2.exe" -OutFile "C:\Windows\System32\PortQry.exe" -UseBasicParsing - -# Download and extract Nmap network scanner -RUN Invoke-WebRequest -Uri "https://nmap.org/dist/nmap-7.92-win32.zip" -OutFile "C:\tools\nmap.zip" -UseBasicParsing; \ - Expand-Archive -Path "C:\tools\nmap.zip" -DestinationPath "C:\tools" -Force; \ - Move-Item -Path "C:\tools\nmap-*\*" -Destination "C:\tools\" -Force; \ - $env:PATH = "C:\tools;$env:PATH" +# PowerShell is already included in Windows Server Core +# No additional tools needed - using only built-in Windows commands # Create a convenience script to display available tools -RUN Set-Content -Path "C:\tools\show-tools.cmd" -Value @" -@echo off -echo. -echo Available Network Troubleshooting Tools: -echo - ipconfig : Show network configuration -echo - netstat : Show network connections -echo - ping : Test connectivity -echo - tracert : Trace route -echo - nslookup : DNS lookup -echo - route : Show/manipulate routing table -echo - netsh : Network shell for configuration -echo - nmap : Network discovery and security auditing -echo - portqry : Port scanner -echo - windump : Packet analyzer (tcpdump for Windows) -echo. -"@ - -# Set PATH to include tools -ENV PATH="C:\tools;C:\Windows\System32;C:\Windows;C:\Windows\System32\WindowsPowerShell\v1.0" +RUN Set-Content -Path "C:\show-tools.cmd" -Value "@echo off\necho.\necho Available Windows Commands:\necho - ipconfig : Show network configuration\necho - netstat : Show network connections\necho - ping : Test connectivity\necho - tracert : Trace route\necho - nslookup : DNS lookup\necho - route : Show/manipulate routing table\necho - netsh : Network shell for configuration\necho." WORKDIR C:\