Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 48 additions & 12 deletions evals/azure-skills/azure-diagnostics/eval.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ stimuli:
tier: smoke
cost: llm
area: routing
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"skill-call","skill":"azure-diagnostics"},{"type":"tool-call-count","count":3}]'
graders:
- type: skill-invocation
Expand All @@ -59,8 +59,8 @@ stimuli:
tier: full
cost: llm
area: routing
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"skill-call","skill":"azure-diagnostics"},{"type":"tool-call-count","count":3}]'
graders:
- type: skill-invocation
Expand All @@ -82,8 +82,8 @@ stimuli:
tier: full
cost: llm
area: routing
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"skill-call","skill":"azure-diagnostics"},{"type":"tool-call-count","count":3}]'
graders:
- type: skill-invocation
Expand All @@ -104,8 +104,8 @@ stimuli:
tier: full
cost: llm
area: routing
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"skill-call","skill":"azure-diagnostics"},{"type":"tool-call-count","count":3}]'
graders:
- type: skill-invocation
Expand All @@ -128,8 +128,8 @@ stimuli:
tier: full
cost: llm
area: routing
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"skill-call","skill":"azure-diagnostics"},{"type":"tool-call-count","count":3}]'
graders:
- type: skill-invocation
Expand All @@ -143,6 +143,42 @@ stimuli:
config:
pattern: "(?i)fatal error|unhandled exception|stack trace"

# ── inspektor-gadget-run-ig-script ──
# Exercises the run-ig helper script for Inspektor Gadget without a live
# cluster. earlyTerminate uses tool-call-result so the run stops once the
# run-ig invocation completes: with no kubectl/cluster in CI the underlying
# `kubectl debug` fails instantly and harmlessly, and the completed tool call
# is fully recorded for the grader to confirm the script was invoked.
# NOTE: early-terminated runs must NOT use the `completed` grader.
- name: "Inspektor Gadget run-ig script invocation"
prompt: "I'm troubleshooting DNS failures on AKS node aks-nodepool1-12345678-vmss000000. Use the azure-diagnostics Inspektor Gadget helper script to run a trace_dns gadget on that node."
config:
runs: 1
tags:
type: integration
tier: full
cost: llm
area: behavior
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"tool-call-result","toolPattern":"bash|powershell|pwsh","argsPattern":"run-ig\\.(sh|ps1)"}]'
graders:
- type: skill-invocation
config:
required:
- azure-diagnostics
# The agent actually invoked the run-ig helper script.
- type: tool-calls
config:
required:
# Copilot CLI uses "powershell" on Windows, "bash" on other platforms.
- name: "(?i)^(bash|powershell|pwsh)$"
command: "(?i)run-ig\\.(sh|ps1)"
# Global: no_runtime_failure
- type: output-not-matches
config:
pattern: "(?i)fatal error|unhandled exception|stack trace"

# ═══════════════════════════════════════════
# Script execution tests
# ═══════════════════════════════════════════
Expand Down Expand Up @@ -172,8 +208,8 @@ stimuli:
tier: full
cost: llm
area: response-quality
requiredSkills:
- azure-diagnostics
requiredSkills:
- azure-diagnostics
earlyTerminate: '[{"type":"tool-call-result","toolPattern":"bash|powershell|pwsh|run_in_terminal","argsPattern":"aks-baseline\\.(ps1|sh)"}]'
graders:
- type: skill-invocation
Expand Down
184 changes: 184 additions & 0 deletions plugins/azure-skills/skills/azure-diagnostics/scripts/run-ig.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<#
.SYNOPSIS
Runs an Inspektor Gadget (IG) trace on an AKS node via `kubectl debug`.

.DESCRIPTION
Handles the mechanical, error-prone assembly of the IG invocation:
- resolves the target node from a pod (or takes a node directly)
- injects the pinned IG image + version
- applies the correct default -Timeout for the gadget type
- adds the k8s namespace/pod/container filters
- handles the special `tcpdump` gadget (pcap-ng output piped to tcpdump)

The privileged debug pod requires explicit user approval and appropriate RBAC.
Use -DryRun to print the assembled command without running it.

.PARAMETER Gadget
Gadget to run, e.g. trace_dns, snapshot_socket, tcpdump (required).

.PARAMETER Pod
Pod name; the node is resolved automatically.

.PARAMETER Namespace
Namespace of the pod (required with -Pod).

.PARAMETER Node
Run directly against a node (node-wide scope).

.PARAMETER Container
Scope to a specific container.

.PARAMETER Timeout
Override the gadget-type default timeout (seconds).

.PARAMETER Filter
Extra IG flags, passed through verbatim (e.g. -Filter --max-entries,20).

.PARAMETER Pf
tcpdump packet filter (tcpdump gadget only, e.g. "port 80").

.PARAMETER IgVersion
Override the pinned IG image tag.

.PARAMETER DryRun
Print the assembled command; do not execute.

.EXAMPLE
./run-ig.ps1 -Gadget trace_dns -Pod web-0 -Namespace default

.EXAMPLE
./run-ig.ps1 -Gadget snapshot_process -Node aks-nodepool1-1234

