Skip to content
Merged
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
159 changes: 102 additions & 57 deletions plugins/aks-desktop/src/utils/azure/az-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1622,72 +1622,117 @@ export async function getClusterResourceGroupViaGraph(
}
}

/**
* Fetches a single page of AKS clusters from Azure Resource Graph.
*
* The Resource Graph query returns at most 1000 results per call with --first 1000. (Maximum)
* If more results exist, the raw response includes a `skip_token` cursor that can
* be used to fetch the next page of results. This function returns both the clusters and
* a `skipToken` (mapped from the raw `skip_token` field) when pagination is required to
* fetch all clusters in larger subscriptions.
*
* @param query - Azure Resource Graph query to execute.
* @param skipToken - Pagination token from a previous call to fetch the next page.
* @returns The cluster records and an optional `skipToken` for the next page.
*/
async function fetchGraphPage(
query: string,
Comment thread
illume marked this conversation as resolved.
skipToken?: string
): Promise<{ clusters: any[]; skipToken?: string }> {
const pageSize = '1000';
const args = ['graph', 'query', '-q', query, '--first', pageSize, '--output', 'json'];
// Append skip token for pagination if provided
if (skipToken) {
args.push('--skip-token', skipToken);
}

const { stdout, stderr } = await runCommandAsync('az', args);

if (stderr && needsRelogin(stderr)) {
throw new Error('Authentication required. Please log in to Azure CLI: az login');
}

if (stderr && stderr.toLowerCase().includes('error')) {
throw new Error(`Resource Graph query failed: ${stderr}`);
}

Comment thread
tejhan marked this conversation as resolved.
try {
const result = JSON.parse(stdout);
const clusters = result.data || [];

return { clusters, skipToken: result.skip_token };
} catch (parseError: unknown) {
const parseErrorMessage = parseError instanceof Error ? parseError.message : String(parseError);
const stdoutPreview = stdout.length > 500 ? stdout.slice(0, 500) + '…' : stdout;
throw new Error(
`Failed to parse Resource Graph query response: ${parseErrorMessage}. ` +
`Stdout length=${stdout.length}, preview=${JSON.stringify(stdoutPreview)}`
);
}
}

// Get clusters using Azure Resource Graph
export async function getClustersViaGraph(
subscriptionId: string,
filterAad: boolean = false
): Promise<any[]> {
try {
if (!isValidGuid(subscriptionId)) {
throw new Error('Invalid subscription ID format');
}

const aadFilter = filterAad ? '| where isnotnull(properties.aadProfile)' : '';

const query = `
Resources
| where type =~ 'microsoft.containerservice/managedclusters'
| where subscriptionId == '${subscriptionId}'
${aadFilter}
| extend nodeCount = array_length(properties.agentPoolProfiles)
| project
if (!isValidGuid(subscriptionId)) {
throw new Error('Invalid subscription ID format');
}

const aadFilter = filterAad ? '| where isnotnull(properties.aadProfile)' : '';

const query = `
Resources
| where type =~ 'microsoft.containerservice/managedclusters'
Comment thread
illume marked this conversation as resolved.
| where subscriptionId == '${subscriptionId}'
${aadFilter}
| extend agentPools = properties.agentPoolProfiles
| mv-expand agentPools
| extend poolNodeCount = toint(agentPools['count'])
Comment on lines +1691 to +1692
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The query uses mv-expand agentPools (inner expand), which will drop clusters where properties.agentPoolProfiles is null/empty (e.g., partially-provisioned or failed clusters). In that case the Resource Graph path would silently omit clusters that the az aks list fallback would still return (with nodeCount=0). Consider using mv-expand kind=outer agentPools (or otherwise guarding/coalescing) so clusters with no pools remain in the result set.

Suggested change
| mv-expand agentPools
| extend poolNodeCount = toint(agentPools['count'])
| mv-expand kind=outer agentPools
| extend poolNodeCount = toint(coalesce(agentPools['count'], 0))

Copilot uses AI. Check for mistakes.
| summarize
nodeCount = sum(poolNodeCount)
by
name,
resourceGroup,
location,
version = properties.kubernetesVersion,
status = properties.provisioningState,
powerState = properties.powerState.code,
nodeCount
| order by name asc
`;

const { stdout, stderr } = await runCommandAsync('az', [
'graph',
'query',
'-q',
query,
'--output',
'json',
]);

if (stderr && needsRelogin(stderr)) {
throw new Error('Authentication required. Please log in to Azure CLI: az login');
}

if (stderr && (stderr.includes('ERROR') || stderr.includes('error'))) {
throw new Error(`Resource Graph query failed: ${stderr}`);
}

try {
const result = JSON.parse(stdout);
const clusters = result.data || [];

return clusters.map((cluster: any) => ({
name: cluster.name,
subscription: subscriptionId,
resourceGroup: cluster.resourceGroup,
location: cluster.location,
version: cluster.version,
status: cluster.status,
powerState: cluster.powerState || 'Unknown',
nodeCount: cluster.nodeCount || 0,
}));
} catch (parseError) {
throw new Error(`Failed to parse Resource Graph response: ${parseError}`);
}
} catch (error) {
throw error;
version = tostring(properties.kubernetesVersion),
status = tostring(properties.provisioningState),
powerState = tostring(properties.powerState.code)
| order by name asc
`;

// Fetch first page
let page = await fetchGraphPage(query);
const allClusters = [...page.clusters];

// Fetch remaining pages if the subscription has more clusters than one page holds.
// The Resource Graph response includes a `skipToken` only when more pages exist;
// on the final page it is null/absent, which will terminate the loop.
const MAX_PAGES = 100; // 100,000 cluster limit.
let pageCount = 1;
while (page.skipToken && pageCount < MAX_PAGES) {
Comment thread
illume marked this conversation as resolved.
page = await fetchGraphPage(query, page.skipToken);
allClusters.push(...page.clusters);
Comment thread
tejhan marked this conversation as resolved.
pageCount++;
}

Comment thread
tejhan marked this conversation as resolved.
if (page.skipToken && pageCount >= MAX_PAGES) {
debugLog(
`Resource Graph pagination hit MAX_PAGES limit (${MAX_PAGES}). Results may be truncated.`
);
}

return allClusters.map((cluster: any) => ({
name: cluster.name,
subscription: subscriptionId,
resourceGroup: cluster.resourceGroup,
location: cluster.location,
version: cluster.version,
status: cluster.status,
powerState: cluster.powerState || 'Unknown',
nodeCount: cluster.nodeCount || 0,
}));
}

// Get total cluster count for a subscription using Azure Resource Graph
Expand Down
Loading