Setting the file. One moment. Retrieve Captures · AKS Network Capture · microsoft/azure-skills · Skills DocsScript Generate Test Traffic
scripts/retrieve-captures.ps1
PowerShell·264 lines·9 KB
WORKSPACE_DIR
9 } else {
10 Join-Path "." "aks-network-captures"
11 }),
12 [string]$Namespace = "default",
13 [string]$RunId,
14 [switch]$Help
15)
16
17function Write-Usage {
18 @"
19Usage: retrieve-captures.ps1 -Name <capture-name> [options]
20
21Options:
22 -OutputPath <path> Node hostPath base
23 -WorkspaceDir <path> Local workspace directory
24 -Namespace <name> Capture Job namespace (default: default)
25 -RunId <id> Exact run ID; required when multiple runs exist
26"@
27}
28
29function Stop-Usage {
30 param([string]$Message)
31 [Console]::Error.WriteLine("Error: $Message")
32 [Console]::Error.WriteLine((Write-Usage))
33 exit 2
34}
35
36function Stop-Failure {
37 param([string]$Message)
38 [Console]::Error.WriteLine("Error: $Message")
39 exit 1
40}
41
42function Test-Rfc1123Label {
43 param([string]$Value)
44 return $Value.Length -le 63 -and
45 $Value -cmatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$"
46}
47
48function Test-DnsSubdomain {
49 param([string]$Value)
50 return $Value.Length -le 253 -and
51 $Value -cmatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$"
52}
53
54function Test-RunId {
55 param([string]$Value)
56 return $Value -cmatch "^[0-9]{8}-[0-9]{6}-[0-9a-f]{24}$"
57}
58
59function Invoke-KubectlChecked {
60 param([string[]]$KubectlArguments, [string]$FailureMessage)
61 $output = (& kubectl @KubectlArguments 2>&1) -join "`n"
62 if ($LASTEXITCODE -ne 0) {
63 throw "$FailureMessage`n$output"
64 }
65 return $output.Trim()
66}
67
68if ($Help) {
69 Write-Usage
70 exit 0
71}
72if ([string]::IsNullOrWhiteSpace($Name)) {
73 Stop-Usage "-Name is required"
74}
75if (-not (Test-Rfc1123Label $Name)) {
76 Stop-Usage "-Name must be an RFC 1123 label"
77}
78if (-not (Test-Rfc1123Label $Namespace)) {
79 Stop-Usage "-Namespace must be an RFC 1123 label"
80}
81if ($OutputPath -notmatch "^/[A-Za-z0-9/_.-]+$" -or $OutputPath.Contains("..")) {
82 Stop-Usage "-OutputPath must be absolute and contain only letters, digits, '/', '_', '.', or '-'"
83}
84if ([string]::IsNullOrWhiteSpace($WorkspaceDir) -or $WorkspaceDir -match "[`r`n]") {
85 Stop-Usage "-WorkspaceDir must be a non-empty single-line path"
86}
87if ($RunId -and -not (Test-RunId $RunId)) {
88 Stop-Usage "-RunId has an invalid format"
89}
90if (-not (Get-Command kubectl -ErrorAction SilentlyContinue)) {
91 Stop-Failure "kubectl is not installed or not on PATH"
92}
93
94try {
95 if (-not $RunId) {
96 $runsOutput = Invoke-KubectlChecked -KubectlArguments @(
97 "get", "jobs", "-n", $Namespace, "-l", "capture-id=$Name",
98 "-o", 'jsonpath={range .items[*]}{.metadata.labels.capture-run}{"\n"}{end}'
99 ) -FailureMessage "could not list capture runs"
100 $runs = @($runsOutput -split "`n" |
101 ForEach-Object { $_.Trim() } |
102 Where-Object { $_ } |
103 Sort-Object -Unique)
104 if ($runs.Count -eq 0) {
105 throw "no capture runs found for '$Name'"
106 }
107 if ($runs.Count -gt 1) {
108 throw "multiple capture runs found; specify -RunId"
109 }
110 $RunId = $runs[0]
111 }
112 if (-not (Test-RunId $RunId)) {
113 throw "Kubernetes returned an invalid capture run identity"
114 }
115
116 $jobsOutput = Invoke-KubectlChecked -KubectlArguments @(
117 "get", "jobs", "-n", $Namespace,
118 "-l", "capture-id=$Name,capture-run=$RunId",
119 "-o", "jsonpath={.items[*].metadata.name}"
120 ) -FailureMessage "could not list capture Jobs"
121 $captureJobs = @($jobsOutput -split "\s+" | Where-Object { $_ })
122 if ($captureJobs.Count -eq 0) {
123 throw "no Jobs found for the requested capture run"
124 }
125} catch {
126 Stop-Failure $_.Exception.Message
127}
128
129$timestamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd-HHmmss")
130$captureOutputDir = Join-Path (Join-Path $WorkspaceDir "network-captures") "$Name-$RunId-$timestamp"
131try {
132 New-Item -ItemType Directory -Path $captureOutputDir -Force -ErrorAction Stop | Out-Null
133} catch {
134 Stop-Failure "could not create the local capture directory: $($_.Exception.Message)"
135}
136
137$tempPods = New-Object System.Collections.Generic.List[string]
138$operationError = $null
139$cleanupErrors = New-Object System.Collections.Generic.List[string]
140
141try {
142 foreach ($job in $captureJobs) {
143 $nodeName = Invoke-KubectlChecked -KubectlArguments @(
144 "get", "job", $job, "-n", $Namespace,
145 "-o", "jsonpath={.spec.template.spec.nodeName}"
146 ) -FailureMessage "could not read the node for Job '$job'"
147 $jobRunId = Invoke-KubectlChecked -KubectlArguments @(
148 "get", "job", $job, "-n", $Namespace,
149 "-o", 'jsonpath={.spec.template.spec.containers[0].env[?(@.name=="RUN_ID")].value}'
150 ) -FailureMessage "could not read the run identity for Job '$job'"
151 if (-not (Test-DnsSubdomain $nodeName)) {
152 throw "Job '$job' returned an invalid node name"
153 }
154 if (-not (Test-RunId $jobRunId) -or $jobRunId -ne $RunId) {
155 throw "Job '$job' does not match the requested run"
156 }
157
158 $podName = Invoke-KubectlChecked -KubectlArguments @(
159 "get", "pods", "-n", $Namespace, "-l", "job-name=$job",
160 "-o", "jsonpath={.items[0].metadata.name}"
161 ) -FailureMessage "could not find the capture pod for Job '$job'"
162 if ([string]::IsNullOrWhiteSpace($podName)) {
163 throw "no capture pod found for Job '$job'"
164 }
165 $podStatus = Invoke-KubectlChecked -KubectlArguments @(
166 "get", "pod", $podName, "-n", $Namespace,
167 "-o", "jsonpath={.status.phase}"
168 ) -FailureMessage "could not read capture pod status"
169 if ($podStatus -ne "Succeeded") {
170 throw "capture pod '$podName' did not succeed; inspect its logs before retrying"
171 }
172
173 $captureSlug = $Name.Substring(0, [Math]::Min(20, $Name.Length))
174 $normalizedNode = $nodeName -replace "[._]", "-"
175 $nodeSlug = $normalizedNode.Substring(0, [Math]::Min(20, $normalizedNode.Length))
176 $retrievalYaml = @"
177apiVersion: v1
178kind: Pod
179metadata:
180 generateName: retrieve-$captureSlug-$nodeSlug-
181 namespace: $Namespace
182 labels: { app: aks-network-capture, capture-id: "$Name", role: retrieval }
183spec:
184 hostNetwork: true
185 nodeName: "$nodeName"
186 restartPolicy: Never
187 containers:
188 - name: retrieve
189 image: mcr.microsoft.com/cbl-mariner/busybox:2.0@sha256:e4fb4d51fc9b70d6cdc1ce66a0af02ab40554d2ca632e1d188fabc760e432fdd
190 command: ["sh", "-c", "sleep 300"]
191 volumeMounts:
192 - name: capture-output
193 mountPath: /capture-root
194 volumes:
195 - name: capture-output
196 hostPath:
197 path: $OutputPath/$Name
198 type: Directory
199"@
200 $tempPodName = ($retrievalYaml |
201 & kubectl create -f - -o "jsonpath={.metadata.name}" 2>&1) -join "`n"
202 if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($tempPodName)) {
203 throw "failed to create a retrieval pod on '$nodeName': $tempPodName"
204 }
205 $tempPodName = $tempPodName.Trim()
206 $tempPods.Add($tempPodName)
207
208 & kubectl wait -n $Namespace --for=condition=Ready `
209 "pod/$tempPodName" --timeout=60s
210 if ($LASTEXITCODE -ne 0) {
211 throw "retrieval pod '$tempPodName' did not become ready"
212 }
213
214 $bundleName = "capture-$Name-$nodeName-$jobRunId.tar.gz"
215 $runDir = "/capture-root/$jobRunId"
216 $localBundle = Join-Path $captureOutputDir $bundleName
217 & kubectl cp -n $Namespace "${tempPodName}:${runDir}/${bundleName}" `
218 $localBundle -c retrieve
219 if ($LASTEXITCODE -ne 0) {
220 throw "failed to copy '$bundleName' from node '$nodeName'"
221 }
222 if (-not (Test-Path -LiteralPath $localBundle -PathType Leaf) -or
223 (Get-Item -LiteralPath $localBundle).Length -eq 0) {
224 throw "the copied capture bundle '$bundleName' is empty"
225 }
226
227 & kubectl exec -n $Namespace $tempPodName -c retrieve -- sh -c `
228 'rm -f "$1/$2" "$1/$3" && rmdir "$1"' sh `
229 $runDir $bundleName "capture-$Name-$nodeName-$jobRunId.pcap"
230 if ($LASTEXITCODE -ne 0) {
231 throw "failed to remove the retrieved node artifacts"
232 }
233 & kubectl delete pod $tempPodName -n $Namespace --ignore-not-found
234 if ($LASTEXITCODE -ne 0) {
235 throw "failed to delete retrieval pod '$tempPodName'"
236 }
237 }
238} catch {
239 $operationError = $_.Exception.Message
240} finally {
241 foreach ($tempPod in $tempPods) {
242 & kubectl delete pod $tempPod -n $Namespace --ignore-not-found 2>$null | Out-Null
243 if ($LASTEXITCODE -ne 0) {
244 $cleanupErrors.Add("failed to delete retrieval pod '$tempPod'")
245 }
246 }
247 & kubectl delete jobs -n $Namespace @($captureJobs) --ignore-not-found 2>$null | Out-Null
248 if ($LASTEXITCODE -ne 0) {
249 $cleanupErrors.Add("failed to delete capture Jobs")
250 }
251}
252
253if ($operationError) {
254 if ($cleanupErrors.Count -gt 0) {
255 Stop-Failure "$operationError; cleanup also failed: $($cleanupErrors -join '; ')"
256 }
257 Stop-Failure $operationError
258}
259if ($cleanupErrors.Count -gt 0) {
260 Stop-Failure ($cleanupErrors -join "; ")
261}
262
263Write-Output "Capture bundles saved to: $captureOutputDir"
264Get-ChildItem -LiteralPath $captureOutputDir