.EXAMPLE
./run-ig.ps1 -Gadget tcpdump -Pod web-0 -Namespace default -Pf "port 80"

.EXAMPLE
./run-ig.ps1 -Gadget traceloop -Pod web-0 -Namespace default -Filter --syscall-filters,open,connect

.EXAMPLE
./run-ig.ps1 -Gadget trace_dns -Pod web-0 -Namespace default -DryRun
#>
[CmdletBinding()]
param(
[string]$Gadget,
[string]$Pod,
[Alias('Ns')]
[string]$Namespace,
[string]$Node,
[string]$Container,
[int]$Timeout,
[string[]]$Filter,
[string]$Pf,
# Pinned IG image tag. Bump this default (and run-ig.sh) to update the IG version.
[string]$IgVersion = 'v0.51.0',
[switch]$DryRun
)

$IgImageRepo = 'mcr.microsoft.com/oss/v2/inspektor-gadget/ig'

if (-not $Gadget) {
Write-Error 'Provide -Gadget <name> (e.g. trace_dns, snapshot_socket, tcpdump).'
exit 2
}
if (-not $Node -and -not $Pod) {
Write-Error 'Provide either -Node <node> or -Pod <pod> -Namespace <namespace>.'
exit 2
}
if ($Pod -and -not $Namespace) {
Write-Error '-Pod requires -Namespace <namespace>.'
exit 2
}
if ($Pf -and $Gadget -ne 'tcpdump') {
Write-Error '-Pf is only valid for the tcpdump gadget.'
exit 2
}

# Default timeout by gadget type, inferred from the gadget name prefix.
# snapshot_* / top_* -> 5s (point-in-time / quick aggregate)
# trace_* / profile_* / tcpdump -> 30s (streaming / sampling)
function Get-DefaultTimeout([string]$g) {
switch -Wildcard ($g) {
'snapshot_*' { return 5 }
'top_*' { return 5 }
'trace_*' { return 30 }
'profile_*' { return 30 }
'tcpdump' { return 30 }
default { return 30 } # unknown gadget: use the safer streaming default
}
}

if (-not $PSBoundParameters.ContainsKey('Timeout') -or $Timeout -le 0) {
$Timeout = Get-DefaultTimeout $Gadget
}

# Resolve the node name from the pod when not given directly.
if (-not $Node) {
$Node = ((& kubectl get pod $Pod -n $Namespace -o "jsonpath={.spec.nodeName}" 2>$null) | Out-String).Trim()
if (-not $Node) {
Write-Error "Could not resolve node for pod '$Pod' in namespace '$Namespace'."
exit 1
}
}

$IgImage = "${IgImageRepo}:${IgVersion}"

# Assemble the k8s scoping filters.
$filters = @()
if ($Namespace) { $filters += @('--k8s-namespace', $Namespace) }
if ($Pod) { $filters += @('--k8s-podname', $Pod) }
if ($Container) { $filters += @('--k8s-containername', $Container) }

# Base kubectl debug invocation.
$debug = @('debug', '--profile=sysadmin', "node/$Node", '--attach', '--quiet', "--image=$IgImage", '--')

if ($Gadget -eq 'tcpdump') {
# tcpdump emits raw pcap-ng; pipe through tcpdump for readable output when available.
$igCmd = @('ig', 'run', "tcpdump:$IgVersion", '-o', 'pcap-ng') + $filters + @('--timeout', "$Timeout")
if ($Pf) { $igCmd += @('--pf', $Pf) }
if ($Filter) { $igCmd += $Filter }
}
else {
$igCmd = @('ig', 'run', "${Gadget}:$IgVersion", '-o', 'json') + $filters + @('--timeout', "$Timeout")
if ($Filter) { $igCmd += $Filter }
}

$fullArgs = $debug + $igCmd

# Pretty-print a shell-quoted version of the command for display.
function Format-Cmd([string[]]$parts) {
($parts | ForEach-Object {
if ($_ -match '\s') { '"' + $_ + '"' } else { $_ }
}) -join ' '
}

$displayCmd = 'kubectl ' + (Format-Cmd $fullArgs)

# The tcpdump gadget is only piped through `tcpdump` when that binary is present.
# Reflect the real behavior in the displayed command so -DryRun does not mislead.
$tcpdumpAvail = $Gadget -eq 'tcpdump' -and [bool](Get-Command tcpdump -ErrorAction SilentlyContinue)
if ($tcpdumpAvail) {
$displayCmd = "$displayCmd | tcpdump -nvr -"
}

Write-Host "Gadget: $Gadget"
Write-Host "Node: $Node"
Write-Host "Timeout: ${Timeout}s"
Write-Host "Image: $IgImage"
Write-Host "Command: $displayCmd"
if ($Gadget -eq 'tcpdump' -and -not $tcpdumpAvail) {
Write-Host 'Note: tcpdump not found; emitting raw pcap-ng to stdout.'
}

if ($DryRun) {
Write-Host '(dry-run: command not executed)'
exit 0
}

Write-Host "Ran gadget $Gadget on node $Node (timeout ${Timeout}s)"

if ($tcpdumpAvail) {
& kubectl @fullArgs | & tcpdump -nvr -
}
else {
& kubectl @fullArgs
}
Loading
Loading