From 40fbd95066b5d4ed79fd3e80ae9c50c3ef6012b8 Mon Sep 17 00:00:00 2001 From: Sunil Yadav Date: Mon, 14 Sep 2026 02:14:56 +0000 Subject: [PATCH 1/2] CI OTLP GA --- report.20260913.220253.791060.0.001.json | 948 ++++++++++++++ report.20260913.222148.802059.0.001.json | 586 +++++++++ report.20260914.015459.849235.0.001.json | 586 +++++++++ src/azure-cli/HISTORY.rst | 7 + .../azure/cli/command_modules/acs/_consts.py | 7 + .../azure/cli/command_modules/acs/_help.py | 114 ++ .../azure/cli/command_modules/acs/_params.py | 89 +- .../cli/command_modules/acs/_validators.py | 212 ++++ .../command_modules/acs/addonconfiguration.py | 21 + .../azure/cli/command_modules/acs/custom.py | 36 + .../acs/managed_cluster_decorator.py | 1121 +++++++++++++++-- .../acs/tests/latest/test_aks_commands.py | 6 +- .../acs/tests/latest/test_custom.py | 37 + .../latest/test_managed_cluster_decorator.py | 758 ++++++++++- .../acs/tests/latest/test_validators.py | 236 ++++ 15 files changed, 4633 insertions(+), 131 deletions(-) create mode 100644 report.20260913.220253.791060.0.001.json create mode 100644 report.20260913.222148.802059.0.001.json create mode 100644 report.20260914.015459.849235.0.001.json diff --git a/report.20260913.220253.791060.0.001.json b/report.20260913.220253.791060.0.001.json new file mode 100644 index 00000000000..5305a05dadb --- /dev/null +++ b/report.20260913.220253.791060.0.001.json @@ -0,0 +1,948 @@ + +{ + "header": { + "reportVersion": 5, + "event": "Allocation failed - JavaScript heap out of memory", + "trigger": "OOMError", + "filename": "report.20260913.220253.791060.0.001.json", + "dumpEventTime": "2026-09-13T22:02:53Z", + "dumpEventTimeStamp": "1789336973149", + "processId": 791060, + "threadId": 0, + "cwd": "/home/sky/work/git/azure-cli", + "commandLine": [ + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-warnings", + "--report-on-fatalerror", + "--optimize-for-size", + "--expose-gc", + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-auto-update", + "--log-dir", + "/home/sky/.local/agency/logs/session_20260913_213103_789937", + "-C", + "/home/sky/work/git/azure-cli", + "--session-id", + "589c7996-065c-4b56-a1eb-920fda782615", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-Z8PWKQWCOcUtRSYo.p789937/.agency-builtin-93rzW2/agency", + "--plugin-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-Z8PWKQWCOcUtRSYo.p789937/.agency-builtin-93rzW2/agency", + "--add-dir", + "/home/sky/work/git/azure-cli", + "--add-dir", + "/home/sky/.config/agency/2026.9.10.9/source/content", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-Z8PWKQWCOcUtRSYo.p789937", + "--additional-mcp-config", + "@/tmp/copilot-mcp-58PMi9.json" + ], + "nodejsVersion": "v24.20.0", + "glibcVersionRuntime": "2.39", + "glibcVersionCompiler": "2.28", + "wordSize": 64, + "arch": "x64", + "platform": "linux", + "componentVersions": { + "acorn": "8.18.0", + "ada": "4.0.0", + "amaro": "1.1.11", + "ares": "1.34.8", + "brotli": "1.2.0", + "cldr": "48.0", + "icu": "78.3", + "llhttp": "9.4.3", + "merve": "1.2.2", + "modules": "137", + "napi": "10", + "nbytes": "0.1.4", + "ncrypto": "0.0.1", + "nghttp2": "1.70.0", + "nghttp3": "", + "ngtcp2": "", + "node": "24.20.0", + "openssl": "3.5.7", + "simdjson": "4.6.6", + "simdutf": "6.4.0", + "sqlite": "3.53.4", + "tz": "2026c", + "undici": "7.29.0", + "unicode": "17.0", + "uv": "1.52.1", + "uvwasi": "0.0.23", + "v8": "13.6.233.17-node.53", + "zlib": "1.3.2.1-motley-42c2f19", + "zstd": "1.5.7" + }, + "release": { + "name": "node", + "lts": "Krypton", + "headersUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0-headers.tar.gz", + "sourceUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0.tar.gz" + }, + "osName": "Linux", + "osRelease": "6.6.87.2-microsoft-standard-WSL2", + "osVersion": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", + "osMachine": "x86_64", + "cpus": [ + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 17243850, + "nice": 220, + "sys": 27300360, + "idle": 226297720, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21071850, + "nice": 1100, + "sys": 22151240, + "idle": 228734130, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18071700, + "nice": 1660, + "sys": 26728080, + "idle": 226589910, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20706880, + "nice": 1220, + "sys": 22202360, + "idle": 229178620, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18396400, + "nice": 3200, + "sys": 26868500, + "idle": 226230760, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20663760, + "nice": 2250, + "sys": 22279740, + "idle": 229361710, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18371080, + "nice": 260, + "sys": 26903890, + "idle": 226216340, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20738280, + "nice": 0, + "sys": 22408720, + "idle": 229171170, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18444480, + "nice": 20, + "sys": 27074160, + "idle": 225949590, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20990340, + "nice": 10, + "sys": 22613190, + "idle": 228716700, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18597920, + "nice": 60, + "sys": 27174150, + "idle": 225669480, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20854740, + "nice": 40, + "sys": 22509030, + "idle": 228992130, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18457600, + "nice": 320, + "sys": 27128500, + "idle": 225907950, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21165180, + "nice": 860, + "sys": 22593420, + "idle": 228593830, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18569100, + "nice": 380, + "sys": 27167510, + "idle": 225748790, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21013700, + "nice": 4180, + "sys": 22444420, + "idle": 228936430, + "irq": 0 + } + ], + "networkInterfaces": [ + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "127.0.0.1", + "netmask": "255.0.0.0", + "family": "IPv4" + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "172.26.10.138", + "netmask": "255.255.240.0", + "family": "IPv4" + }, + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "::1", + "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "family": "IPv6", + "scopeid": 0 + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "fe80::215:5dff:fe63:ea9a", + "netmask": "ffff:ffff:ffff:ffff::", + "family": "IPv6", + "scopeid": 2 + } + ], + "host": "CPC-suyad-CLQQJ" + }, + "javascriptStack": { + "message": "No stack.", + "stack": [ + "Unavailable." + ], + "errorProperties": { + } + }, + "javascriptHeap": { + "totalMemory": 4294688768, + "executableMemory": 10661888, + "totalCommittedMemory": 3987521536, + "availableMemory": 14339936, + "totalGlobalHandlesMemory": 118856, + "usedGlobalHandlesMemory": 89856, + "usedMemory": 3918844656, + "memoryLimit": 4298113024, + "mallocedMemory": 1073224, + "externalMemory": 36616667, + "peakMallocedMemory": 64922936, + "nativeContextCount": 2, + "detachedContextCount": 0, + "doesZapGarbage": 0, + "heapSpaces": { + "read_only_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_space": { + "memorySize": 2097152, + "committedMemory": 2097152, + "capacity": 1048512, + "used": 0, + "available": 1048512 + }, + "old_space": { + "memorySize": 4272324608, + "committedMemory": 3965419520, + "capacity": 3909473144, + "used": 3900654552, + "available": 8818592 + }, + "code_space": { + "memorySize": 9175040, + "committedMemory": 9019392, + "capacity": 8244352, + "used": 8244352, + "available": 0 + }, + "shared_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_space": { + "memorySize": 7991296, + "committedMemory": 7884800, + "capacity": 6864584, + "used": 6864584, + "available": 0 + }, + "shared_trusted_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 1048576, + "used": 0, + "available": 1048576 + }, + "large_object_space": { + "memorySize": 1212416, + "committedMemory": 1212416, + "capacity": 1199616, + "used": 1199616, + "available": 0 + }, + "code_large_object_space": { + "memorySize": 1486848, + "committedMemory": 1486848, + "capacity": 1483328, + "used": 1483328, + "available": 0 + }, + "shared_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "shared_trusted_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_large_object_space": { + "memorySize": 401408, + "committedMemory": 401408, + "capacity": 398224, + "used": 398224, + "available": 0 + } + } + }, + "nativeStack": [ + { + "pc": "0x00000000009fd934", + "symbol": "node::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000009fded7", + "symbol": "node::TriggerNodeReport(v8::Isolate*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x000000000075760c", + "symbol": "node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60e30", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60f1f", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f047f5", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04822", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04b1a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f1581a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f19bc0", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000019b78a1", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + } + ], + "resourceUsage": { + "free_memory": 17596624896, + "total_memory": 33628012544, + "rss": 5382168576, + "constrained_memory": 18446744073709551615, + "available_memory": 17596624896, + "userCpuSeconds": 1349.28, + "kernelCpuSeconds": 248.14, + "cpuConsumptionPercent": 84.7439, + "userCpuConsumptionPercent": 71.5799, + "kernelCpuConsumptionPercent": 13.1639, + "maxRss": 5477617664, + "pageFaults": { + "IORequired": 14, + "IONotRequired": 10503854 + }, + "fsActivity": { + "reads": 6184, + "writes": 66184 + } + }, + "uvthreadResourceUsage": { + "userCpuSeconds": 518.991, + "kernelCpuSeconds": 95.5223, + "cpuConsumptionPercent": 32.6002, + "userCpuConsumptionPercent": 27.5327, + "kernelCpuConsumptionPercent": 5.06749, + "fsActivity": { + "reads": 128, + "writes": 16 + } + }, + "libuv": [ + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bca9450" + }, + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x0000000006b64b60" + }, + { + "type": "timer", + "is_active": true, + "is_referenced": true, + "address": "0x000000004bd2da68", + "repeat": 0, + "firesInMsFromNow": 69, + "expired": false + }, + { + "type": "check", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd2db00" + }, + { + "type": "idle", + "is_active": false, + "is_referenced": true, + "address": "0x000000004bd2db78" + }, + { + "type": "prepare", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd2dbf0" + }, + { + "type": "check", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd2dc68" + }, + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd2dce0" + }, + { + "type": "tty", + "is_active": false, + "is_referenced": true, + "address": "0x000000004bd8e240", + "width": 156, + "height": 50, + "fd": 20, + "writeQueueSize": 0, + "readable": true, + "writable": true + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd5acc8", + "signum": 28, + "signal": "SIGWINCH" + }, + { + "type": "tty", + "is_active": true, + "is_referenced": true, + "address": "0x000000004be882c0", + "width": 156, + "height": 50, + "fd": 22, + "writeQueueSize": 0, + "readable": true, + "writable": true + }, + { + "type": "tty", + "is_active": false, + "is_referenced": true, + "address": "0x000000004c16acd0", + "width": 156, + "height": 50, + "fd": 23, + "writeQueueSize": 0, + "readable": true, + "writable": true + }, + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bd5dad8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x0000727518000e48" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c339508", + "signum": 15, + "signal": "SIGTERM" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c3493c8", + "signum": 2, + "signal": "SIGINT" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c348b68", + "signum": 1, + "signal": "SIGHUP" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004bff95f8", + "signum": 3, + "signal": "SIGQUIT" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c45cc08" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c488f08", + "signum": 18, + "signal": "SIGCONT" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c486e98", + "signum": 14, + "signal": "SIGALRM" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c487278", + "signum": 6, + "signal": "SIGABRT" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c487388", + "signum": 26, + "signal": "SIGVTALRM" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c487528", + "signum": 24, + "signal": "SIGXCPU" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485508", + "signum": 25, + "signal": "SIGXFSZ" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485658", + "signum": 12, + "signal": "SIGUSR2" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c4857a8", + "signum": 5, + "signal": "SIGTRAP" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c4858f8", + "signum": 31, + "signal": "SIGSYS" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485a48", + "signum": 6, + "signal": "SIGABRT" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485b58", + "signum": 29, + "signal": "SIGIO" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485ca8", + "signum": 29, + "signal": "SIGIO" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485db8", + "signum": 30, + "signal": "SIGPWR" + }, + { + "type": "signal", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c485f08", + "signum": 16, + "signal": "SIGSTKFLT" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c49f0f8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c5072c8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c4f0ea8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c4fc588" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c560098" + }, + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c4be868" + }, + { + "type": "async", + "is_active": true, + "is_referenced": false, + "address": "0x000000004c6a60e8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c533c38" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c4b9488" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c57aea8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c552a88" + }, + { + "type": "pipe", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c5be540", + "localEndpoint": null, + "remoteEndpoint": "/tmp/mcp-lonCdX/mcp.sock", + "sendBufferSize": 212992, + "recvBufferSize": 212992, + "fd": 46, + "writeQueueSize": 0, + "readable": true, + "writable": true + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004be2f2c8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x00007274c4160198" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c50d4e8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c5720a8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c68b018" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c5a7ea8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c5f7fc8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004da9f918" + }, + { + "type": "timer", + "is_active": true, + "is_referenced": false, + "address": "0x000000004e1d36a0", + "repeat": 0, + "firesInMsFromNow": 5891, + "expired": false + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004c427828" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x000000004d279e78" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x0000000050aef6b8" + }, + { + "type": "async", + "is_active": true, + "is_referenced": true, + "address": "0x0000000050ab0d18" + }, + { + "type": "loop", + "is_active": true, + "address": "0x0000000006b77aa0", + "loopIdleTimeSeconds": 1246.88 + } + ], + "workers": [ + ], + "userLimits": { + "core_file_size_blocks": { + "soft": 0, + "hard": "unlimited" + }, + "data_seg_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "file_size_blocks": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_locked_memory_bytes": { + "soft": 67108864, + "hard": 67108864 + }, + "max_memory_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "open_files": { + "soft": 1048576, + "hard": 1048576 + }, + "stack_size_bytes": { + "soft": 8388608, + "hard": "unlimited" + }, + "cpu_time_seconds": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_user_processes": { + "soft": 128241, + "hard": 128241 + }, + "virtual_memory_bytes": { + "soft": "unlimited", + "hard": "unlimited" + } + }, + "sharedObjects": [ + "linux-vdso.so.1", + "/lib/x86_64-linux-gnu/libdl.so.2", + "/lib/x86_64-linux-gnu/libstdc++.so.6", + "/lib/x86_64-linux-gnu/libm.so.6", + "/lib/x86_64-linux-gnu/libgcc_s.so.1", + "/lib/x86_64-linux-gnu/libpthread.so.0", + "/lib/x86_64-linux-gnu/libc.so.6", + "/lib64/ld-linux-x86-64.so.2", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/runtime.node", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/cli-native.node" + ] +} diff --git a/report.20260913.222148.802059.0.001.json b/report.20260913.222148.802059.0.001.json new file mode 100644 index 00000000000..e09d0565b32 --- /dev/null +++ b/report.20260913.222148.802059.0.001.json @@ -0,0 +1,586 @@ + +{ + "header": { + "reportVersion": 5, + "event": "Allocation failed - JavaScript heap out of memory", + "trigger": "OOMError", + "filename": "report.20260913.222148.802059.0.001.json", + "dumpEventTime": "2026-09-13T22:21:48Z", + "dumpEventTimeStamp": "1789338108171", + "processId": 802059, + "threadId": null, + "cwd": "/home/sky/work/git/azure-cli", + "commandLine": [ + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-warnings", + "--report-on-fatalerror", + "--optimize-for-size", + "--expose-gc", + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-auto-update", + "--log-dir", + "/home/sky/.local/agency/logs/session_20260913_220718_801887", + "-C", + "/home/sky/work/git/azure-cli", + "--session-id", + "971a6209-433e-4a8f-9f9c-acd6bea4d99f", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-gmS6kE9jjlTkmDMf.p801887/.agency-builtin-pdYVfO/agency", + "--plugin-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-gmS6kE9jjlTkmDMf.p801887/.agency-builtin-pdYVfO/agency", + "--add-dir", + "/home/sky/work/git/azure-cli", + "--add-dir", + "/home/sky/.config/agency/2026.9.10.9/source/content", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-gmS6kE9jjlTkmDMf.p801887", + "--additional-mcp-config", + "@/tmp/copilot-mcp-37TBhq.json" + ], + "nodejsVersion": "v24.20.0", + "glibcVersionRuntime": "2.39", + "glibcVersionCompiler": "2.28", + "wordSize": 64, + "arch": "x64", + "platform": "linux", + "componentVersions": { + "acorn": "8.18.0", + "ada": "4.0.0", + "amaro": "1.1.11", + "ares": "1.34.8", + "brotli": "1.2.0", + "cldr": "48.0", + "icu": "78.3", + "llhttp": "9.4.3", + "merve": "1.2.2", + "modules": "137", + "napi": "10", + "nbytes": "0.1.4", + "ncrypto": "0.0.1", + "nghttp2": "1.70.0", + "nghttp3": "", + "ngtcp2": "", + "node": "24.20.0", + "openssl": "3.5.7", + "simdjson": "4.6.6", + "simdutf": "6.4.0", + "sqlite": "3.53.4", + "tz": "2026c", + "undici": "7.29.0", + "unicode": "17.0", + "uv": "1.52.1", + "uvwasi": "0.0.23", + "v8": "13.6.233.17-node.53", + "zlib": "1.3.2.1-motley-42c2f19", + "zstd": "1.5.7" + }, + "release": { + "name": "node", + "lts": "Krypton", + "headersUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0-headers.tar.gz", + "sourceUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0.tar.gz" + }, + "osName": "Linux", + "osRelease": "6.6.87.2-microsoft-standard-WSL2", + "osVersion": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", + "osMachine": "x86_64", + "cpus": [ + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 17351950, + "nice": 220, + "sys": 27391200, + "idle": 227188560, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21204910, + "nice": 1100, + "sys": 22226300, + "idle": 229638940, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18191440, + "nice": 1660, + "sys": 26816780, + "idle": 227492640, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20828320, + "nice": 1220, + "sys": 22275340, + "idle": 230099340, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18519090, + "nice": 3200, + "sys": 26954770, + "idle": 227132270, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20787410, + "nice": 2250, + "sys": 22353110, + "idle": 230280330, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18492840, + "nice": 260, + "sys": 26992030, + "idle": 227118270, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20871570, + "nice": 0, + "sys": 22482250, + "idle": 230079680, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18570440, + "nice": 20, + "sys": 27159900, + "idle": 226848950, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21129680, + "nice": 10, + "sys": 22688000, + "idle": 229618730, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18728450, + "nice": 60, + "sys": 27262240, + "idle": 226562460, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 20992450, + "nice": 40, + "sys": 22582370, + "idle": 229897210, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18587510, + "nice": 320, + "sys": 27215160, + "idle": 226801880, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21290960, + "nice": 860, + "sys": 22666460, + "idle": 229511360, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18700700, + "nice": 380, + "sys": 27254510, + "idle": 226640780, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21151170, + "nice": 4180, + "sys": 22517660, + "idle": 229842920, + "irq": 0 + } + ], + "networkInterfaces": [ + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "127.0.0.1", + "netmask": "255.0.0.0", + "family": "IPv4" + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "172.26.10.138", + "netmask": "255.255.240.0", + "family": "IPv4" + }, + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "::1", + "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "family": "IPv6", + "scopeid": 0 + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "fe80::215:5dff:fe63:ea9a", + "netmask": "ffff:ffff:ffff:ffff::", + "family": "IPv6", + "scopeid": 2 + } + ], + "host": "CPC-suyad-CLQQJ" + }, + "javascriptStack": { + "message": "No stack.", + "stack": [ + "Unavailable." + ], + "errorProperties": { + } + }, + "javascriptHeap": { + "totalMemory": 4297310208, + "executableMemory": 10661888, + "totalCommittedMemory": 4045316096, + "availableMemory": 3092400, + "totalGlobalHandlesMemory": 139264, + "usedGlobalHandlesMemory": 90880, + "usedMemory": 3898508768, + "memoryLimit": 4298113024, + "mallocedMemory": 1130600, + "externalMemory": 36570636, + "peakMallocedMemory": 1154359888, + "nativeContextCount": 1, + "detachedContextCount": 0, + "doesZapGarbage": 0, + "heapSpaces": { + "read_only_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_space": { + "memorySize": 2097152, + "committedMemory": 2097152, + "capacity": 1048512, + "used": 0, + "available": 1048512 + }, + "old_space": { + "memorySize": 4274946048, + "committedMemory": 4022951936, + "capacity": 3880415048, + "used": 3880222552, + "available": 192496 + }, + "code_space": { + "memorySize": 9175040, + "committedMemory": 9175040, + "capacity": 8293312, + "used": 8293312, + "available": 0 + }, + "shared_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_space": { + "memorySize": 7991296, + "committedMemory": 7991296, + "capacity": 6911736, + "used": 6911736, + "available": 0 + }, + "shared_trusted_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 1048576, + "used": 0, + "available": 1048576 + }, + "large_object_space": { + "memorySize": 1212416, + "committedMemory": 1212416, + "capacity": 1199616, + "used": 1199616, + "available": 0 + }, + "code_large_object_space": { + "memorySize": 1486848, + "committedMemory": 1486848, + "capacity": 1483328, + "used": 1483328, + "available": 0 + }, + "shared_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "shared_trusted_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_large_object_space": { + "memorySize": 401408, + "committedMemory": 401408, + "capacity": 398224, + "used": 398224, + "available": 0 + } + } + }, + "nativeStack": [ + { + "pc": "0x00000000009fd934", + "symbol": "node::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000009fded7", + "symbol": "node::TriggerNodeReport(v8::Isolate*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x000000000075760c", + "symbol": "node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60e30", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60f1f", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f047f5", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04822", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04b1a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f1581a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f19bc0", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000019b78a1", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + } + ], + "resourceUsage": { + "free_memory": 16427233280, + "total_memory": 33628012544, + "rss": 6679822336, + "constrained_memory": 18446744073709551615, + "available_memory": 16427233280, + "userCpuSeconds": 1128.16, + "kernelCpuSeconds": 485.323, + "cpuConsumptionPercent": 190.945, + "userCpuConsumptionPercent": 133.51, + "kernelCpuConsumptionPercent": 57.4347, + "maxRss": 6816464896, + "pageFaults": { + "IORequired": 0, + "IONotRequired": 9655436 + }, + "fsActivity": { + "reads": 1960, + "writes": 229272 + } + }, + "uvthreadResourceUsage": { + "userCpuSeconds": 454.429, + "kernelCpuSeconds": 89.9959, + "cpuConsumptionPercent": 64.429, + "userCpuConsumptionPercent": 53.7786, + "kernelCpuConsumptionPercent": 10.6504, + "fsActivity": { + "reads": 1944, + "writes": 24 + } + }, + "libuv": [ + ], + "workers": [ + ], + "environmentVariables": { + "AGENCY_ENGINE": "copilot", + "AGENCY_LOG_SESSION_DIR": "/home/sky/.local/agency/logs/session_20260913_220718_801887", + "AGENCY_OPERATION_ID": "00-c211a59fe6619cecd90583af9dd54ac8-2a7d25e65d4ede7d-00", + "AGENCY_REPO_DIR": "/home/sky/work/git/azure-cli", + "AGENCY_SESSION_ID": "971a6209-433e-4a8f-9f9c-acd6bea4d99f", + "AGENCY_SESSION_SUBPROCESS": "1", + "AGENCY_SOURCE_DIR": "/home/sky/.config/agency/2026.9.10.9/source/content", + "CONTENT_EXCLUSION": "true", + "COPILOT_AGENT_SESSION_ID": "00-c211a59fe6619cecd90583af9dd54ac8-2a7d25e65d4ede7d-00", + "COPILOT_CUSTOM_INSTRUCTIONS_DIRS": "/home/sky/.local/agency/logs/session_20260913_220718_801887/custom_instructions/launch_20260913_220742_801887", + "COPILOT_HOME": "/home/sky/.copilot", + "COPILOT_OTEL_ENABLED": "true", + "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus", + "DISPLAY": ":0", + "ENABLE_AKS_GOPROXY_AAD_AUTH": "true", + "HOME": "/home/sky", + "HOSTTYPE": "x86_64", + "LANG": "C.UTF-8", + "LESSCLOSE": "/usr/bin/lesspipe %s %s", + "LESSOPEN": "| /usr/bin/lesspipe %s", + "LOGNAME": "sky", + "LOG_ANALYTICS_WORKSPACE_RESOURCE_ID": "/subscriptions/26ad903f-2330-429d-8389-864ac35c4350/resourcegroups/defaultresourcegroup-eus2/providers/microsoft.operationalinsights/workspaces/defaultworkspace-26ad903f-2330-429d-8389-864ac35c4350-eus2", + "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:", + "MSFT_AGENCY": "true", + "MSFT_DELEGATE_COMMAND_ARGS": "[\"remote\",\"create\",\"--output\",\"plain\",\"--prompt\",\"{prompt}\"]", + "MSFT_DELEGATE_COMMAND_CMD": "/home/sky/.config/agency/2026.9.10.9/agency", + "MSFT_DELEGATE_COMMAND_NAME": "Agency Hub", + "NAME": "CPC-suyad-CLQQJ", + "NVM_CD_FLAGS": "", + "NVM_DIR": "/home/sky/.nvm", + "OLDPWD": "/home/sky/work/git/aks-rp", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:34525", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/json", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false", + "OTEL_RESOURCE_ATTRIBUTES": "agency.run_kind=user_session,agency.session_id=971a6209-433e-4a8f-9f9c-acd6bea4d99f", + "PATH": "/home/sky/.local/bin:/home/sky/bin:/home/sky/.config/agency/CurrentVersion:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Program Files (x86)/Microsoft SDKs/Azure/CLI2/wbin:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files/Azure Dev CLI:/mnt/c/Program Files/GitHub CLI/:/mnt/c/Users/vmadmin/AppData/Local/Microsoft/WindowsApps:/mnt/c/Program Files/Microsoft SQL Server/Client SDK/ODBC/170/Tools/Binn/:/mnt/c/Program Files/Microsoft SQL Server/150/Tools/Binn/:/mnt/c/Program Files/Microsoft VS Code/bin:/mnt/c/.tools/dotnet:/mnt/c/.tools/.npm-global:/mnt/c/ProgramData/chocolatey/bin:/mnt/c/Program Files (x86)/Microsoft SQL Server/160/DTS/Binn/:/mnt/c/nvm:/mnt/c/Program Files/nodejs:/mnt/c/ES.DevProd/SpringBoard/SBCli/SpringBoardCli:/mnt/c/Program Files/GVFS:/mnt/c/ES.DevProd/VMSetupScripts:/mnt/c/Program Files/nodejs/:/mnt/c/ProgramData/global-npm:/mnt/c/Windows/system32/config/systemprofile/AppData/Local/Microsoft/WindowsApps:/mnt/c/Program Files/Microsoft Dev Box Agent/Scripts:/mnt/c/Program Files/Go/bin:/mnt/c/Program Files (x86)/Windows Kits/10/Windows Performance Toolkit/:/Docker/host/bin:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files/dotnet/:/mnt/c/Program Files/PowerShell/7/:/mnt/c/Users/suyadav/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/suyadav/AppData/Local/Programs/Microsoft VS Code Insiders/bin:/mnt/c/Users/suyadav/go/bin:/mnt/c/Users/suyadav/.dotnet/tools:/mnt/c/Users/suyadav/AppData/Local/Programs/AzureAuth/0.9.5:/snap/bin:/usr/local/go/bin", + "PULSE_SERVER": "unix:/mnt/wslg/PulseServer", + "PWD": "/home/sky/work/git/azure-cli", + "RENDER_SNAPSHOTS": "true", + "SHELL": "/bin/bash", + "SHLVL": "1", + "TERM": "xterm-256color", + "USER": "sky", + "WAYLAND_DISPLAY": "wayland-0", + "WSL2_GUI_APPS_ENABLED": "1", + "WSLENV": "WT_SESSION:WT_PROFILE_ID:", + "WSL_DISTRO_NAME": "Ubuntu", + "WSL_INTEROP": "/run/WSL/801693_interop", + "WT_PROFILE_ID": "{779d64bb-f0e3-5d5d-92b1-7e3970e3791c}", + "WT_SESSION": "eb87bb5b-d87c-45c1-a961-f33b95ba5333", + "XDG_DATA_DIRS": "/usr/local/share:/usr/share:/var/lib/snapd/desktop", + "XDG_RUNTIME_DIR": "/run/user/1000/", + "_": "/home/sky/.config/agency/CurrentVersion/agency", + "COPILOT_CLI_BINARY_VERSION": "1.0.83", + "COPILOT_CLI_RESOLVED_DIST_DIR": "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83", + "COPILOT_LOADER_PID": "802059", + "NODE_ENV": "production", + "COLORTERM": "truecolor" + }, + "userLimits": { + "core_file_size_blocks": { + "soft": 0, + "hard": "unlimited" + }, + "data_seg_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "file_size_blocks": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_locked_memory_bytes": { + "soft": 67108864, + "hard": 67108864 + }, + "max_memory_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "open_files": { + "soft": 1048576, + "hard": 1048576 + }, + "stack_size_bytes": { + "soft": 8388608, + "hard": "unlimited" + }, + "cpu_time_seconds": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_user_processes": { + "soft": 128241, + "hard": 128241 + }, + "virtual_memory_bytes": { + "soft": "unlimited", + "hard": "unlimited" + } + }, + "sharedObjects": [ + "linux-vdso.so.1", + "/lib/x86_64-linux-gnu/libdl.so.2", + "/lib/x86_64-linux-gnu/libstdc++.so.6", + "/lib/x86_64-linux-gnu/libm.so.6", + "/lib/x86_64-linux-gnu/libgcc_s.so.1", + "/lib/x86_64-linux-gnu/libpthread.so.0", + "/lib/x86_64-linux-gnu/libc.so.6", + "/lib64/ld-linux-x86-64.so.2", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/runtime.node", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/cli-native.node" + ] +} diff --git a/report.20260914.015459.849235.0.001.json b/report.20260914.015459.849235.0.001.json new file mode 100644 index 00000000000..725b672ade2 --- /dev/null +++ b/report.20260914.015459.849235.0.001.json @@ -0,0 +1,586 @@ + +{ + "header": { + "reportVersion": 5, + "event": "Allocation failed - JavaScript heap out of memory", + "trigger": "OOMError", + "filename": "report.20260914.015459.849235.0.001.json", + "dumpEventTime": "2026-09-14T01:54:59Z", + "dumpEventTimeStamp": "1789350899096", + "processId": 849235, + "threadId": null, + "cwd": "/home/sky/work/git/azure-cli", + "commandLine": [ + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-warnings", + "--report-on-fatalerror", + "--optimize-for-size", + "--expose-gc", + "/home/sky/.copilot-cli/1.0.83/copilot", + "--no-auto-update", + "--log-dir", + "/home/sky/.local/agency/logs/session_20260914_014152_848992", + "-C", + "/home/sky/work/git/azure-cli", + "--session-id", + "7639ced3-37b2-4b20-9d1a-722f84c8dad0", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-JAkJnRDjRulcG12a.p848992/.agency-builtin-IFarZz/agency", + "--plugin-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-JAkJnRDjRulcG12a.p848992/.agency-builtin-IFarZz/agency", + "--add-dir", + "/home/sky/work/git/azure-cli", + "--add-dir", + "/home/sky/.config/agency/2026.9.10.9/source/content", + "--add-dir", + "/home/sky/.local/agency/plugins/sessions/agency-plugin-JAkJnRDjRulcG12a.p848992", + "--additional-mcp-config", + "@/tmp/copilot-mcp-wDfthx.json" + ], + "nodejsVersion": "v24.20.0", + "glibcVersionRuntime": "2.39", + "glibcVersionCompiler": "2.28", + "wordSize": 64, + "arch": "x64", + "platform": "linux", + "componentVersions": { + "acorn": "8.18.0", + "ada": "4.0.0", + "amaro": "1.1.11", + "ares": "1.34.8", + "brotli": "1.2.0", + "cldr": "48.0", + "icu": "78.3", + "llhttp": "9.4.3", + "merve": "1.2.2", + "modules": "137", + "napi": "10", + "nbytes": "0.1.4", + "ncrypto": "0.0.1", + "nghttp2": "1.70.0", + "nghttp3": "", + "ngtcp2": "", + "node": "24.20.0", + "openssl": "3.5.7", + "simdjson": "4.6.6", + "simdutf": "6.4.0", + "sqlite": "3.53.4", + "tz": "2026c", + "undici": "7.29.0", + "unicode": "17.0", + "uv": "1.52.1", + "uvwasi": "0.0.23", + "v8": "13.6.233.17-node.53", + "zlib": "1.3.2.1-motley-42c2f19", + "zstd": "1.5.7" + }, + "release": { + "name": "node", + "lts": "Krypton", + "headersUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0-headers.tar.gz", + "sourceUrl": "https://nodejs.org/download/release/v24.20.0/node-v24.20.0.tar.gz" + }, + "osName": "Linux", + "osRelease": "6.6.87.2-microsoft-standard-WSL2", + "osVersion": "#1 SMP PREEMPT_DYNAMIC Thu Jun 5 18:30:46 UTC 2025", + "osMachine": "x86_64", + "cpus": [ + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 17606920, + "nice": 230, + "sys": 27666680, + "idle": 239266040, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21437640, + "nice": 1110, + "sys": 22397640, + "idle": 241935750, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18483260, + "nice": 1670, + "sys": 27077780, + "idle": 239588750, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21066600, + "nice": 1220, + "sys": 22449720, + "idle": 242391140, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18814620, + "nice": 3200, + "sys": 27213420, + "idle": 239229520, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21013750, + "nice": 2250, + "sys": 22532020, + "idle": 242585290, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18788130, + "nice": 290, + "sys": 27249030, + "idle": 239219090, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21100570, + "nice": 60, + "sys": 22653130, + "idle": 242390070, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18866810, + "nice": 20, + "sys": 27419690, + "idle": 238946320, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21361520, + "nice": 10, + "sys": 22862730, + "idle": 241922820, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 19017510, + "nice": 60, + "sys": 27522230, + "idle": 238663810, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21209310, + "nice": 40, + "sys": 22752920, + "idle": 242222030, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 18890280, + "nice": 320, + "sys": 27477150, + "idle": 238890820, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21528730, + "nice": 860, + "sys": 22838850, + "idle": 241812680, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 19006200, + "nice": 380, + "sys": 27516520, + "idle": 238726230, + "irq": 0 + }, + { + "model": "Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz", + "speed": 0, + "user": 21372540, + "nice": 4180, + "sys": 22683980, + "idle": 242166800, + "irq": 0 + } + ], + "networkInterfaces": [ + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "127.0.0.1", + "netmask": "255.0.0.0", + "family": "IPv4" + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "172.26.10.138", + "netmask": "255.255.240.0", + "family": "IPv4" + }, + { + "name": "lo", + "internal": true, + "mac": "00:00:00:00:00:00", + "address": "::1", + "netmask": "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + "family": "IPv6", + "scopeid": 0 + }, + { + "name": "eth0", + "internal": false, + "mac": "00:15:5d:63:ea:9a", + "address": "fe80::215:5dff:fe63:ea9a", + "netmask": "ffff:ffff:ffff:ffff::", + "family": "IPv6", + "scopeid": 2 + } + ], + "host": "CPC-suyad-CLQQJ" + }, + "javascriptStack": { + "message": "No stack.", + "stack": [ + "Unavailable." + ], + "errorProperties": { + } + }, + "javascriptHeap": { + "totalMemory": 4297310208, + "executableMemory": 10399744, + "totalCommittedMemory": 4015153152, + "availableMemory": 3079304, + "totalGlobalHandlesMemory": 122880, + "usedGlobalHandlesMemory": 90720, + "usedMemory": 3893760496, + "memoryLimit": 4298113024, + "mallocedMemory": 1073224, + "externalMemory": 36573134, + "peakMallocedMemory": 1143149728, + "nativeContextCount": 1, + "detachedContextCount": 0, + "doesZapGarbage": 0, + "heapSpaces": { + "read_only_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_space": { + "memorySize": 2097152, + "committedMemory": 2097152, + "capacity": 1048512, + "used": 0, + "available": 1048512 + }, + "old_space": { + "memorySize": 4275470336, + "committedMemory": 3993325568, + "capacity": 3875885280, + "used": 3875705880, + "available": 179400 + }, + "code_space": { + "memorySize": 8912896, + "committedMemory": 8900608, + "capacity": 8183360, + "used": 8183360, + "available": 0 + }, + "shared_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_space": { + "memorySize": 7729152, + "committedMemory": 7729152, + "capacity": 6790088, + "used": 6790088, + "available": 0 + }, + "shared_trusted_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "new_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 1048576, + "used": 0, + "available": 1048576 + }, + "large_object_space": { + "memorySize": 1212416, + "committedMemory": 1212416, + "capacity": 1199616, + "used": 1199616, + "available": 0 + }, + "code_large_object_space": { + "memorySize": 1486848, + "committedMemory": 1486848, + "capacity": 1483328, + "used": 1483328, + "available": 0 + }, + "shared_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "shared_trusted_large_object_space": { + "memorySize": 0, + "committedMemory": 0, + "capacity": 0, + "used": 0, + "available": 0 + }, + "trusted_large_object_space": { + "memorySize": 401408, + "committedMemory": 401408, + "capacity": 398224, + "used": 398224, + "available": 0 + } + } + }, + "nativeStack": [ + { + "pc": "0x00000000009fd934", + "symbol": "node::TriggerNodeReport(v8::Isolate*, node::Environment*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000009fded7", + "symbol": "node::TriggerNodeReport(v8::Isolate*, char const*, char const*, std::__cxx11::basic_string, std::allocator > const&, v8::Local) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x000000000075760c", + "symbol": "node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60e30", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000c60f1f", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f047f5", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04822", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f04b1a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f1581a", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x0000000000f19bc0", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + }, + { + "pc": "0x00000000019b78a1", + "symbol": " [/home/sky/.copilot-cli/1.0.83/copilot]" + } + ], + "resourceUsage": { + "free_memory": 16851529728, + "total_memory": 33628012544, + "rss": 6370729984, + "constrained_memory": 18446744073709551615, + "available_memory": 16851529728, + "userCpuSeconds": 1133.2, + "kernelCpuSeconds": 522.151, + "cpuConsumptionPercent": 217.523, + "userCpuConsumptionPercent": 148.909, + "kernelCpuConsumptionPercent": 68.6137, + "maxRss": 6387707904, + "pageFaults": { + "IORequired": 1, + "IONotRequired": 9998193 + }, + "fsActivity": { + "reads": 24, + "writes": 202352 + } + }, + "uvthreadResourceUsage": { + "userCpuSeconds": 452.644, + "kernelCpuSeconds": 88.3571, + "cpuConsumptionPercent": 71.0908, + "userCpuConsumptionPercent": 59.4801, + "kernelCpuConsumptionPercent": 11.6107, + "fsActivity": { + "reads": 0, + "writes": 24 + } + }, + "libuv": [ + ], + "workers": [ + ], + "environmentVariables": { + "AGENCY_ENGINE": "copilot", + "AGENCY_LOG_SESSION_DIR": "/home/sky/.local/agency/logs/session_20260914_014152_848992", + "AGENCY_OPERATION_ID": "00-2413ef5859bdb60b20d2f607acdf14ad-2a67b1853bee455c-00", + "AGENCY_REPO_DIR": "/home/sky/work/git/azure-cli", + "AGENCY_SESSION_ID": "7639ced3-37b2-4b20-9d1a-722f84c8dad0", + "AGENCY_SESSION_SUBPROCESS": "1", + "AGENCY_SOURCE_DIR": "/home/sky/.config/agency/2026.9.10.9/source/content", + "CONTENT_EXCLUSION": "true", + "COPILOT_AGENT_SESSION_ID": "00-2413ef5859bdb60b20d2f607acdf14ad-2a67b1853bee455c-00", + "COPILOT_CUSTOM_INSTRUCTIONS_DIRS": "/home/sky/.local/agency/logs/session_20260914_014152_848992/custom_instructions/launch_20260914_014217_848992", + "COPILOT_HOME": "/home/sky/.copilot", + "COPILOT_OTEL_ENABLED": "true", + "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus", + "DISPLAY": ":0", + "ENABLE_AKS_GOPROXY_AAD_AUTH": "true", + "HOME": "/home/sky", + "HOSTTYPE": "x86_64", + "LANG": "C.UTF-8", + "LESSCLOSE": "/usr/bin/lesspipe %s %s", + "LESSOPEN": "| /usr/bin/lesspipe %s", + "LOGNAME": "sky", + "LOG_ANALYTICS_WORKSPACE_RESOURCE_ID": "/subscriptions/26ad903f-2330-429d-8389-864ac35c4350/resourcegroups/defaultresourcegroup-eus2/providers/microsoft.operationalinsights/workspaces/defaultworkspace-26ad903f-2330-429d-8389-864ac35c4350-eus2", + "LS_COLORS": "rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:", + "MSFT_AGENCY": "true", + "MSFT_DELEGATE_COMMAND_ARGS": "[\"remote\",\"create\",\"--output\",\"plain\",\"--prompt\",\"{prompt}\"]", + "MSFT_DELEGATE_COMMAND_CMD": "/home/sky/.config/agency/2026.9.10.9/agency", + "MSFT_DELEGATE_COMMAND_NAME": "Agency Hub", + "NAME": "CPC-suyad-CLQQJ", + "NVM_CD_FLAGS": "", + "NVM_DIR": "/home/sky/.nvm", + "OLDPWD": "/home/sky/work/git/aks-rp", + "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:46785", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/json", + "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL": "http/json", + "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT": "false", + "OTEL_RESOURCE_ATTRIBUTES": "agency.run_kind=user_session,agency.session_id=7639ced3-37b2-4b20-9d1a-722f84c8dad0", + "PATH": "/home/sky/.local/bin:/home/sky/bin:/home/sky/.config/agency/CurrentVersion:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/usr/lib/wsl/lib:/mnt/c/Program Files (x86)/Microsoft SDKs/Azure/CLI2/wbin:/mnt/c/Windows/system32:/mnt/c/Windows:/mnt/c/Windows/System32/Wbem:/mnt/c/Windows/System32/WindowsPowerShell/v1.0/:/mnt/c/Windows/System32/OpenSSH/:/mnt/c/Program Files/Azure Dev CLI:/mnt/c/Program Files/GitHub CLI/:/mnt/c/Users/vmadmin/AppData/Local/Microsoft/WindowsApps:/mnt/c/Program Files/Microsoft SQL Server/Client SDK/ODBC/170/Tools/Binn/:/mnt/c/Program Files/Microsoft SQL Server/150/Tools/Binn/:/mnt/c/Program Files/Microsoft VS Code/bin:/mnt/c/.tools/dotnet:/mnt/c/.tools/.npm-global:/mnt/c/ProgramData/chocolatey/bin:/mnt/c/Program Files (x86)/Microsoft SQL Server/160/DTS/Binn/:/mnt/c/nvm:/mnt/c/Program Files/nodejs:/mnt/c/ES.DevProd/SpringBoard/SBCli/SpringBoardCli:/mnt/c/Program Files/GVFS:/mnt/c/ES.DevProd/VMSetupScripts:/mnt/c/Program Files/nodejs/:/mnt/c/ProgramData/global-npm:/mnt/c/Windows/system32/config/systemprofile/AppData/Local/Microsoft/WindowsApps:/mnt/c/Program Files/Microsoft Dev Box Agent/Scripts:/mnt/c/Program Files/Go/bin:/mnt/c/Program Files (x86)/Windows Kits/10/Windows Performance Toolkit/:/Docker/host/bin:/mnt/c/Program Files/Git/cmd:/mnt/c/Program Files/dotnet/:/mnt/c/Program Files/PowerShell/7/:/mnt/c/Users/suyadav/AppData/Local/Microsoft/WindowsApps:/mnt/c/Users/suyadav/AppData/Local/Programs/Microsoft VS Code Insiders/bin:/mnt/c/Users/suyadav/go/bin:/mnt/c/Users/suyadav/.dotnet/tools:/mnt/c/Users/suyadav/AppData/Local/Programs/AzureAuth/0.9.5:/snap/bin:/usr/local/go/bin", + "PULSE_SERVER": "unix:/mnt/wslg/PulseServer", + "PWD": "/home/sky/work/git/azure-cli", + "RENDER_SNAPSHOTS": "true", + "SHELL": "/bin/bash", + "SHLVL": "1", + "TERM": "xterm-256color", + "USER": "sky", + "WAYLAND_DISPLAY": "wayland-0", + "WSL2_GUI_APPS_ENABLED": "1", + "WSLENV": "WT_SESSION:WT_PROFILE_ID:", + "WSL_DISTRO_NAME": "Ubuntu", + "WSL_INTEROP": "/run/WSL/848804_interop", + "WT_PROFILE_ID": "{779d64bb-f0e3-5d5d-92b1-7e3970e3791c}", + "WT_SESSION": "0ceb046a-1591-4443-8f2e-159f86ef48a7", + "XDG_DATA_DIRS": "/usr/local/share:/usr/share:/var/lib/snapd/desktop", + "XDG_RUNTIME_DIR": "/run/user/1000/", + "_": "/home/sky/.config/agency/CurrentVersion/agency", + "COPILOT_CLI_BINARY_VERSION": "1.0.83", + "COPILOT_CLI_RESOLVED_DIST_DIR": "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83", + "COPILOT_LOADER_PID": "849235", + "NODE_ENV": "production", + "COLORTERM": "truecolor" + }, + "userLimits": { + "core_file_size_blocks": { + "soft": 0, + "hard": "unlimited" + }, + "data_seg_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "file_size_blocks": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_locked_memory_bytes": { + "soft": 67108864, + "hard": 67108864 + }, + "max_memory_size_bytes": { + "soft": "unlimited", + "hard": "unlimited" + }, + "open_files": { + "soft": 1048576, + "hard": 1048576 + }, + "stack_size_bytes": { + "soft": 8388608, + "hard": "unlimited" + }, + "cpu_time_seconds": { + "soft": "unlimited", + "hard": "unlimited" + }, + "max_user_processes": { + "soft": 128241, + "hard": 128241 + }, + "virtual_memory_bytes": { + "soft": "unlimited", + "hard": "unlimited" + } + }, + "sharedObjects": [ + "linux-vdso.so.1", + "/lib/x86_64-linux-gnu/libdl.so.2", + "/lib/x86_64-linux-gnu/libstdc++.so.6", + "/lib/x86_64-linux-gnu/libm.so.6", + "/lib/x86_64-linux-gnu/libgcc_s.so.1", + "/lib/x86_64-linux-gnu/libpthread.so.0", + "/lib/x86_64-linux-gnu/libc.so.6", + "/lib64/ld-linux-x86-64.so.2", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/runtime.node", + "/home/sky/.cache/copilot/pkg/linux-x64/1.0.83/prebuilds/linux-x64/cli-native.node" + ] +} diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index c222025df06..22d2a03c0e8 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -24,6 +24,13 @@ Release History * `az aks nodepool rollback`: Show an accurate warning when only the node OS upgrade channel is enabled (#33854) * Implement enable/disable flags for user-defined scheduler configuration (#33934) * `az aks update`: Fix Azure Container Storage configuration detection for lowercase and boolean extension settings (#33938) +* `az aks create`, `az aks update`: Add `--enable-azure-monitor-logs` to onboard Container Insights through the Azure Monitor profile using managed identity authentication +* `az aks update`: Add `--disable-azure-monitor-logs` to offboard Container Insights +* `az aks create`, `az aks update`: Add `--syslog-port`, `--enable-prometheus-metrics-scraping` and `--disable-prometheus-metrics-scraping` to tune the Azure Monitor Container Insights configuration +* `az aks create`, `az aks update`: Add `--enable-opentelemetry-metrics`, `--disable-opentelemetry-metrics`, `--opentelemetry-metrics-port-http` and `--opentelemetry-metrics-port-grpc` for the OpenTelemetry metrics receiver +* `az aks create`, `az aks update`: Add `--enable-opentelemetry-logs-traces`, `--disable-opentelemetry-logs-traces`, `--opentelemetry-logs-traces-port-http` and `--opentelemetry-logs-traces-port-grpc` for the OpenTelemetry logs and traces receiver +* `az aks create`, `az aks update`: Write container network logs to `azureMonitorProfile.containerInsights.containerNetworkLogs` instead of the monitoring addon configuration, and reject `--enable-container-network-logs` on clusters using legacy shared key authentication +* `az aks create`, `az aks update`, `az aks enable-addons`: Deprecate `--enable-msi-auth-for-monitoring` in favor of `--enable-azure-monitor-logs` **App Config** diff --git a/src/azure-cli/azure/cli/command_modules/acs/_consts.py b/src/azure-cli/azure/cli/command_modules/acs/_consts.py index 227790750ee..46e6ca8f2a8 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_consts.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_consts.py @@ -160,6 +160,13 @@ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID = "logAnalyticsWorkspaceResourceID" CONST_MONITORING_USING_AAD_MSI_AUTH = "useAADAuth" +# container network logs (azureMonitorProfile.containerInsights.containerNetworkLogs) +CONST_CONTAINER_NETWORK_LOGS_ENABLED = "Enabled" +CONST_CONTAINER_NETWORK_LOGS_DISABLED = "Disabled" +# legacy omsagent addon config key, superseded by containerNetworkLogs on the Azure Monitor +# profile path. Only read, to keep recognizing clusters onboarded before the switch. +CONST_MONITORING_ENABLE_RETINA_NETWORK_FLAGS = "enableRetinaNetworkFlags" + # virtual node CONST_VIRTUAL_NODE_ADDON_NAME = "aciConnector" CONST_VIRTUAL_NODE_SUBNET_NAME = "SubnetName" diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index 3b34e767a5e..685bfa18a28 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -330,6 +330,21 @@ - name: --enable-high-log-scale-mode type: bool short-summary: Enable High Log Scale Mode for Container Logs. Auto-enabled when --enable-container-network-logs is specified. + - name: --enable-azure-monitor-logs + type: bool + short-summary: Enable Azure Monitor logs (Container Insights) for the cluster using managed identity authentication. + long-summary: | + Configures Container Insights through the cluster's Azure Monitor profile instead of the monitoring addon. + Cannot be combined with "--enable-addons monitoring" or with "--enable-msi-auth-for-monitoring". + - name: --syslog-port + type: int + short-summary: TCP port that the Azure Monitor agent listens on for syslog data. Requires --enable-azure-monitor-logs. + - name: --enable-prometheus-metrics-scraping + type: bool + short-summary: Enable Prometheus metrics scraping by the Azure Monitor agent. Requires --enable-azure-monitor-logs. + - name: --disable-prometheus-metrics-scraping + type: bool + short-summary: Disable Prometheus metrics scraping by the Azure Monitor agent. Requires --enable-azure-monitor-logs. - name: --sku type: string short-summary: Specify SKU name for managed clusters. Use '--sku base' enables a base managed cluster. Use '--sku automatic' enables an automatic managed cluster. @@ -564,6 +579,30 @@ - name: --enable-azure-monitor-app-monitoring type: bool short-summary: Enable Azure Monitor Application Monitoring auto-instrumentation for a Kubernetes cluster. + - name: --enable-opentelemetry-metrics + type: bool + short-summary: Enable the OpenTelemetry (OTLP) metrics receiver. Requires --enable-azure-monitor-metrics. + - name: --disable-opentelemetry-metrics + type: bool + short-summary: Disable the OpenTelemetry (OTLP) metrics receiver. + - name: --opentelemetry-metrics-port-http + type: int + short-summary: HTTP/protobuf port for the OpenTelemetry metrics receiver. + - name: --opentelemetry-metrics-port-grpc + type: int + short-summary: gRPC port for the OpenTelemetry metrics receiver. + - name: --enable-opentelemetry-logs-traces + type: bool + short-summary: Enable the OpenTelemetry (OTLP) logs and traces receiver. Requires --enable-azure-monitor-logs. + - name: --disable-opentelemetry-logs-traces + type: bool + short-summary: Disable the OpenTelemetry (OTLP) logs and traces receiver. + - name: --opentelemetry-logs-traces-port-http + type: int + short-summary: HTTP/protobuf port for the OpenTelemetry logs and traces receiver. + - name: --opentelemetry-logs-traces-port-grpc + type: int + short-summary: gRPC port for the OpenTelemetry logs and traces receiver. - name: --nodepool-taints type: string short-summary: The node taints for all node pool. @@ -752,6 +791,14 @@ text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-keda - name: Create a kubernetes cluster with the Azure Monitor managed service for Prometheus integration enabled. text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-metrics + - name: Create a kubernetes cluster with Azure Monitor logs (Container Insights) enabled. + text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs + - name: Create a kubernetes cluster with Azure Monitor logs enabled and syslog collected on a custom port. + text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs --enable-syslog --syslog-port 28330 + - name: Create a kubernetes cluster with the OpenTelemetry logs and traces receiver enabled. + text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs --enable-opentelemetry-logs-traces --opentelemetry-logs-traces-port-grpc 4317 + - name: Create a kubernetes cluster with the OpenTelemetry metrics receiver enabled. + text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-metrics --enable-opentelemetry-metrics --opentelemetry-metrics-port-grpc 4319 - name: Create a kubernetes cluster with vertical pod autoscaler enaled. text: az aks create -g MyResourceGroup -n MyManagedCluster --enable-vpa - name: create a kubernetes cluster with a Capacity Reservation Group(CRG) ID. @@ -1120,6 +1167,63 @@ - name: --disable-azure-monitor-app-monitoring type: bool short-summary: Disable Azure Monitor Application Monitoring auto-instrumentation for a Kubernetes cluster. + - name: --enable-azure-monitor-logs + type: bool + short-summary: Enable Azure Monitor logs (Container Insights) for the cluster using managed identity authentication. + long-summary: | + Configures Container Insights through the cluster's Azure Monitor profile instead of the monitoring addon. + Clusters still using legacy shared key authentication must first migrate to managed identity authentication. + - name: --disable-azure-monitor-logs + type: bool + short-summary: Disable Azure Monitor logs (Container Insights) for the cluster. + - name: --workspace-resource-id + type: string + short-summary: The resource ID of an existing Log Analytics Workspace to use for storing monitoring data. If not specified, uses the default Log Analytics Workspace if it exists, otherwise creates one. + - name: --enable-msi-auth-for-monitoring + type: bool + short-summary: Enable Managed Identity Auth for Monitoring addon. + - name: --enable-syslog + type: bool + short-summary: Enable syslog data collection for Monitoring addon. + - name: --data-collection-settings + type: string + short-summary: Path to JSON file containing data collection settings for Monitoring addon. + - name: --ampls-resource-id + type: string + short-summary: Resource ID of Azure Monitor Private Link scope for Monitoring Addon. + - name: --syslog-port + type: int + short-summary: TCP port that the Azure Monitor agent listens on for syslog data. Requires Azure Monitor logs to be enabled. + - name: --enable-prometheus-metrics-scraping + type: bool + short-summary: Enable Prometheus metrics scraping by the Azure Monitor agent. Requires Azure Monitor logs to be enabled. + - name: --disable-prometheus-metrics-scraping + type: bool + short-summary: Disable Prometheus metrics scraping by the Azure Monitor agent. Requires Azure Monitor logs to be enabled. + - name: --enable-opentelemetry-metrics + type: bool + short-summary: Enable the OpenTelemetry (OTLP) metrics receiver. Requires Azure Monitor metrics to be enabled. + - name: --disable-opentelemetry-metrics + type: bool + short-summary: Disable the OpenTelemetry (OTLP) metrics receiver. + - name: --opentelemetry-metrics-port-http + type: int + short-summary: HTTP/protobuf port for the OpenTelemetry metrics receiver. + - name: --opentelemetry-metrics-port-grpc + type: int + short-summary: gRPC port for the OpenTelemetry metrics receiver. + - name: --enable-opentelemetry-logs-traces + type: bool + short-summary: Enable the OpenTelemetry (OTLP) logs and traces receiver. Requires Azure Monitor logs to be enabled. + - name: --disable-opentelemetry-logs-traces + type: bool + short-summary: Disable the OpenTelemetry (OTLP) logs and traces receiver. + - name: --opentelemetry-logs-traces-port-http + type: int + short-summary: HTTP/protobuf port for the OpenTelemetry logs and traces receiver. + - name: --opentelemetry-logs-traces-port-grpc + type: int + short-summary: gRPC port for the OpenTelemetry logs and traces receiver. - name: --nodepool-taints type: string short-summary: The node taints for all node pool. @@ -1247,6 +1351,16 @@ examples: - name: Reconcile the cluster back to its current state. text: az aks update -g MyResourceGroup -n MyManagedCluster + - name: Enable Azure Monitor logs (Container Insights) on an existing cluster. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-azure-monitor-logs + - name: Disable Azure Monitor logs (Container Insights) on an existing cluster. + text: az aks update -g MyResourceGroup -n MyManagedCluster --disable-azure-monitor-logs + - name: Change the syslog port used by Azure Monitor logs on an existing cluster. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-syslog --syslog-port 28330 + - name: Enable the OpenTelemetry logs and traces receiver on an existing cluster. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-opentelemetry-logs-traces --opentelemetry-logs-traces-port-grpc 4317 + - name: Enable the OpenTelemetry metrics receiver on an existing cluster. + text: az aks update -g MyResourceGroup -n MyManagedCluster --enable-opentelemetry-metrics --opentelemetry-metrics-port-grpc 4319 - name: Update a kubernetes cluster with standard SKU load balancer to use two AKS created IPs for the load balancer outbound connection usage. text: az aks update -g MyResourceGroup -n MyManagedCluster --load-balancer-managed-outbound-ip-count 2 - name: Update a kubernetes cluster with standard SKU load balancer to use the provided public IPs for the load balancer outbound connection usage. diff --git a/src/azure-cli/azure/cli/command_modules/acs/_params.py b/src/azure-cli/azure/cli/command_modules/acs/_params.py index c73a1e0c872..ff9a3db32a4 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_params.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_params.py @@ -140,6 +140,12 @@ validate_bootstrap_container_registry_resource_id, validate_gateway_prefix_size, validate_artifact_streaming, + validate_azure_monitor_logs_and_enable_addons, + validate_azure_monitor_logs_enable_disable, + validate_container_insights_settings_for_create, + validate_container_insights_settings_for_update, + validate_azure_monitor_and_opentelemetry_for_create, + validate_azure_monitor_and_opentelemetry_for_update, ) from azure.cli.core.commands.parameters import ( edge_zone_type, file_type, get_enum_type, @@ -504,11 +510,44 @@ def load_arguments(self, _): # addons c.argument('enable_addons', options_list=['--enable-addons', '-a']) c.argument('workspace_resource_id') - c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag()) + c.argument( + 'enable_msi_auth_for_monitoring', + arg_type=get_three_state_flag(), + deprecate_info=c.deprecate( + target='--enable-msi-auth-for-monitoring', + redirect='--enable-azure-monitor-logs', + ), + ) c.argument('enable_syslog', arg_type=get_three_state_flag()) c.argument('data_collection_settings') c.argument('ampls_resource_id', validator=validate_azuremonitor_privatelinkscope_resourceid) c.argument('enable_high_log_scale_mode', arg_type=get_three_state_flag()) + # azure monitor logs (container insights on the azure monitor profile) + c.argument( + 'enable_azure_monitor_logs', + action='store_true', + validator=validate_azure_monitor_logs_and_enable_addons, + ) + c.argument( + 'syslog_port', + type=int, + validator=validate_container_insights_settings_for_create, + ) + c.argument('enable_prometheus_metrics_scraping', action='store_true') + c.argument('disable_prometheus_metrics_scraping', action='store_true') + # opentelemetry + c.argument( + 'enable_opentelemetry_metrics', + action='store_true', + validator=validate_azure_monitor_and_opentelemetry_for_create, + ) + c.argument('disable_opentelemetry_metrics', action='store_true') + c.argument('opentelemetry_metrics_port_http', type=int) + c.argument('opentelemetry_metrics_port_grpc', type=int) + c.argument('enable_opentelemetry_logs_traces', action='store_true') + c.argument('disable_opentelemetry_logs_traces', action='store_true') + c.argument('opentelemetry_logs_traces_port_http', type=int) + c.argument('opentelemetry_logs_traces_port_grpc', type=int) c.argument('aci_subnet_name') c.argument('appgw_name', arg_group='Application Gateway') c.argument('appgw_subnet_cidr', arg_group='Application Gateway') @@ -846,6 +885,45 @@ def load_arguments(self, _): ) c.argument('enable_azure_monitor_app_monitoring', action='store_true') c.argument('disable_azure_monitor_app_monitoring', action='store_true') + # azure monitor logs (container insights on the azure monitor profile) + c.argument( + 'enable_azure_monitor_logs', + action='store_true', + validator=validate_azure_monitor_logs_enable_disable, + ) + c.argument('disable_azure_monitor_logs', action='store_true') + c.argument('workspace_resource_id') + c.argument( + 'enable_msi_auth_for_monitoring', + arg_type=get_three_state_flag(), + deprecate_info=c.deprecate( + target='--enable-msi-auth-for-monitoring', + redirect='--enable-azure-monitor-logs', + ), + ) + c.argument('enable_syslog', arg_type=get_three_state_flag()) + c.argument('data_collection_settings') + c.argument('ampls_resource_id', validator=validate_azuremonitor_privatelinkscope_resourceid) + c.argument( + 'syslog_port', + type=int, + validator=validate_container_insights_settings_for_update, + ) + c.argument('enable_prometheus_metrics_scraping', action='store_true') + c.argument('disable_prometheus_metrics_scraping', action='store_true') + # opentelemetry + c.argument( + 'enable_opentelemetry_metrics', + action='store_true', + validator=validate_azure_monitor_and_opentelemetry_for_update, + ) + c.argument('disable_opentelemetry_metrics', action='store_true') + c.argument('opentelemetry_metrics_port_http', type=int) + c.argument('opentelemetry_metrics_port_grpc', type=int) + c.argument('enable_opentelemetry_logs_traces', action='store_true') + c.argument('disable_opentelemetry_logs_traces', action='store_true') + c.argument('opentelemetry_logs_traces_port_http', type=int) + c.argument('opentelemetry_logs_traces_port_grpc', type=int) # azure container storage c.argument( "enable_azure_container_storage", @@ -969,7 +1047,14 @@ def load_arguments(self, _): c.argument('enable_sgxquotehelper', action='store_true') c.argument('enable_secret_rotation', action='store_true') c.argument('rotation_poll_interval') - c.argument('enable_msi_auth_for_monitoring', arg_type=get_three_state_flag()) + c.argument( + 'enable_msi_auth_for_monitoring', + arg_type=get_three_state_flag(), + deprecate_info=c.deprecate( + target='--enable-msi-auth-for-monitoring', + redirect='--enable-azure-monitor-logs', + ), + ) c.argument('enable_syslog', arg_type=get_three_state_flag()) c.argument('data_collection_settings') c.argument('ampls_resource_id', validator=validate_azuremonitor_privatelinkscope_resourceid) diff --git a/src/azure-cli/azure/cli/command_modules/acs/_validators.py b/src/azure-cli/azure/cli/command_modules/acs/_validators.py index 05bde5da70c..5e449c3fc61 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_validators.py @@ -1014,3 +1014,215 @@ def validate_artifact_streaming(namespace): raise ArgumentUsageError('--enable-artifact-streaming can only be set for Linux nodepools') if disable_artifact_streaming: raise ArgumentUsageError('--disable-artifact-streaming can only be set for Linux nodepools') + + +def _reject_msi_auth_flag_with_azure_monitor_logs(namespace, explicit_values): + """Reject --enable-msi-auth-for-monitoring alongside --enable-azure-monitor-logs. + + Onboarding through the Azure Monitor profile is managed identity only, so the auth flag has no + meaning there. ``explicit_values`` differs by command because the flag's default does: it is + ``True`` on create, where only an explicit ``false`` is distinguishable from the default, and + ``None`` on update, where any value is explicit. + """ + if not getattr(namespace, "enable_azure_monitor_logs", False): + return + if getattr(namespace, "enable_msi_auth_for_monitoring", None) in explicit_values: + raise MutuallyExclusiveArgumentError( + "Cannot specify both '--enable-azure-monitor-logs' and " + "'--enable-msi-auth-for-monitoring'. '--enable-azure-monitor-logs' onboards through " + "the Azure Monitor profile, which always uses managed identity authentication." + ) + + +def validate_azure_monitor_logs_and_enable_addons(namespace): + """Validate that enable_azure_monitor_logs and enable_addons don't conflict.""" + if getattr(namespace, "enable_azure_monitor_logs", False): + enable_addons = getattr(namespace, "enable_addons", None) + if enable_addons and "monitoring" in enable_addons: + raise ArgumentUsageError( + "Cannot specify both '--enable-azure-monitor-logs' and '--enable-addons monitoring'. " + "Use either '--enable-azure-monitor-logs' or '--enable-addons monitoring'." + ) + # On create the flag defaults to True, so only an explicit false is detectable here. + _reject_msi_auth_flag_with_azure_monitor_logs(namespace, (False,)) + + +def validate_azure_monitor_logs_enable_disable(namespace): + """Validate that enable and disable azure monitor logs parameters don't conflict.""" + if ( + getattr(namespace, "enable_azure_monitor_logs", False) and + getattr(namespace, "disable_azure_monitor_logs", False) + ): + raise MutuallyExclusiveArgumentError( + "Cannot specify both '--enable-azure-monitor-logs' and '--disable-azure-monitor-logs'. " + "Use either '--enable-azure-monitor-logs' or '--disable-azure-monitor-logs'." + ) + # On update the flag defaults to None, so any value is an explicit use. + _reject_msi_auth_flag_with_azure_monitor_logs(namespace, (True, False)) + + +def _specified_container_insights_setting_flags(namespace): + """Return the AMP containerInsights tuning flags explicitly present on the command line.""" + flags = [] + if getattr(namespace, "syslog_port", None) is not None: + flags.append("--syslog-port") + if getattr(namespace, "enable_prometheus_metrics_scraping", False): + flags.append("--enable-prometheus-metrics-scraping") + if getattr(namespace, "disable_prometheus_metrics_scraping", False): + flags.append("--disable-prometheus-metrics-scraping") + return flags + + +def _validate_container_insights_settings_common(namespace): + """Validations for the containerInsights tuning flags that do not depend on cluster state.""" + if ( + getattr(namespace, "enable_prometheus_metrics_scraping", False) and + getattr(namespace, "disable_prometheus_metrics_scraping", False) + ): + raise MutuallyExclusiveArgumentError( + "Cannot specify both --enable-prometheus-metrics-scraping and " + "--disable-prometheus-metrics-scraping at the same time." + ) + + syslog_port = getattr(namespace, "syslog_port", None) + if syslog_port is not None and not 1 <= syslog_port <= 65535: + raise InvalidArgumentValueError( + f"--syslog-port must be a valid TCP port between 1 and 65535, got {syslog_port}." + ) + + flags = _specified_container_insights_setting_flags(namespace) + if flags and getattr(namespace, "disable_azure_monitor_logs", False): + raise ArgumentUsageError( + f"{', '.join(flags)} cannot be specified with --disable-azure-monitor-logs." + ) + + +def validate_container_insights_settings_for_create(namespace): + """Validate the containerInsights tuning flags for create operations.""" + _validate_container_insights_settings_common(namespace) + + flags = _specified_container_insights_setting_flags(namespace) + if flags and not getattr(namespace, "enable_azure_monitor_logs", False): + raise ArgumentUsageError( + f"{', '.join(flags)} requires Azure Monitor logs to be enabled. " + "Please add --enable-azure-monitor-logs to your command." + ) + + +def validate_container_insights_settings_for_update(namespace): + """Validate the containerInsights tuning flags for update operations.""" + _validate_container_insights_settings_common(namespace) + # Whether Azure Monitor logs is already enabled on the cluster is only visible once the + # ManagedCluster has been fetched, so that dependency check is deferred to the decorator. + + +def validate_opentelemetry_ports(namespace): + """Validate that the OpenTelemetry HTTP and gRPC ports are in range and all distinct.""" + ports = [ + ("--opentelemetry-metrics-port-http", getattr(namespace, "opentelemetry_metrics_port_http", None)), + ("--opentelemetry-metrics-port-grpc", getattr(namespace, "opentelemetry_metrics_port_grpc", None)), + ("--opentelemetry-logs-traces-port-http", getattr(namespace, "opentelemetry_logs_traces_port_http", None)), + ("--opentelemetry-logs-traces-port-grpc", getattr(namespace, "opentelemetry_logs_traces_port_grpc", None)), + ] + + for flag, port in ports: + if port is not None and not 1 <= port <= 65535: + raise ArgumentUsageError( + f"OpenTelemetry port {flag} must be between 1 and 65535, got {port}." + ) + + # All specified OpenTelemetry ports (HTTP and gRPC, metrics and logs/traces) must be distinct + specified = [(flag, port) for flag, port in ports if port is not None] + for i, (flag_i, port_i) in enumerate(specified): + for flag_j, port_j in specified[i + 1:]: + if port_i == port_j: + raise ArgumentUsageError( + "OpenTelemetry ports must all be different. " + f"{flag_i} and {flag_j} cannot both be set to {port_i}." + ) + + +def validate_opentelemetry_metrics_dependencies(namespace): + """Validate OpenTelemetry metrics dependencies for create operations.""" + enable_otlp_metrics = getattr(namespace, "enable_opentelemetry_metrics", False) + disable_otlp_metrics = getattr(namespace, "disable_opentelemetry_metrics", False) + + if enable_otlp_metrics and disable_otlp_metrics: + raise MutuallyExclusiveArgumentError( + "Cannot specify both --enable-opentelemetry-metrics and --disable-opentelemetry-metrics " + "at the same time." + ) + + # For create operations, require explicit Azure Monitor metrics enablement + if enable_otlp_metrics and not getattr(namespace, "enable_azure_monitor_metrics", False): + raise ArgumentUsageError( + "OpenTelemetry metrics requires Azure Monitor metrics to be enabled. " + "Please add --enable-azure-monitor-metrics to your command." + ) + + +def validate_opentelemetry_metrics_dependencies_for_update(namespace): + """Validate OpenTelemetry metrics dependencies for update operations.""" + enable_otlp_metrics = getattr(namespace, "enable_opentelemetry_metrics", False) + disable_otlp_metrics = getattr(namespace, "disable_opentelemetry_metrics", False) + + if enable_otlp_metrics and disable_otlp_metrics: + raise MutuallyExclusiveArgumentError( + "Cannot specify both --enable-opentelemetry-metrics and --disable-opentelemetry-metrics " + "at the same time." + ) + # Whether Azure Monitor metrics is already enabled on the cluster is only visible once the + # ManagedCluster has been fetched, so that dependency check is deferred to the decorator. + + +def validate_opentelemetry_logs_traces_dependencies(namespace): + """Validate OpenTelemetry logs and traces dependencies for create operations.""" + enable_otlp_logs = getattr(namespace, "enable_opentelemetry_logs_traces", False) + disable_otlp_logs = getattr(namespace, "disable_opentelemetry_logs_traces", False) + + if enable_otlp_logs and disable_otlp_logs: + raise MutuallyExclusiveArgumentError( + "Cannot specify both --enable-opentelemetry-logs-traces and " + "--disable-opentelemetry-logs-traces at the same time." + ) + + # For create operations, Azure Monitor logs must be enabled by the same command, either + # through the Azure Monitor profile or the legacy monitoring addon. + enable_addons = getattr(namespace, "enable_addons", None) + azure_monitor_logs_enabled = ( + getattr(namespace, "enable_azure_monitor_logs", False) or + (enable_addons and "monitoring" in enable_addons) + ) + if enable_otlp_logs and not azure_monitor_logs_enabled: + raise ArgumentUsageError( + "OpenTelemetry logs and traces requires Azure Monitor logs to be enabled. " + "Please add --enable-azure-monitor-logs to your command." + ) + + +def validate_opentelemetry_logs_traces_dependencies_for_update(namespace): + """Validate OpenTelemetry logs and traces dependencies for update operations.""" + enable_otlp_logs = getattr(namespace, "enable_opentelemetry_logs_traces", False) + disable_otlp_logs = getattr(namespace, "disable_opentelemetry_logs_traces", False) + + if enable_otlp_logs and disable_otlp_logs: + raise MutuallyExclusiveArgumentError( + "Cannot specify both --enable-opentelemetry-logs-traces and " + "--disable-opentelemetry-logs-traces at the same time." + ) + # Whether Azure Monitor logs is already enabled on the cluster is only visible once the + # ManagedCluster has been fetched, so that dependency check is deferred to the decorator. + + +def validate_azure_monitor_and_opentelemetry_for_create(namespace): + """Main validator for Azure Monitor and OpenTelemetry configurations for create operations.""" + validate_opentelemetry_ports(namespace) + validate_opentelemetry_metrics_dependencies(namespace) + validate_opentelemetry_logs_traces_dependencies(namespace) + + +def validate_azure_monitor_and_opentelemetry_for_update(namespace): + """Main validator for Azure Monitor and OpenTelemetry configurations for update operations.""" + validate_opentelemetry_ports(namespace) + validate_opentelemetry_metrics_dependencies_for_update(namespace) + validate_opentelemetry_logs_traces_dependencies_for_update(namespace) diff --git a/src/azure-cli/azure/cli/command_modules/acs/addonconfiguration.py b/src/azure-cli/azure/cli/command_modules/acs/addonconfiguration.py index 0194aff5a8f..03336519208 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/addonconfiguration.py +++ b/src/azure-cli/azure/cli/command_modules/acs/addonconfiguration.py @@ -390,6 +390,27 @@ def get_existing_container_insights_extension_dcr_tags(cmd, dcr_url): return tags +def warn_on_legacy_monitoring_auth(enable_msi_auth_for_monitoring, addons): + """Warn when the user explicitly opts into legacy shared key authentication for monitoring. + + The argument level deprecation already fires for any explicit use of + --enable-msi-auth-for-monitoring. This adds the migration pointer for the value that actually + leaves the cluster on shared key authentication, which is also the state that later blocks + --enable-azure-monitor-logs. + """ + if enable_msi_auth_for_monitoring is not False: + return + if "monitoring" not in (addons or ""): + return + logger.warning( + "--enable-msi-auth-for-monitoring false configures Container Insights with legacy shared " + "key authentication. Managed identity authentication is recommended, and is required by " + "'--enable-azure-monitor-logs'. See " + "https://learn.microsoft.com/en-us/azure/azure-monitor/containers/" + "container-insights-authentication?tabs=cli#migrate-to-managed-identity-authentication" + ) + + # pylint: disable=too-many-locals,too-many-branches,too-many-statements,line-too-long def ensure_container_insights_for_monitoring( cmd, diff --git a/src/azure-cli/azure/cli/command_modules/acs/custom.py b/src/azure-cli/azure/cli/command_modules/acs/custom.py index 02ec9df017d..3b10d7597d6 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/custom.py @@ -90,6 +90,7 @@ add_virtual_node_role_assignment, ensure_container_insights_for_monitoring, ensure_default_log_analytics_workspace_for_monitoring, + warn_on_legacy_monitoring_auth, ) from azure.cli.core._profile import Profile from azure.cli.core.azclierror import ( @@ -955,6 +956,20 @@ def aks_create( data_collection_settings=None, ampls_resource_id=None, enable_high_log_scale_mode=None, + # azure monitor logs (container insights on the azure monitor profile) + enable_azure_monitor_logs=False, + syslog_port=None, + enable_prometheus_metrics_scraping=False, + disable_prometheus_metrics_scraping=False, + # opentelemetry + enable_opentelemetry_metrics=False, + disable_opentelemetry_metrics=False, + opentelemetry_metrics_port_http=None, + opentelemetry_metrics_port_grpc=None, + enable_opentelemetry_logs_traces=False, + disable_opentelemetry_logs_traces=False, + opentelemetry_logs_traces_port_http=None, + opentelemetry_logs_traces_port_grpc=None, aci_subnet_name=None, appgw_name=None, appgw_subnet_cidr=None, @@ -1223,6 +1238,26 @@ def aks_update( disable_control_plane_metrics=False, enable_azure_monitor_app_monitoring=False, disable_azure_monitor_app_monitoring=False, + # azure monitor logs (container insights on the azure monitor profile) + enable_azure_monitor_logs=False, + disable_azure_monitor_logs=False, + workspace_resource_id=None, + enable_msi_auth_for_monitoring=None, + enable_syslog=None, + data_collection_settings=None, + ampls_resource_id=None, + syslog_port=None, + enable_prometheus_metrics_scraping=False, + disable_prometheus_metrics_scraping=False, + # opentelemetry + enable_opentelemetry_metrics=False, + disable_opentelemetry_metrics=False, + opentelemetry_metrics_port_http=None, + opentelemetry_metrics_port_grpc=None, + enable_opentelemetry_logs_traces=False, + disable_opentelemetry_logs_traces=False, + opentelemetry_logs_traces_port_http=None, + opentelemetry_logs_traces_port_grpc=None, # azure container storage enable_azure_container_storage=None, disable_azure_container_storage=None, @@ -1627,6 +1662,7 @@ def aks_enable_addons(cmd, client, resource_group_name, name, addons, ampls_resource_id=None, enable_high_log_scale_mode=None, no_wait=False,): + warn_on_legacy_monitoring_auth(enable_msi_auth_for_monitoring, addons) instance = client.get(resource_group_name, name) msi_auth = False if instance.service_principal_profile.client_id == "msi": diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 6a0480a8850..92d4d572549 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -54,6 +54,9 @@ CONST_APP_ROUTING_ISTIO_MODE_DISABLED, CONST_MANAGED_GATEWAY_INSTALLATION_DISABLED, CONST_MANAGED_GATEWAY_INSTALLATION_STANDARD, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, + CONST_CONTAINER_NETWORK_LOGS_DISABLED, + CONST_MONITORING_ENABLE_RETINA_NETWORK_FLAGS, ) from azure.cli.command_modules.acs.azurecontainerstorage._consts import ( CONST_ACSTOR_EXT_INSTALLATION_NAME, @@ -173,6 +176,124 @@ def _get_monitoring_addon_key_from_consts(addon_profiles, addon_consts): addon_consts.get("CONST_MONITORING_ADDON_NAME"), ) + +def _get_container_insights_profile(mc): + """Return the Azure Monitor Profile containerInsights object, or None.""" + azure_monitor_profile = getattr(mc, "azure_monitor_profile", None) if mc is not None else None + if azure_monitor_profile is None: + return None + return getattr(azure_monitor_profile, "container_insights", None) + + +def _is_container_insights_enabled(mc): + """Whether Azure Monitor logs are enabled through the AMP containerInsights profile.""" + container_insights = _get_container_insights_profile(mc) + return bool(container_insights and container_insights.enabled) + + +def _get_monitoring_addon_profile(cluster, addon_consts): + """Return the omsagent addon profile object for a cluster, or None.""" + addon_profiles = getattr(cluster, "addon_profiles", None) if cluster is not None else None + if not addon_profiles: + return None + addon_key = _get_monitoring_addon_key_from_consts(addon_profiles, addon_consts) + return addon_profiles.get(addon_key) + + +def _is_monitoring_enabled_on_mc(mc, addon_consts): + """Whether Azure Monitor logs are enabled through either the AMP profile or the legacy addon.""" + if _is_container_insights_enabled(mc): + return True + addon_profile = _get_monitoring_addon_profile(mc, addon_consts) + return bool(addon_profile and addon_profile.enabled) + + +def _apply_container_insights_settings(container_insights, syslog_port, disable_prometheus_scraping): + """Write the optional AMP containerInsights tuning fields, leaving unset ones untouched.""" + if syslog_port is not None: + container_insights.syslog_port = syslog_port + if disable_prometheus_scraping is not None: + container_insights.disable_prometheus_metrics_scraping = disable_prometheus_scraping + + +def _get_addon_config_value(config, key): + """Case-insensitive lookup of an addon config value. + + ARM echoes addon config keys back in whatever casing they were written with, and the RP + reads them case-insensitively, so match that here. + """ + if not config or not key: + return None + if key in config: + return config[key] + lowered = key.lower() + for existing_key, value in config.items(): + if existing_key.lower() == lowered: + return value + return None + + +def _is_container_network_logs_enabled_on_mc(mc, addon_consts): + """Whether container network logs are on, via the AMP profile or the legacy addon config key.""" + container_insights = _get_container_insights_profile(mc) + if container_insights and safe_lower(container_insights.container_network_logs) == \ + CONST_CONTAINER_NETWORK_LOGS_ENABLED.lower(): + return True + addon_profile = _get_monitoring_addon_profile(mc, addon_consts) + if addon_profile is None: + return False + value = _get_addon_config_value(addon_profile.config, CONST_MONITORING_ENABLE_RETINA_NETWORK_FLAGS) + return safe_lower(value) == "true" + + +def _is_monitoring_aad_auth(cluster, addon_consts): + """Whether Azure Monitor logs uses managed identity (AAD) auth on this cluster. + + The AMP containerInsights profile carries no auth information, and the RP mirrors a legacy + addon into it, so a cluster onboarded with shared key auth still ends up with a + containerInsights profile. The presence of that profile is therefore inconclusive, and the + omsagent addon config is the only authoritative source of the auth mode. + + Mirror the RP's derivation: + + * no omsagent addon at all -> fresh onboarding, which always defaults to AAD auth + * addon present but disabled -> a re-enable, which the RP also treats as fresh onboarding + * otherwise -> the addon's ``useAADAuth`` value, where absent or empty means legacy auth + """ + addon_profile = _get_monitoring_addon_profile(cluster, addon_consts) + if addon_profile is None or not addon_profile.enabled: + return True + msi_auth_key = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") + return safe_lower(_get_addon_config_value(addon_profile.config, msi_auth_key)) == "true" + + +def _build_monitoring_addon_shim(cluster, models, addon_consts): + """Build the object that drives DCR/DCE/DCRA/AMPLS provisioning. + + ``ensure_container_insights_for_monitoring`` reads only ``enabled`` and the workspace id out + of ``config``. Synthesizing those from the AMP containerInsights profile lets the provisioning + run without the legacy omsagent addon being present, while the fallback keeps clusters + onboarded before the AMP switch working unchanged. + + The auth mode is derived separately via :func:`_is_monitoring_aad_auth` rather than assumed + from the AMP profile, because the RP mirrors legacy shared key clusters into that profile. + """ + workspace_key = addon_consts.get("CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID") + msi_auth_key = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") + + container_insights = _get_container_insights_profile(cluster) + if container_insights and container_insights.enabled and container_insights.log_analytics_workspace_resource_id: + return models.ManagedClusterAddonProfile( + enabled=True, + config={ + workspace_key: container_insights.log_analytics_workspace_resource_id, + msi_auth_key: "true" if _is_monitoring_aad_auth(cluster, addon_consts) else "false", + }, + ) + + return _get_monitoring_addon_profile(cluster, addon_consts) + + # TODO # 1. remove enable_rbac related implementation # 2. add validation for all/some of the parameters involved in the getter of outbound_type/enable_addons @@ -2768,14 +2889,13 @@ def get_container_network_logs(self, mc: ManagedCluster) -> Union[bool, None]: enable_addons = self.raw_param.get("enable_addons") monitoring_via_enable_addons = enable_addons and "monitoring" in enable_addons - # Check if monitoring is already enabled on the cluster + # Check if Azure Monitor logs is being enabled by this command through the AMP path + enable_azure_monitor_logs = bool(self.raw_param.get("enable_azure_monitor_logs")) + + # Check if monitoring is already enabled on the cluster, via the AMP containerInsights + # profile or the legacy omsagent addon addon_consts = self.get_addon_consts() - monitoring_addon_key = _get_monitoring_addon_key_from_consts(mc.addon_profiles, addon_consts) - monitoring_on_cluster = ( - mc.addon_profiles and - mc.addon_profiles.get(monitoring_addon_key) and - mc.addon_profiles[monitoring_addon_key].enabled - ) + monitoring_on_cluster = _is_monitoring_enabled_on_mc(mc, addon_consts) # Check if ACNS is being enabled or already enabled acns_enabled = ( @@ -2792,13 +2912,27 @@ def get_container_network_logs(self, mc: ManagedCluster) -> Union[bool, None]: network_dataplane = network_dataplane_param or network_dataplane_cluster cilium_enabled = safe_lower(network_dataplane) == "cilium" - monitoring_enabled = monitoring_via_enable_addons or monitoring_on_cluster + monitoring_enabled = monitoring_via_enable_addons or enable_azure_monitor_logs or monitoring_on_cluster if enable_cnl and (not acns_enabled or not monitoring_enabled or not cilium_enabled): raise InvalidArgumentValueError( - "Container network logs requires ACNS to be enabled, the monitoring addon to be enabled, " + "Container network logs requires ACNS to be enabled, Azure Monitor logs to be enabled, " "and the cilium network dataplane." ) + + # Container network logs rely on high log scale mode, whose data path is a data collection + # rule. Data collection rules are only provisioned for managed identity authentication, so + # the feature cannot work on a cluster still using legacy shared key authentication. + if enable_cnl and not _is_monitoring_aad_auth(mc, addon_consts): + raise InvalidArgumentValueError( + "Container network logs requires Azure Monitor logs to use managed identity " + "authentication. This cluster is onboarded with legacy (shared key) authentication, " + "which does not support the data collection rule that container network logs depends " + "on. Migrate the cluster to managed identity authentication first, then retry. See " + "https://learn.microsoft.com/en-us/azure/azure-monitor/containers/" + "container-insights-authentication?tabs=cli#migrate-to-managed-identity-authentication" + ) + enable_cnl = bool(enable_cnl) if enable_cnl is not None else False disable_cnl = bool(disable_cnl) if disable_cnl is not None else False return enable_cnl or not disable_cnl @@ -3124,9 +3258,14 @@ def _get_enable_addons(self, enable_validation: bool = False) -> List[str]: # check monitoring/workspace_resource_id workspace_resource_id = self._get_workspace_resource_id(read_only=True) - if "monitoring" not in enable_addons and workspace_resource_id: + if ( + "monitoring" not in enable_addons and + workspace_resource_id and + not self.raw_param.get("enable_azure_monitor_logs") + ): raise RequiredArgumentMissingError( - '"--workspace-resource-id" requires "--enable-addons monitoring".') + '"--workspace-resource-id" requires "--enable-addons monitoring" or ' + '"--enable-azure-monitor-logs".') # check virtual node/aci_subnet_name/vnet_subnet_id # Note: The external parameters involved in the validation are not verified in their own getters. @@ -3216,9 +3355,14 @@ def _get_workspace_resource_id( # validation if enable_validation: enable_addons = self._get_enable_addons(enable_validation=False) - if workspace_resource_id and "monitoring" not in enable_addons: + if ( + workspace_resource_id and + "monitoring" not in enable_addons and + not self.raw_param.get("enable_azure_monitor_logs") + ): raise RequiredArgumentMissingError( - '"--workspace-resource-id" requires "--enable-addons monitoring".') + '"--workspace-resource-id" requires "--enable-addons monitoring" or ' + '"--enable-azure-monitor-logs".') # this parameter does not need validation return workspace_resource_id @@ -3242,6 +3386,11 @@ def get_enable_msi_auth_for_monitoring(self) -> Union[bool, None]: :return: bool or None """ + # The Azure Monitor profile is managed identity only, so onboarding through + # --enable-azure-monitor-logs always authenticates with managed identity. + if self.raw_param.get("enable_azure_monitor_logs"): + return True + # determine the value of constants addon_consts = self.get_addon_consts() CONST_MONITORING_USING_AAD_MSI_AUTH = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") @@ -3284,6 +3433,129 @@ def get_enable_msi_auth_for_monitoring(self) -> Union[bool, None]: # this parameter does not need validation return enable_msi_auth_for_monitoring + # Azure Monitor logs (Container Insights on the Azure Monitor profile) + def _get_enable_azure_monitor_logs(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_azure_monitor_logs. + + :return: bool + """ + enable_azure_monitor_logs = self.raw_param.get("enable_azure_monitor_logs") + if enable_validation and enable_azure_monitor_logs: + if self._get_disable_azure_monitor_logs(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-monitor-logs and --disable-azure-monitor-logs at the same time." + ) + # The Azure Monitor profile is managed identity only, so the legacy auth flag is + # meaningless on this path. On update the parameter defaults to None, so any value is + # an explicit request. On create it defaults to True, which is indistinguishable from + # the user passing it; only an explicit False is detectable there, and that is the case + # that actually conflicts because it asks for shared key auth. + enable_msi_auth = self.raw_param.get("enable_msi_auth_for_monitoring") + explicitly_requested = ( + enable_msi_auth is not None + if self.decorator_mode == DecoratorMode.UPDATE + else enable_msi_auth is False + ) + if explicitly_requested: + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-msi-auth-for-monitoring with --enable-azure-monitor-logs. " + "Azure Monitor logs always uses managed identity authentication, so the flag has no " + "effect. Remove --enable-msi-auth-for-monitoring, or use '--enable-addons monitoring' " + "if you need to control the authentication mode." + ) + return bool(enable_azure_monitor_logs) + + def get_enable_azure_monitor_logs(self) -> bool: + """Obtain the value of enable_azure_monitor_logs. + + :return: bool + """ + return self._get_enable_azure_monitor_logs(enable_validation=True) + + def _get_disable_azure_monitor_logs(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_azure_monitor_logs. + + :return: bool + """ + disable_azure_monitor_logs = self.raw_param.get("disable_azure_monitor_logs") + if enable_validation and disable_azure_monitor_logs and self._get_enable_azure_monitor_logs(False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-azure-monitor-logs and --disable-azure-monitor-logs at the same time." + ) + return bool(disable_azure_monitor_logs) + + def get_disable_azure_monitor_logs(self) -> bool: + """Obtain the value of disable_azure_monitor_logs. + + :return: bool + """ + return self._get_disable_azure_monitor_logs(enable_validation=True) + + def _validate_container_insights_setting(self, flag_name: str) -> None: + """Validate that an AMP containerInsights setting can be applied by this command. + + These settings live only on the Azure Monitor profile, so they need the profile to be + turned on by this command or, on update, to be on already. + """ + if self._get_disable_azure_monitor_logs(False): + raise InvalidArgumentValueError( + f"{flag_name} cannot be specified when --disable-azure-monitor-logs is used." + ) + if self._get_enable_azure_monitor_logs(False): + return + if self.decorator_mode == DecoratorMode.UPDATE: + if _is_monitoring_enabled_on_mc(self.mc, self.get_addon_consts()): + return + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-azure-monitor-logs is also " + "specified or Azure Monitor logs is already enabled on the cluster." + ) + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-azure-monitor-logs is also specified." + ) + + def get_syslog_port(self) -> Union[int, None]: + """Obtain the value of syslog_port. + + Returns None when the flag is omitted, which leaves the server default (28330) in place. + + :return: int or None + """ + syslog_port = self.raw_param.get("syslog_port") + if syslog_port is None: + return None + + if syslog_port < 1 or syslog_port > 65535: + raise InvalidArgumentValueError( + "--syslog-port must be a valid TCP port between 1 and 65535." + ) + self._validate_container_insights_setting("--syslog-port") + return syslog_port + + def get_disable_prometheus_metrics_scraping(self) -> Union[bool, None]: + """Obtain the value to write to containerInsights.disablePrometheusMetricsScraping. + + Returns None when neither flag is given, so the field is left untouched. + + :return: bool or None + """ + enable_scraping = self.raw_param.get("enable_prometheus_metrics_scraping") + disable_scraping = self.raw_param.get("disable_prometheus_metrics_scraping") + + if enable_scraping and disable_scraping: + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-prometheus-metrics-scraping and " + "--disable-prometheus-metrics-scraping at the same time." + ) + if not enable_scraping and not disable_scraping: + return None + + self._validate_container_insights_setting( + "--disable-prometheus-metrics-scraping" if disable_scraping + else "--enable-prometheus-metrics-scraping" + ) + return bool(disable_scraping) + def get_enable_syslog(self) -> Union[bool, None]: """Obtain the value of enable_syslog. @@ -5969,6 +6241,252 @@ def get_disable_azure_monitor_metrics(self) -> bool: """ return self._get_disable_azure_monitor_metrics(enable_validation=True) + # OpenTelemetry (azureMonitorProfile.appMonitoring.openTelemetry*) + def _get_enable_opentelemetry_metrics(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_opentelemetry_metrics. + + :return: bool + """ + enable_opentelemetry_metrics = self.raw_param.get("enable_opentelemetry_metrics") + + if enable_validation and enable_opentelemetry_metrics: + if self._get_disable_opentelemetry_metrics(enable_validation=False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-opentelemetry-metrics and " + "--disable-opentelemetry-metrics at the same time." + ) + + # OpenTelemetry metrics ride on the Azure Monitor metrics profile, so it must either be + # turned on by this same command or, on update, already be on. + azure_monitor_enabled_in_profile = ( + self.decorator_mode == DecoratorMode.UPDATE and + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.metrics and + self.mc.azure_monitor_profile.metrics.enabled + ) + if not self.raw_param.get("enable_azure_monitor_metrics") and not azure_monitor_enabled_in_profile: + raise ArgumentUsageError( + "OpenTelemetry metrics requires Azure Monitor metrics to be enabled. " + "Please add --enable-azure-monitor-metrics to your command." + ) + return bool(enable_opentelemetry_metrics) + + def get_enable_opentelemetry_metrics(self) -> bool: + """Obtain the value of enable_opentelemetry_metrics. + + :return: bool + """ + return self._get_enable_opentelemetry_metrics(enable_validation=True) + + def _get_disable_opentelemetry_metrics(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_opentelemetry_metrics. + + :return: bool + """ + disable_opentelemetry_metrics = self.raw_param.get("disable_opentelemetry_metrics") + if ( + enable_validation and + disable_opentelemetry_metrics and + self._get_enable_opentelemetry_metrics(enable_validation=False) + ): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-opentelemetry-metrics and " + "--disable-opentelemetry-metrics at the same time." + ) + return bool(disable_opentelemetry_metrics) + + def get_disable_opentelemetry_metrics(self) -> bool: + """Obtain the value of disable_opentelemetry_metrics. + + :return: bool + """ + return self._get_disable_opentelemetry_metrics(enable_validation=True) + + def _get_enable_opentelemetry_logs_traces(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of enable_opentelemetry_logs_traces. + + :return: bool + """ + enable_opentelemetry_logs_traces = self.raw_param.get("enable_opentelemetry_logs_traces") + + if enable_validation and enable_opentelemetry_logs_traces: + if self._get_disable_opentelemetry_logs_traces(enable_validation=False): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-opentelemetry-logs-traces and " + "--disable-opentelemetry-logs-traces at the same time." + ) + + # OpenTelemetry logs and traces ride on Azure Monitor logs, which may be turned on by + # this command through the Azure Monitor profile or the legacy monitoring addon, or on + # update already be on through either of those on the cluster. + enabled_by_this_command = bool( + self.raw_param.get("enable_azure_monitor_logs") or + "monitoring" in (self.raw_param.get("enable_addons") or "") + ) + monitoring_currently_enabled = ( + self.decorator_mode == DecoratorMode.UPDATE and + self.mc and + _is_monitoring_enabled_on_mc(self.mc, self.get_addon_consts()) + ) + if not enabled_by_this_command and not monitoring_currently_enabled: + raise ArgumentUsageError( + "OpenTelemetry logs and traces requires Azure Monitor logs to be enabled. " + "Please add --enable-azure-monitor-logs to your command." + ) + return bool(enable_opentelemetry_logs_traces) + + def get_enable_opentelemetry_logs_traces(self) -> bool: + """Obtain the value of enable_opentelemetry_logs_traces. + + :return: bool + """ + return self._get_enable_opentelemetry_logs_traces(enable_validation=True) + + def _get_disable_opentelemetry_logs_traces(self, enable_validation: bool = False) -> bool: + """Internal function to obtain the value of disable_opentelemetry_logs_traces. + + :return: bool + """ + disable_opentelemetry_logs_traces = self.raw_param.get("disable_opentelemetry_logs_traces") + if ( + enable_validation and + disable_opentelemetry_logs_traces and + self._get_enable_opentelemetry_logs_traces(enable_validation=False) + ): + raise MutuallyExclusiveArgumentError( + "Cannot specify --enable-opentelemetry-logs-traces and " + "--disable-opentelemetry-logs-traces at the same time." + ) + return bool(disable_opentelemetry_logs_traces) + + def get_disable_opentelemetry_logs_traces(self) -> bool: + """Obtain the value of disable_opentelemetry_logs_traces. + + :return: bool + """ + return self._get_disable_opentelemetry_logs_traces(enable_validation=True) + + def _get_opentelemetry_metrics_port(self, param_name: str, flag_name: str) -> Union[int, None]: + """Shared validation for the OTLP metrics host port flags. + + :return: int or None + """ + port = self.raw_param.get(param_name) + if port is None: + return None + + if port < 1 or port > 65535: + raise InvalidArgumentValueError( + f"{flag_name} must be a valid TCP port between 1 and 65535." + ) + if self.get_disable_azure_monitor_metrics(): + raise InvalidArgumentValueError( + f"{flag_name} cannot be specified when --disable-azure-monitor-metrics is used." + ) + if self.get_disable_opentelemetry_metrics(): + raise InvalidArgumentValueError( + f"{flag_name} cannot be specified when --disable-opentelemetry-metrics is used." + ) + + if self.get_enable_opentelemetry_metrics(): + return port + if self.decorator_mode == DecoratorMode.UPDATE: + already_enabled = ( + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.app_monitoring and + self.mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics and + self.mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled + ) + if already_enabled: + return port + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-opentelemetry-metrics is also " + "specified or OpenTelemetry metrics are already enabled." + ) + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-opentelemetry-metrics is also specified." + ) + + def get_opentelemetry_metrics_port_http(self) -> Union[int, None]: + """Obtain the value of opentelemetry_metrics_port_http. + + :return: int or None + """ + return self._get_opentelemetry_metrics_port( + "opentelemetry_metrics_port_http", "--opentelemetry-metrics-port-http" + ) + + def get_opentelemetry_metrics_port_grpc(self) -> Union[int, None]: + """Obtain the value of opentelemetry_metrics_port_grpc. + + :return: int or None + """ + return self._get_opentelemetry_metrics_port( + "opentelemetry_metrics_port_grpc", "--opentelemetry-metrics-port-grpc" + ) + + def _get_opentelemetry_logs_traces_port(self, param_name: str, flag_name: str) -> Union[int, None]: + """Shared validation for the OTLP logs and traces host port flags. + + :return: int or None + """ + port = self.raw_param.get(param_name) + if port is None: + return None + + if port < 1 or port > 65535: + raise InvalidArgumentValueError( + f"{flag_name} must be a valid TCP port between 1 and 65535." + ) + if self.get_disable_azure_monitor_logs(): + raise InvalidArgumentValueError( + f"{flag_name} cannot be specified when --disable-azure-monitor-logs is used." + ) + if self.get_disable_opentelemetry_logs_traces(): + raise InvalidArgumentValueError( + f"{flag_name} cannot be specified when --disable-opentelemetry-logs-traces is used." + ) + + if self.get_enable_opentelemetry_logs_traces(): + return port + if self.decorator_mode == DecoratorMode.UPDATE: + already_enabled = ( + self.mc and + self.mc.azure_monitor_profile and + self.mc.azure_monitor_profile.app_monitoring and + self.mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces and + self.mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled + ) + if already_enabled: + return port + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-opentelemetry-logs-traces is also " + "specified or OpenTelemetry logs and traces are already enabled." + ) + raise InvalidArgumentValueError( + f"{flag_name} can only be specified when --enable-opentelemetry-logs-traces is also specified." + ) + + def get_opentelemetry_logs_traces_port_http(self) -> Union[int, None]: + """Obtain the value of opentelemetry_logs_traces_port_http. + + :return: int or None + """ + return self._get_opentelemetry_logs_traces_port( + "opentelemetry_logs_traces_port_http", "--opentelemetry-logs-traces-port-http" + ) + + def get_opentelemetry_logs_traces_port_grpc(self) -> Union[int, None]: + """Obtain the value of opentelemetry_logs_traces_port_grpc. + + :return: int or None + """ + return self._get_opentelemetry_logs_traces_port( + "opentelemetry_logs_traces_port_grpc", "--opentelemetry-logs-traces-port-grpc" + ) + def validate_control_plane_metrics_params(self) -> None: """Validate the --enable/--disable-control-plane-metrics flag combo and its interaction with --enable/--disable-azure-monitor-metrics. @@ -7359,14 +7877,15 @@ def set_up_addon_profiles(self, mc: ManagedCluster) -> ManagedCluster: CONST_AZURE_KEYVAULT_SECRETS_PROVIDER_ADDON_NAME ] = self.build_azure_keyvault_secrets_provider_addon_profile() - # Set up container network logs if enabled + # Set up container network logs if enabled. This is written on the Azure Monitor profile + # rather than the legacy omsagent addon config key. container_network_logs_enabled = self.context.get_container_network_logs(mc) if container_network_logs_enabled is not None: - monitoring_addon_profile = addon_profiles.get(CONST_MONITORING_ADDON_NAME) - if monitoring_addon_profile: - config = monitoring_addon_profile.config or {} - config["enableRetinaNetworkFlags"] = str(container_network_logs_enabled) - monitoring_addon_profile.config = config + self._ensure_container_insights(mc).container_network_logs = ( + CONST_CONTAINER_NETWORK_LOGS_ENABLED + if container_network_logs_enabled + else CONST_CONTAINER_NETWORK_LOGS_DISABLED + ) # Trigger validation for high log scale mode when container network logs are enabled. # This ensures proper error messages are raised before cluster creation if the user @@ -7375,6 +7894,11 @@ def set_up_addon_profiles(self, mc: ManagedCluster) -> ManagedCluster: self.context.get_enable_high_log_scale_mode() mc.addon_profiles = addon_profiles + + # Handle enable Azure Monitor logs, which onboards through the Azure Monitor profile and + # intentionally does not author an omsagent addon entry. + if self.context.get_enable_azure_monitor_logs(): + self._setup_azure_monitor_logs(mc) return mc def set_up_aad_profile(self, mc: ManagedCluster) -> ManagedCluster: @@ -7848,6 +8372,119 @@ def set_up_k8s_support_plan(self, mc: ManagedCluster) -> ManagedCluster: mc.support_plan = support_plan return mc + def _ensure_azure_monitor_profile(self, mc: ManagedCluster) -> None: + """Ensure the azure monitor profile exists on the managed cluster.""" + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() + + def _ensure_container_insights(self, mc: ManagedCluster): + """Ensure the AMP containerInsights profile exists and return it.""" + self._ensure_azure_monitor_profile(mc) + if mc.azure_monitor_profile.container_insights is None: + mc.azure_monitor_profile.container_insights = ( + self.models.ManagedClusterAzureMonitorProfileContainerInsights() + ) + return mc.azure_monitor_profile.container_insights + + def _ensure_app_monitoring_profile(self, mc: ManagedCluster) -> None: + """Ensure the app monitoring profile exists on the managed cluster.""" + self._ensure_azure_monitor_profile(mc) + if mc.azure_monitor_profile.app_monitoring is None: + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring() + ) + + def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: + """Set up Azure Monitor logs on the Azure Monitor profile.""" + workspace_resource_id = self.context.raw_param.get("workspace_resource_id") + if not workspace_resource_id: + workspace_resource_id = self.context.external_functions.ensure_default_log_analytics_workspace_for_monitoring( # pylint: disable=line-too-long + self.cmd, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + ) + workspace_resource_id = "/" + workspace_resource_id.strip(" /") + + # Write the Azure Monitor profile rather than the legacy omsagent addon. The AMP path is + # managed identity only, so no auth mode is recorded here. + container_insights = self._ensure_container_insights(mc) + container_insights.enabled = True + container_insights.log_analytics_workspace_resource_id = workspace_resource_id + + container_network_logs_enabled = self.context.get_container_network_logs(mc) + if container_network_logs_enabled is not None: + container_insights.container_network_logs = ( + CONST_CONTAINER_NETWORK_LOGS_ENABLED + if container_network_logs_enabled + else CONST_CONTAINER_NETWORK_LOGS_DISABLED + ) + + _apply_container_insights_settings( + container_insights, + self.context.get_syslog_port(), + self.context.get_disable_prometheus_metrics_scraping(), + ) + + # DCR and DCRA creation is deferred to postprocessing_after_mc_created so that all flags + # are finalized and the cluster exists. + self.context.set_intermediate("monitoring_addon_enabled", True, overwrite_exists=True) + + def _setup_opentelemetry_metrics(self, mc: ManagedCluster) -> None: + """Set up the OpenTelemetry metrics configuration.""" + self._ensure_app_monitoring_profile(mc) + + otlp_metrics_config = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics(enabled=True) + ) + metrics_port_http = self.context.get_opentelemetry_metrics_port_http() + if metrics_port_http is not None: + otlp_metrics_config.http_port = metrics_port_http + metrics_port_grpc = self.context.get_opentelemetry_metrics_port_grpc() + if metrics_port_grpc is not None: + otlp_metrics_config.grpc_port = metrics_port_grpc + + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics = otlp_metrics_config + + def _disable_opentelemetry_metrics(self, mc: ManagedCluster) -> None: + """Disable the OpenTelemetry metrics configuration.""" + self._ensure_app_monitoring_profile(mc) + if mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics is None: + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics(enabled=False) + ) + else: + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled = False + # Clear the ports when disabling OpenTelemetry metrics + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.grpc_port = None + + def _setup_opentelemetry_logs_traces(self, mc: ManagedCluster) -> None: + """Set up the OpenTelemetry logs and traces configuration.""" + self._ensure_app_monitoring_profile(mc) + + otel_logs_cls = self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces + otlp_logs_config = otel_logs_cls(enabled=True) + logs_port_http = self.context.get_opentelemetry_logs_traces_port_http() + if logs_port_http is not None: + otlp_logs_config.http_port = logs_port_http + logs_port_grpc = self.context.get_opentelemetry_logs_traces_port_grpc() + if logs_port_grpc is not None: + otlp_logs_config.grpc_port = logs_port_grpc + + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces = otlp_logs_config + + def _disable_opentelemetry_logs_traces(self, mc: ManagedCluster) -> None: + """Disable the OpenTelemetry logs and traces configuration.""" + self._ensure_app_monitoring_profile(mc) + otel_logs_cls = self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces + if mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces is None: + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces = otel_logs_cls(enabled=False) + else: + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled = False + # Clear the ports when disabling OpenTelemetry logs and traces + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.grpc_port = None + def set_up_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: """Set up azure monitor profile for the ManagedCluster object. :return: the ManagedCluster object @@ -7891,6 +8528,14 @@ def set_up_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: mc.azure_monitor_profile.app_monitoring.auto_instrumentation = ( self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation(enabled=True) ) + if self.context.get_enable_opentelemetry_metrics(): + self._setup_opentelemetry_metrics(mc) + if self.context.get_disable_opentelemetry_metrics(): + self._disable_opentelemetry_metrics(mc) + if self.context.get_enable_opentelemetry_logs_traces(): + self._setup_opentelemetry_logs_traces(mc) + if self.context.get_disable_opentelemetry_logs_traces(): + self._disable_opentelemetry_logs_traces(mc) return mc def set_up_ingress_web_app_routing(self, mc: ManagedCluster) -> ManagedCluster: @@ -8244,6 +8889,60 @@ def immediate_processing_after_request(self, mc: ManagedCluster) -> None: "Could not create a role assignment for subnet. Are you an Owner on this subscription?" ) + def _postprocess_monitoring(self, cluster: ManagedCluster) -> None: + """Provision the monitoring data path after the cluster is created.""" + # Azure Monitor logs onboarded through the Azure Monitor profile is managed identity only, + # and unlike the legacy addon path it has no build-time DCR creation step, so the data + # collection rule is created here alongside the association. + azure_monitor_logs_enabled = self.context.get_enable_azure_monitor_logs() + enable_msi_auth_for_monitoring = self.context.get_enable_msi_auth_for_monitoring() + + if not enable_msi_auth_for_monitoring: + # add cluster spn/msi Monitoring Metrics Publisher role assignment to publish metrics to MDM + # mdm metrics is supported only in azure public cloud, so add the role assignment only in this cloud + if self.cmd.cli_ctx.cloud.name.lower() == "azurecloud": + from azure.mgmt.core.tools import resource_id + + cluster_resource_id = resource_id( + subscription=self.context.get_subscription_id(), + resource_group=self.context.get_resource_group_name(), + namespace="Microsoft.ContainerService", + type="managedClusters", + name=self.context.get_name(), + ) + self.context.external_functions.add_monitoring_role_assignment( + cluster, cluster_resource_id, self.cmd + ) + return + + if not azure_monitor_logs_enabled and self.context.raw_param.get("enable_addons") is None: + return + + # Create the DCR Association here. Drive provisioning off whichever profile carries the + # workspace, so the legacy omsagent addon object is not required to exist. + addon_consts = self.context.get_addon_consts() + monitoring_profile = _build_monitoring_addon_shim(cluster, self.models, addon_consts) + if monitoring_profile is None: + return + + self.context.external_functions.ensure_container_insights_for_monitoring( + self.cmd, + monitoring_profile, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.get_location(), + remove_monitoring=False, + aad_route=enable_msi_auth_for_monitoring, + create_dcr=azure_monitor_logs_enabled, + create_dcra=True, + enable_syslog=self.context.get_enable_syslog(), + data_collection_settings=self.context.get_data_collection_settings(), + is_private_cluster=self.context.get_enable_private_cluster(), + ampls_resource_id=self.context.get_ampls_resource_id(), + enable_high_log_scale_mode=self.context.get_enable_high_log_scale_mode(), + ) + # pylint: disable=too-many-locals def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: """Postprocessing performed after the cluster is created. @@ -8253,45 +8952,7 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: # monitoring addon monitoring_addon_enabled = self.context.get_intermediate("monitoring_addon_enabled", default_value=False) if monitoring_addon_enabled: - enable_msi_auth_for_monitoring = self.context.get_enable_msi_auth_for_monitoring() - if not enable_msi_auth_for_monitoring: - # add cluster spn/msi Monitoring Metrics Publisher role assignment to publish metrics to MDM - # mdm metrics is supported only in azure public cloud, so add the role assignment only in this cloud - cloud_name = self.cmd.cli_ctx.cloud.name - if cloud_name.lower() == "azurecloud": - from azure.mgmt.core.tools import resource_id - - cluster_resource_id = resource_id( - subscription=self.context.get_subscription_id(), - resource_group=self.context.get_resource_group_name(), - namespace="Microsoft.ContainerService", - type="managedClusters", - name=self.context.get_name(), - ) - self.context.external_functions.add_monitoring_role_assignment( - cluster, cluster_resource_id, self.cmd - ) - elif self.context.raw_param.get("enable_addons") is not None: - # Create the DCR Association here - addon_consts = self.context.get_addon_consts() - monitoring_addon_key = _get_monitoring_addon_key_from_consts(cluster.addon_profiles, addon_consts) - self.context.external_functions.ensure_container_insights_for_monitoring( - self.cmd, - cluster.addon_profiles[monitoring_addon_key], - self.context.get_subscription_id(), - self.context.get_resource_group_name(), - self.context.get_name(), - self.context.get_location(), - remove_monitoring=False, - aad_route=self.context.get_enable_msi_auth_for_monitoring(), - create_dcr=False, - create_dcra=True, - enable_syslog=self.context.get_enable_syslog(), - data_collection_settings=self.context.get_data_collection_settings(), - is_private_cluster=self.context.get_enable_private_cluster(), - ampls_resource_id=self.context.get_ampls_resource_id(), - enable_high_log_scale_mode=self.context.get_enable_high_log_scale_mode(), - ) + self._postprocess_monitoring(cluster) # ingress appgw addon ingress_appgw_addon_enabled = self.context.get_intermediate("ingress_appgw_addon_enabled", default_value=False) @@ -9159,8 +9820,6 @@ def update_monitoring_profile_flow_logs(self, mc: ManagedCluster) -> ManagedClus self._ensure_mc(mc) addon_consts = self.context.get_addon_consts() - CONST_MONITORING_USING_AAD_MSI_AUTH = addon_consts.get("CONST_MONITORING_USING_AAD_MSI_AUTH") - monitoring_addon_key = _get_monitoring_addon_key_from_consts(mc.addon_profiles, addon_consts) enable_high_log_scale_mode = self.context.get_enable_high_log_scale_mode() enable_cnl = self.context.raw_param.get("enable_container_network_logs") @@ -9173,41 +9832,43 @@ def update_monitoring_profile_flow_logs(self, mc: ManagedCluster) -> ManagedClus # Validate HLSM on the update path if enable_high_log_scale_mode is True and not enable_cnl: - # HLSM requires monitoring addon with MSI auth to be enabled - monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) if mc.addon_profiles else None - if ( - not monitoring_addon_profile or - not monitoring_addon_profile.enabled or - safe_lower( - (monitoring_addon_profile.config or {}).get(CONST_MONITORING_USING_AAD_MSI_AUTH) - ) != "true" - ): + # HLSM requires Azure Monitor logs to be enabled, either through the AMP + # containerInsights profile or the legacy omsagent addon, or by this same command. + monitoring_being_enabled = bool(self.context.raw_param.get("enable_azure_monitor_logs")) + if not monitoring_being_enabled and not _is_monitoring_enabled_on_mc(mc, addon_consts): raise RequiredArgumentMissingError( - "--enable-high-log-scale-mode requires the monitoring addon to be enabled with MSI auth " - "(useAADAuth=true). Please enable the monitoring addon with --enable-addons monitoring first." + "--enable-high-log-scale-mode requires Azure Monitor logs to be enabled on the " + "cluster. Please enable it first with --enable-azure-monitor-logs or " + "--enable-addons monitoring." + ) + # High log scale mode needs a data collection rule, which only exists for managed + # identity authentication. The auth mode must be read off the omsagent addon: the RP + # mirrors legacy shared key clusters into the AMP profile, so an enabled + # containerInsights profile does not by itself mean managed identity is in use. + if not _is_monitoring_aad_auth(mc, addon_consts): + raise RequiredArgumentMissingError( + "--enable-high-log-scale-mode requires MSI authentication to be enabled for the " + "monitoring addon. Please enable it with --enable-msi-auth-for-monitoring." ) if enable_high_log_scale_mode is False: # Check if CNL is already enabled on the cluster — cannot disable HLSM while CNL is active - monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) if mc.addon_profiles else None - if monitoring_addon_profile and monitoring_addon_profile.config: - existing_cnl = safe_lower( - monitoring_addon_profile.config.get("enableRetinaNetworkFlags") + if _is_container_network_logs_enabled_on_mc(mc, addon_consts): + raise MutuallyExclusiveArgumentError( + "Cannot disable --enable-high-log-scale-mode while container network logs are enabled. " + "Please disable container network logs first with --disable-container-network-logs." ) - if existing_cnl == "true": - raise MutuallyExclusiveArgumentError( - "Cannot disable --enable-high-log-scale-mode while container network logs are enabled. " - "Please disable container network logs first with --disable-container-network-logs." - ) container_network_logs_enabled = self.context.get_container_network_logs(mc) if container_network_logs_enabled is not None: - if mc.addon_profiles: - monitoring_addon_profile = mc.addon_profiles.get(monitoring_addon_key) - if monitoring_addon_profile: - config = monitoring_addon_profile.config or {} - config["enableRetinaNetworkFlags"] = str(container_network_logs_enabled) - mc.addon_profiles[monitoring_addon_key].config = config + # Written on the AMP profile rather than the legacy omsagent config key. This runs in + # addition to _setup_azure_monitor_logs because either may execute first depending on + # the order the base class invokes them; both write the same value. + self._ensure_container_insights(mc).container_network_logs = ( + CONST_CONTAINER_NETWORK_LOGS_ENABLED + if container_network_logs_enabled + else CONST_CONTAINER_NETWORK_LOGS_DISABLED + ) # When CNL or HLSM flags are provided, mark that monitoring postprocessing is needed # so the DCR gets updated with the correct streams @@ -10037,6 +10698,251 @@ def update_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: mc.azure_monitor_profile.app_monitoring.auto_instrumentation = ( self.models.ManagedClusterAzureMonitorProfileAppMonitoringAutoInstrumentation(enabled=False) ) + + if self.context.get_enable_opentelemetry_metrics(): + self._setup_opentelemetry_metrics(mc) + if self.context.get_disable_opentelemetry_metrics(): + self._disable_opentelemetry_metrics(mc) + if self.context.get_enable_opentelemetry_logs_traces(): + self._setup_opentelemetry_logs_traces(mc) + if self.context.get_disable_opentelemetry_logs_traces(): + self._disable_opentelemetry_logs_traces(mc) + return mc + + def _ensure_azure_monitor_profile(self, mc: ManagedCluster) -> None: + """Ensure the azure monitor profile exists on the managed cluster.""" + if mc.azure_monitor_profile is None: + mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() + + def _ensure_container_insights(self, mc: ManagedCluster): + """Ensure the AMP containerInsights profile exists and return it.""" + self._ensure_azure_monitor_profile(mc) + if mc.azure_monitor_profile.container_insights is None: + mc.azure_monitor_profile.container_insights = ( + self.models.ManagedClusterAzureMonitorProfileContainerInsights() + ) + return mc.azure_monitor_profile.container_insights + + def _ensure_app_monitoring_profile(self, mc: ManagedCluster) -> None: + """Ensure the app monitoring profile exists on the managed cluster.""" + self._ensure_azure_monitor_profile(mc) + if mc.azure_monitor_profile.app_monitoring is None: + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring() + ) + + def _setup_opentelemetry_metrics(self, mc: ManagedCluster) -> None: + """Set up the OpenTelemetry metrics configuration.""" + self._ensure_app_monitoring_profile(mc) + + otlp_metrics_config = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics(enabled=True) + ) + metrics_port_http = self.context.get_opentelemetry_metrics_port_http() + if metrics_port_http is not None: + otlp_metrics_config.http_port = metrics_port_http + metrics_port_grpc = self.context.get_opentelemetry_metrics_port_grpc() + if metrics_port_grpc is not None: + otlp_metrics_config.grpc_port = metrics_port_grpc + + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics = otlp_metrics_config + + def _disable_opentelemetry_metrics(self, mc: ManagedCluster) -> None: + """Disable the OpenTelemetry metrics configuration.""" + self._ensure_app_monitoring_profile(mc) + if mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics is None: + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics(enabled=False) + ) + else: + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled = False + # Clear the ports when disabling OpenTelemetry metrics + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.grpc_port = None + + def _setup_opentelemetry_logs_traces(self, mc: ManagedCluster) -> None: + """Set up the OpenTelemetry logs and traces configuration.""" + self._ensure_app_monitoring_profile(mc) + + otel_logs_cls = self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces + otlp_logs_config = otel_logs_cls(enabled=True) + logs_port_http = self.context.get_opentelemetry_logs_traces_port_http() + if logs_port_http is not None: + otlp_logs_config.http_port = logs_port_http + logs_port_grpc = self.context.get_opentelemetry_logs_traces_port_grpc() + if logs_port_grpc is not None: + otlp_logs_config.grpc_port = logs_port_grpc + + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces = otlp_logs_config + + def _disable_opentelemetry_logs_traces(self, mc: ManagedCluster) -> None: + """Disable the OpenTelemetry logs and traces configuration.""" + self._ensure_app_monitoring_profile(mc) + otel_logs_cls = self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces + if mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces is None: + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces = otel_logs_cls(enabled=False) + else: + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled = False + # Clear the ports when disabling OpenTelemetry logs and traces + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.grpc_port = None + + def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: + """Set up Azure Monitor logs on the Azure Monitor profile.""" + addon_consts = self.context.get_addon_consts() + CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID = addon_consts.get( + "CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID" + ) + + # --enable-azure-monitor-logs onboards through the Azure Monitor profile, which is managed + # identity only. A cluster already onboarded with legacy (shared key) authentication keeps + # that authentication mode on the server side, so this flag cannot be honoured as asked. + # Reject it up front, before a default workspace is created, rather than silently leaving + # the cluster on legacy auth. + if not _is_monitoring_aad_auth(mc, addon_consts): + raise ArgumentUsageError( + "Azure Monitor logs is already enabled on this cluster using legacy " + "(non-managed-identity) authentication. '--enable-azure-monitor-logs' requires " + "managed identity authentication. Migrate the cluster to managed identity " + "authentication first, then retry. See " + "https://learn.microsoft.com/en-us/azure/azure-monitor/containers/" + "container-insights-authentication?tabs=cli#migrate-to-managed-identity-authentication" + ) + + workspace_resource_id = self.context.raw_param.get("workspace_resource_id") + if not workspace_resource_id: + workspace_resource_id = self.context.external_functions.ensure_default_log_analytics_workspace_for_monitoring( # pylint: disable=line-too-long + self.cmd, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + ) + workspace_resource_id = "/" + workspace_resource_id.strip(" /") + + # Detect a workspace change so the DCR destination gets rewritten in postprocessing. The + # previous workspace may live on the AMP profile or, for clusters onboarded before the AMP + # switch, on the legacy addon. + container_insights = self._ensure_container_insights(mc) + old_workspace = container_insights.log_analytics_workspace_resource_id or "" + if not old_workspace: + addon_profile = _get_monitoring_addon_profile(mc, addon_consts) + if addon_profile: + old_workspace = _get_addon_config_value( + addon_profile.config, CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID + ) or "" + if old_workspace and old_workspace.lower() != workspace_resource_id.lower(): + self.context.set_intermediate( + "monitoring_addon_postprocessing_required", True, overwrite_exists=True + ) + + # Write the Azure Monitor profile rather than the legacy omsagent addon. No auth mode is + # recorded here: the RP derives it, defaulting new onboardings (and re-enables of a disabled + # addon) to managed identity. + container_insights.enabled = True + container_insights.log_analytics_workspace_resource_id = workspace_resource_id + + # Container network logs are applied here as well as in update_monitoring_profile_flow_logs, + # because that method may run before this one depending on the order the base class invokes + # them. Both write the same value, so the result is order-independent. + container_network_logs_enabled = self.context.get_container_network_logs(mc) + if container_network_logs_enabled is not None: + container_insights.container_network_logs = ( + CONST_CONTAINER_NETWORK_LOGS_ENABLED + if container_network_logs_enabled + else CONST_CONTAINER_NETWORK_LOGS_DISABLED + ) + + _apply_container_insights_settings( + container_insights, + self.context.get_syslog_port(), + self.context.get_disable_prometheus_metrics_scraping(), + ) + + self.context.set_intermediate("monitoring_addon_enabled", True, overwrite_exists=True) + self.context.set_intermediate( + "monitoring_addon_postprocessing_required", True, overwrite_exists=True + ) + + def _disable_azure_monitor_logs(self, mc: ManagedCluster) -> None: + """Disable Azure Monitor logs on the Azure Monitor profile.""" + addon_consts = self.context.get_addon_consts() + + # Azure Monitor logs may be on through the AMP profile, or through the legacy addon on + # clusters onboarded before the AMP switch. Absence of the addon must not short-circuit + # the disable. + if not _is_monitoring_enabled_on_mc(mc, addon_consts): + return + + # Perform DCR/DCRA cleanup BEFORE disabling, the same way aks_disable_addons does. Only + # managed identity clusters have a DCR/DCRA to clean up, so decide from local state first + # to avoid an ARM round trip when there is nothing to do. The auth mode has to come from + # the omsagent addon: the RP mirrors legacy shared key clusters into the AMP profile, so + # the presence of that profile says nothing about how the agent authenticates. + if _is_monitoring_aad_auth(mc, addon_consts): + current_cluster = self.client.get(self.context.get_resource_group_name(), self.context.get_name()) + monitoring_profile = _build_monitoring_addon_shim(current_cluster, self.models, addon_consts) + + if monitoring_profile and monitoring_profile.enabled: + try: + self.context.external_functions.ensure_container_insights_for_monitoring( + self.cmd, + monitoring_profile, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + self.context.get_name(), + current_cluster.location, + remove_monitoring=True, + aad_route=_is_monitoring_aad_auth(current_cluster, addon_consts), + create_dcr=False, + create_dcra=True, + enable_syslog=False, + data_collection_settings=None, + is_private_cluster=False, + ampls_resource_id=None, + enable_high_log_scale_mode=False, + ) + except TypeError: + # Ignore TypeError just like aks_disable_addons does + pass + + # Disable through the AMP profile. The RP keeps the legacy addon in sync, so the addon + # object is intentionally left untouched here. + container_insights = self._ensure_container_insights(mc) + container_insights.enabled = False + # Reset container network logs so a later re-enable does not silently carry them forward. + container_insights.container_network_logs = CONST_CONTAINER_NETWORK_LOGS_DISABLED + + def update_azure_monitor_logs(self, mc: ManagedCluster) -> ManagedCluster: + """Update Azure Monitor logs (Container Insights) for the ManagedCluster object. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + if self.context.get_enable_azure_monitor_logs(): + self._setup_azure_monitor_logs(mc) + if self.context.get_disable_azure_monitor_logs(): + self._disable_azure_monitor_logs(mc) + return mc + + def update_azure_monitor_logs_settings(self, mc: ManagedCluster) -> ManagedCluster: + """Update the AMP containerInsights tuning settings for the ManagedCluster object. + + These flags are independent of --enable-azure-monitor-logs, so they also apply to a cluster + where Azure Monitor logs is already enabled. When neither flag is given nothing is touched, + which keeps the rest of the monitoring configuration intact. + + :return: the ManagedCluster object + """ + self._ensure_mc(mc) + + syslog_port = self.context.get_syslog_port() + disable_prometheus_scraping = self.context.get_disable_prometheus_metrics_scraping() + if syslog_port is None and disable_prometheus_scraping is None: + return mc + + _apply_container_insights_settings( + self._ensure_container_insights(mc), syslog_port, disable_prometheus_scraping + ) return mc # pylint: disable=too-many-statements,too-many-locals @@ -10698,6 +11604,10 @@ def update_mc_profile_default(self) -> ManagedCluster: mc = self.update_k8s_support_plan(mc) # update azure monitor metrics profile mc = self.update_azure_monitor_profile(mc) + # update azure monitor logs (container insights) enablement + mc = self.update_azure_monitor_logs(mc) + # update azure monitor logs (container insights) settings + mc = self.update_azure_monitor_logs_settings(mc) # update azure container storage mc = self.update_azure_container_storage(mc) # update cluster upgrade settings @@ -10837,24 +11747,27 @@ def postprocessing_after_mc_created(self, cluster: ManagedCluster) -> None: enable_msi_auth_for_monitoring and self.context.raw_param.get("enable_addons") is not None ) or monitoring_addon_postprocessing_required: addon_consts = self.context.get_addon_consts() - monitoring_addon_key = _get_monitoring_addon_key_from_consts(cluster.addon_profiles, addon_consts) - self.context.external_functions.ensure_container_insights_for_monitoring( - self.cmd, - cluster.addon_profiles[monitoring_addon_key], - self.context.get_subscription_id(), - self.context.get_resource_group_name(), - self.context.get_name(), - self.context.get_location(), - remove_monitoring=False, - aad_route=True, - create_dcr=monitoring_addon_postprocessing_required, - create_dcra=enable_msi_auth_for_monitoring, - enable_syslog=self.context.get_enable_syslog(), - data_collection_settings=self.context.get_data_collection_settings(), - is_private_cluster=self.context.get_enable_private_cluster(), - ampls_resource_id=self.context.get_ampls_resource_id(), - enable_high_log_scale_mode=self.context.get_enable_high_log_scale_mode(), - ) + # Drive provisioning off whichever profile carries the workspace, so the legacy + # omsagent addon object is not required to exist. + monitoring_profile = _build_monitoring_addon_shim(cluster, self.models, addon_consts) + if monitoring_profile: + self.context.external_functions.ensure_container_insights_for_monitoring( + self.cmd, + monitoring_profile, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + self.context.get_name(), + self.context.get_location(), + remove_monitoring=False, + aad_route=True, + create_dcr=monitoring_addon_postprocessing_required, + create_dcra=enable_msi_auth_for_monitoring, + enable_syslog=self.context.get_enable_syslog(), + data_collection_settings=self.context.get_data_collection_settings(), + is_private_cluster=self.context.get_enable_private_cluster(), + ampls_resource_id=self.context.get_ampls_resource_id(), + enable_high_log_scale_mode=self.context.get_enable_high_log_scale_mode(), + ) # ingress appgw addon ingress_appgw_addon_enabled = self.context.get_intermediate("ingress_appgw_addon_enabled", default_value=False) diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py index 41efe797a22..a6c7d7d826f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_aks_commands.py @@ -14566,7 +14566,7 @@ def test_aks_create_acns_with_flow_logs( self.check("provisioningState", "Succeeded"), self.check("networkProfile.advancedNetworking.observability.enabled", True), self.check("addonProfiles.omsagent.enabled", True), - self.check("addonProfiles.omsagent.config.enableRetinaNetworkFlags", "True"), + self.check("azureMonitorProfile.containerInsights.containerNetworkLogs", "Enabled"), ], ) @@ -14587,7 +14587,7 @@ def test_aks_create_acns_with_flow_logs( ], ) self._wait_for_cluster_property( - "addonProfiles.omsagent.config.enableRetinaNetworkFlags", "False" + "azureMonitorProfile.containerInsights.containerNetworkLogs", "Disabled" ) # update: enable high log scale mode independently via aks update @@ -14613,7 +14613,7 @@ def test_aks_create_acns_with_flow_logs( ], ) self._wait_for_cluster_property( - "addonProfiles.omsagent.config.enableRetinaNetworkFlags", "True" + "azureMonitorProfile.containerInsights.containerNetworkLogs", "Enabled" ) # delete diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py index 2e12429ac29..37b957be96f 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_custom.py @@ -22,6 +22,7 @@ from azure.cli.command_modules.acs.addonconfiguration import ( _create_or_update_dcr_with_table_readiness_retry, ensure_default_log_analytics_workspace_for_monitoring, + warn_on_legacy_monitoring_auth, ) from azure.cli.command_modules.acs.custom import ( _get_command_context, @@ -1924,5 +1925,41 @@ def test_other_errors_keep_three_attempt_limit(self): self.mock_sleep.assert_not_called() +class TestWarnOnLegacyMonitoringAuth(unittest.TestCase): + def _warn_mock(self): + return mock.patch("azure.cli.command_modules.acs.addonconfiguration.logger.warning") + + def test_warns_for_explicit_false_with_monitoring_addon(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(False, "monitoring") + warn.assert_called_once() + self.assertIn("legacy shared key authentication", warn.call_args[0][0]) + + def test_warns_when_monitoring_is_one_of_several_addons(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(False, "monitoring,virtual-node") + warn.assert_called_once() + + def test_no_warning_when_msi_auth_is_true(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(True, "monitoring") + warn.assert_not_called() + + def test_no_warning_when_msi_auth_is_not_specified(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(None, "monitoring") + warn.assert_not_called() + + def test_no_warning_without_monitoring_addon(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(False, "virtual-node") + warn.assert_not_called() + + def test_no_warning_without_addons(self): + with self._warn_mock() as warn: + warn_on_legacy_monitoring_auth(False, None) + warn.assert_not_called() + + if __name__ == "__main__": unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index ba118f12ae8..10513ecfed8 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -4,6 +4,8 @@ # -------------------------------------------------------------------------------------------- import importlib +import os +import tempfile import unittest from unittest import mock from unittest.mock import Mock, call, patch, ANY @@ -36,6 +38,8 @@ CONST_VIRTUAL_NODE_ADDON_NAME, CONST_VIRTUAL_NODE_SUBNET_NAME, CONST_MONITORING_USING_AAD_MSI_AUTH, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, + CONST_CONTAINER_NETWORK_LOGS_DISABLED, CONST_LOAD_BALANCER_SKU_STANDARD, CONST_LOAD_BALANCER_SKU_BASIC, CONST_APP_ROUTING_ISTIO_MODE_ENABLED, @@ -16448,6 +16452,7 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsagent": self.models.ManagedClusterAddonProfile( enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, ) @@ -16468,9 +16473,14 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsagent": self.models.ManagedClusterAddonProfile( enabled=True, - config={"enableRetinaNetworkFlags": "True"} + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ), + ), ) self.assertEqual(dec_mc_1, ground_truth_mc_1) @@ -16498,9 +16508,14 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsagent": self.models.ManagedClusterAddonProfile( enabled=True, - config={"enableRetinaNetworkFlags": "True"} + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ), + ), ) dec_2.context.attach_mc(mc_2) dec_mc_2 = dec_2.update_monitoring_profile_flow_logs(mc_2) @@ -16519,9 +16534,14 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsagent": self.models.ManagedClusterAddonProfile( enabled=True, - config={"enableRetinaNetworkFlags": "False"} + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_DISABLED, + ), + ), ) self.assertEqual(dec_mc_2, ground_truth_mc_2) @@ -16592,11 +16612,14 @@ def test_enable_container_network_logs(self): config={ CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID: "/test_workspace_resource_id", CONST_MONITORING_USING_AAD_MSI_AUTH: "true", - "enableRetinaNetworkFlags": "True", }, ), } self.assertEqual(dec_mc_4.addon_profiles["omsagent"], ground_truth_mc_4["omsagent"]) + self.assertEqual( + dec_mc_4.azure_monitor_profile.container_insights.container_network_logs, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ) # Case 5: enable_acns and enable_container_network_logs without monitoring addon dec_5 = AKSManagedClusterCreateDecorator( @@ -16764,18 +16787,15 @@ def test_enable_container_network_logs(self): ) dec_9.context.attach_mc(mc_9) dec_mc_9 = dec_9.update_monitoring_profile_flow_logs(mc_9) - # HLSM should be enabled but CNL remains unset — no enableRetinaNetworkFlags change + # HLSM should be enabled but CNL remains unset — no containerNetworkLogs change # The monitoring_addon_postprocessing_required intermediate should be set self.assertTrue( dec_9.context.get_intermediate("monitoring_addon_postprocessing_required") ) # Verify HLSM is resolved to True self.assertEqual(dec_9.context.get_enable_high_log_scale_mode(), True) - # Verify CNL flag was NOT added to addon config (HLSM alone doesn't set it) - self.assertNotIn( - "enableRetinaNetworkFlags", - dec_mc_9.addon_profiles["omsagent"].config or {}, - ) + # Verify CNL was NOT set on the Azure Monitor profile (HLSM alone doesn't set it) + self.assertIsNone(dec_mc_9.azure_monitor_profile) # Case 10: UPDATE - disable HLSM while CNL is active -> should ERROR dec_10 = AKSManagedClusterUpdateDecorator( @@ -16799,12 +16819,14 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsagent": self.models.ManagedClusterAddonProfile( enabled=True, - config={ - CONST_MONITORING_USING_AAD_MSI_AUTH: "true", - "enableRetinaNetworkFlags": "True", - }, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ), + ), ) dec_10.context.attach_mc(mc_10) with self.assertRaises(MutuallyExclusiveArgumentError): @@ -16840,8 +16862,8 @@ def test_enable_container_network_logs(self): dec_11.context.attach_mc(mc_11) dec_mc_11 = dec_11.update_monitoring_profile_flow_logs(mc_11) self.assertEqual( - dec_mc_11.addon_profiles["omsagent"].config["enableRetinaNetworkFlags"], - "True", + dec_mc_11.azure_monitor_profile.container_insights.container_network_logs, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, ) self.assertTrue( dec_11.context.get_intermediate("monitoring_addon_postprocessing_required") @@ -16996,8 +17018,8 @@ def test_enable_container_network_logs(self): dec_16.context.attach_mc(mc_16) dec_mc_16 = dec_16.update_monitoring_profile_flow_logs(mc_16) self.assertEqual( - dec_mc_16.addon_profiles[CONST_MONITORING_ADDON_NAME].config["enableRetinaNetworkFlags"], - "True", + dec_mc_16.azure_monitor_profile.container_insights.container_network_logs, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, ) self.assertTrue( dec_16.context.get_intermediate("monitoring_addon_postprocessing_required") @@ -17025,12 +17047,14 @@ def test_enable_container_network_logs(self): addon_profiles={ "omsAgent": self.models.ManagedClusterAddonProfile( enabled=True, - config={ - CONST_MONITORING_USING_AAD_MSI_AUTH: "true", - "enableRetinaNetworkFlags": "True", - }, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "true"}, ) }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ), + ), ) dec_17.context.attach_mc(mc_17) with self.assertRaises(MutuallyExclusiveArgumentError): @@ -17421,5 +17445,695 @@ def test_update_upstream_kubescheduler_user_configuration(self): self.assertEqual(dec_mc_5, ground_truth_mc_5) +class AKSAzureMonitorLogsCreateTestCase(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSManagedClusterModels(self.cmd, ResourceType.MGMT_CONTAINERSERVICE) + self.client = MockClient() + + def _create_dec(self, raw_param): + params = { + "name": "test_name", + "resource_group_name": "test_rg_name", + "location": "test_location", + } + params.update(raw_param) + dec = AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + params, + ResourceType.MGMT_CONTAINERSERVICE, + ) + dec.context.set_intermediate("subscription_id", "test_subscription_id") + return dec + + def test_enable_azure_monitor_logs_sets_container_insights(self): + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_azure_monitor_logs(mc) + + container_insights = mc.azure_monitor_profile.container_insights + self.assertTrue(container_insights.enabled) + self.assertEqual( + container_insights.log_analytics_workspace_resource_id, "/test_workspace_resource_id" + ) + self.assertTrue(dec.context.get_intermediate("monitoring_addon_enabled")) + # No legacy addon profile is written for the Azure Monitor profile path + self.assertIsNone(mc.addon_profiles) + + def test_enable_azure_monitor_logs_creates_default_workspace(self): + dec = self._create_dec({"enable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_default_log_analytics_workspace_for_monitoring", + return_value="test_default_workspace_resource_id", + ): + dec._setup_azure_monitor_logs(mc) + self.assertEqual( + mc.azure_monitor_profile.container_insights.log_analytics_workspace_resource_id, + "/test_default_workspace_resource_id", + ) + + def test_enable_azure_monitor_logs_applies_settings(self): + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + "syslog_port": 28330, + "disable_prometheus_metrics_scraping": True, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_azure_monitor_logs(mc) + + container_insights = mc.azure_monitor_profile.container_insights + self.assertEqual(container_insights.syslog_port, 28330) + self.assertTrue(container_insights.disable_prometheus_metrics_scraping) + + def test_settings_without_enable_azure_monitor_logs_errors(self): + dec = self._create_dec({"syslog_port": 28330}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.context.get_syslog_port() + + def test_syslog_port_out_of_range_errors(self): + dec = self._create_dec({"enable_azure_monitor_logs": True, "syslog_port": 70000}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.context.get_syslog_port() + + def test_enable_azure_monitor_logs_with_explicit_legacy_auth_errors(self): + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "enable_msi_auth_for_monitoring": False, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_azure_monitor_logs() + + def test_enable_azure_monitor_logs_forces_msi_auth(self): + dec = self._create_dec({"enable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + self.assertTrue(dec.context.get_enable_msi_auth_for_monitoring()) + + def test_set_up_addon_profiles_authors_no_omsagent_addon(self): + # R1: --enable-azure-monitor-logs must configure the Azure Monitor profile only. + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec_mc = dec.set_up_addon_profiles(mc) + + self.assertTrue(dec_mc.azure_monitor_profile.container_insights.enabled) + self.assertEqual( + dec_mc.azure_monitor_profile.container_insights.log_analytics_workspace_resource_id, + "/test_workspace_resource_id", + ) + self.assertNotIn(CONST_MONITORING_ADDON_NAME, dec_mc.addon_profiles or {}) + self.assertTrue(dec.context.get_intermediate("monitoring_addon_enabled")) + + def test_postprocessing_provisions_dcr_and_dcra_from_amp_profile(self): + # R1 parity: DCR/DCE/DCRA/AMPLS provisioning must be driven off the Azure Monitor profile + # and must not require the omsagent addon to exist. + with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as settings_file: + settings_file.write('{"interval": "1m"}') + settings_path = settings_file.name + self.addCleanup(os.unlink, settings_path) + + ampls_resource_id = ( + "/subscriptions/1234-5678/resourceGroups/test_rg_name/providers/" + "microsoft.insights/privatelinkscopes/test_ampls" + ) + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + "enable_syslog": True, + "data_collection_settings": settings_path, + "ampls_resource_id": ampls_resource_id, + "enable_high_log_scale_mode": True, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec.set_up_addon_profiles(mc) + + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ) as ensure_mock: + dec.postprocessing_after_mc_created(mc) + + ensure_mock.assert_called_once() + kwargs = ensure_mock.call_args.kwargs + self.assertTrue(kwargs["aad_route"]) + self.assertTrue(kwargs["create_dcr"]) + self.assertTrue(kwargs["create_dcra"]) + self.assertTrue(kwargs["enable_syslog"]) + self.assertEqual(kwargs["data_collection_settings"], settings_path) + self.assertEqual(kwargs["ampls_resource_id"], ampls_resource_id) + self.assertTrue(kwargs["enable_high_log_scale_mode"]) + # The addon shim is synthesized from the Azure Monitor profile, not the omsagent addon + addon_shim = ensure_mock.call_args.args[1] + self.assertTrue(addon_shim.enabled) + self.assertEqual( + addon_shim.config[CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID], + "/test_workspace_resource_id", + ) + self.assertEqual(addon_shim.config[CONST_MONITORING_USING_AAD_MSI_AUTH], "true") + + def test_container_network_logs_written_to_azure_monitor_profile(self): + # R1: --enable-container-network-logs maps to containerInsights.containerNetworkLogs + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + "enable_acns": True, + "network_dataplane": "cilium", + "enable_container_network_logs": True, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec_mc = dec.set_up_addon_profiles(mc) + + self.assertEqual( + dec_mc.azure_monitor_profile.container_insights.container_network_logs, + CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ) + self.assertNotIn(CONST_MONITORING_ADDON_NAME, dec_mc.addon_profiles or {}) + + def test_enable_prometheus_scraping_leaves_field_false(self): + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + "enable_prometheus_metrics_scraping": True, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_azure_monitor_logs(mc) + self.assertFalse( + mc.azure_monitor_profile.container_insights.disable_prometheus_metrics_scraping + ) + + +class AKSAzureMonitorLogsUpdateTestCase(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSManagedClusterModels(self.cmd, ResourceType.MGMT_CONTAINERSERVICE) + self.client = MockClient() + + def _update_dec(self, raw_param): + params = { + "name": "test_name", + "resource_group_name": "test_rg_name", + } + params.update(raw_param) + dec = AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + params, + ResourceType.MGMT_CONTAINERSERVICE, + ) + dec.context.set_intermediate("subscription_id", "test_subscription_id") + return dec + + def test_enable_azure_monitor_logs_on_fresh_cluster(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs(mc) + + container_insights = dec_mc.azure_monitor_profile.container_insights + self.assertTrue(container_insights.enabled) + self.assertEqual( + container_insights.log_analytics_workspace_resource_id, "/test_workspace_resource_id" + ) + self.assertTrue(dec.context.get_intermediate("monitoring_addon_postprocessing_required")) + + def test_enable_azure_monitor_logs_on_legacy_auth_cluster_errors(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "false"}, + ) + }, + ) + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError): + dec.update_azure_monitor_logs(mc) + + def test_enable_azure_monitor_logs_on_disabled_legacy_addon_is_allowed(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: self.models.ManagedClusterAddonProfile(enabled=False) + }, + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs(mc) + self.assertTrue(dec_mc.azure_monitor_profile.container_insights.enabled) + + def test_enable_azure_monitor_logs_detects_workspace_change(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "/new_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/old_workspace_resource_id", + ), + ), + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs(mc) + self.assertTrue(dec.context.get_intermediate("monitoring_addon_postprocessing_required")) + self.assertEqual( + dec_mc.azure_monitor_profile.container_insights.log_analytics_workspace_resource_id, + "/new_workspace_resource_id", + ) + + def test_disable_azure_monitor_logs(self): + dec = self._update_dec({"disable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/test_workspace_resource_id", + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + ), + ), + ) + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ) as ensure_mock: + dec_mc = dec.update_azure_monitor_logs(mc) + + ensure_mock.assert_called_once() + self.assertEqual(ensure_mock.call_args.kwargs["remove_monitoring"], True) + self.assertEqual(ensure_mock.call_args.kwargs["aad_route"], True) + self.assertFalse(dec_mc.azure_monitor_profile.container_insights.enabled) + self.assertEqual( + dec_mc.azure_monitor_profile.container_insights.container_network_logs, + CONST_CONTAINER_NETWORK_LOGS_DISABLED, + ) + + def test_disable_azure_monitor_logs_noop_when_not_enabled(self): + dec = self._update_dec({"disable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ) as ensure_mock: + dec_mc = dec.update_azure_monitor_logs(mc) + ensure_mock.assert_not_called() + self.assertIsNone(dec_mc.azure_monitor_profile) + + def test_enable_and_disable_azure_monitor_logs_errors(self): + dec = self._update_dec( + {"enable_azure_monitor_logs": True, "disable_azure_monitor_logs": True} + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_azure_monitor_logs() + + def test_enable_azure_monitor_logs_with_explicit_msi_auth_flag_errors(self): + dec = self._update_dec( + {"enable_azure_monitor_logs": True, "enable_msi_auth_for_monitoring": True} + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_azure_monitor_logs() + + def test_update_settings_on_already_enabled_cluster(self): + dec = self._update_dec({"syslog_port": 28331}) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + ), + ), + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs_settings(mc) + self.assertEqual(dec_mc.azure_monitor_profile.container_insights.syslog_port, 28331) + + def test_update_settings_on_legacy_addon_cluster(self): + dec = self._update_dec({"disable_prometheus_metrics_scraping": True}) + mc = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: self.models.ManagedClusterAddonProfile(enabled=True) + }, + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs_settings(mc) + self.assertTrue( + dec_mc.azure_monitor_profile.container_insights.disable_prometheus_metrics_scraping + ) + + def test_update_settings_without_monitoring_errors(self): + dec = self._update_dec({"syslog_port": 28331}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.update_azure_monitor_logs_settings(mc) + + def test_update_settings_noop_when_no_flags(self): + dec = self._update_dec({}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_logs_settings(mc) + self.assertIsNone(dec_mc.azure_monitor_profile) + + +class AKSOpenTelemetryCreateTestCase(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSManagedClusterModels(self.cmd, ResourceType.MGMT_CONTAINERSERVICE) + self.client = MockClient() + + def _create_dec(self, raw_param): + params = { + "name": "test_name", + "resource_group_name": "test_rg_name", + "location": "test_location", + } + params.update(raw_param) + return AKSManagedClusterCreateDecorator( + self.cmd, + self.client, + params, + ResourceType.MGMT_CONTAINERSERVICE, + ) + + def test_enable_opentelemetry_metrics_with_ports(self): + dec = self._create_dec( + { + "enable_managed_identity": True, + "enable_azure_monitor_metrics": True, + "enable_opentelemetry_metrics": True, + "opentelemetry_metrics_port_http": 4318, + "opentelemetry_metrics_port_grpc": 4317, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_opentelemetry_metrics(mc) + + otlp = mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics + self.assertTrue(otlp.enabled) + self.assertEqual(otlp.http_port, 4318) + self.assertEqual(otlp.grpc_port, 4317) + + def test_enable_opentelemetry_logs_traces_with_ports(self): + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "enable_opentelemetry_logs_traces": True, + "opentelemetry_logs_traces_port_http": 4320, + "opentelemetry_logs_traces_port_grpc": 4319, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_opentelemetry_logs_traces(mc) + + otlp = mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces + self.assertTrue(otlp.enabled) + self.assertEqual(otlp.http_port, 4320) + self.assertEqual(otlp.grpc_port, 4319) + + def test_opentelemetry_metrics_without_azure_monitor_metrics_errors(self): + dec = self._create_dec({"enable_opentelemetry_metrics": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError): + dec.context.get_enable_opentelemetry_metrics() + + def test_opentelemetry_logs_traces_without_azure_monitor_logs_errors(self): + dec = self._create_dec({"enable_opentelemetry_logs_traces": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError): + dec.context.get_enable_opentelemetry_logs_traces() + + def test_opentelemetry_logs_traces_with_monitoring_addon_allowed(self): + dec = self._create_dec( + {"enable_addons": "monitoring", "enable_opentelemetry_logs_traces": True} + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + self.assertTrue(dec.context.get_enable_opentelemetry_logs_traces()) + + def test_opentelemetry_metrics_port_out_of_range_errors(self): + dec = self._create_dec( + { + "enable_managed_identity": True, + "enable_azure_monitor_metrics": True, + "enable_opentelemetry_metrics": True, + "opentelemetry_metrics_port_http": 70000, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.context.get_opentelemetry_metrics_port_http() + + def test_opentelemetry_metrics_port_without_enable_errors(self): + dec = self._create_dec({"opentelemetry_metrics_port_http": 4318}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.context.get_opentelemetry_metrics_port_http() + + def test_enable_and_disable_opentelemetry_metrics_errors(self): + dec = self._create_dec( + { + "enable_managed_identity": True, + "enable_azure_monitor_metrics": True, + "enable_opentelemetry_metrics": True, + "disable_opentelemetry_metrics": True, + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(MutuallyExclusiveArgumentError): + dec.context.get_enable_opentelemetry_metrics() + + def test_set_up_azure_monitor_profile_wires_opentelemetry(self): + dec = self._create_dec( + { + "enable_managed_identity": True, + "enable_azure_monitor_metrics": True, + "enable_opentelemetry_metrics": True, + "opentelemetry_metrics_port_grpc": 4317, + "enable_azure_monitor_logs": True, + "enable_opentelemetry_logs_traces": True, + "opentelemetry_logs_traces_port_grpc": 4319, + "azure_monitor_workspace_resource_id": "test_workspace_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + identity=self.models.ManagedClusterIdentity(type="SystemAssigned"), + ) + dec.context.attach_mc(mc) + dec_mc = dec.set_up_azure_monitor_profile(mc) + + app_monitoring = dec_mc.azure_monitor_profile.app_monitoring + self.assertTrue(app_monitoring.open_telemetry_metrics.enabled) + self.assertEqual(app_monitoring.open_telemetry_metrics.grpc_port, 4317) + self.assertTrue(app_monitoring.open_telemetry_logs_and_traces.enabled) + self.assertEqual(app_monitoring.open_telemetry_logs_and_traces.grpc_port, 4319) + + +class AKSOpenTelemetryUpdateTestCase(unittest.TestCase): + def setUp(self): + self.cli_ctx = MockCLI() + self.cmd = MockCmd(self.cli_ctx) + self.models = AKSManagedClusterModels(self.cmd, ResourceType.MGMT_CONTAINERSERVICE) + self.client = MockClient() + + def _update_dec(self, raw_param): + params = { + "name": "test_name", + "resource_group_name": "test_rg_name", + } + params.update(raw_param) + return AKSManagedClusterUpdateDecorator( + self.cmd, + self.client, + params, + ResourceType.MGMT_CONTAINERSERVICE, + ) + + def _mc_with_monitoring(self): + return self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + metrics=self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=True), + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + ), + ), + ) + + def test_enable_opentelemetry_metrics_on_enabled_cluster(self): + dec = self._update_dec( + {"enable_opentelemetry_metrics": True, "opentelemetry_metrics_port_grpc": 4317} + ) + mc = self._mc_with_monitoring() + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_profile(mc) + + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics + self.assertTrue(otlp.enabled) + self.assertEqual(otlp.grpc_port, 4317) + + def test_enable_opentelemetry_logs_traces_on_enabled_cluster(self): + dec = self._update_dec( + {"enable_opentelemetry_logs_traces": True, "opentelemetry_logs_traces_port_http": 4320} + ) + mc = self._mc_with_monitoring() + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_profile(mc) + + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces + self.assertTrue(otlp.enabled) + self.assertEqual(otlp.http_port, 4320) + + def test_disable_opentelemetry_metrics_clears_ports(self): + dec = self._update_dec({"disable_opentelemetry_metrics": True}) + mc = self._mc_with_monitoring() + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring( + open_telemetry_metrics=( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics( + enabled=True, http_port=4318, grpc_port=4317 + ) + ), + ) + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_profile(mc) + + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics + self.assertFalse(otlp.enabled) + self.assertIsNone(otlp.http_port) + self.assertIsNone(otlp.grpc_port) + + def test_disable_opentelemetry_logs_traces_clears_ports(self): + dec = self._update_dec({"disable_opentelemetry_logs_traces": True}) + mc = self._mc_with_monitoring() + otel_logs_cls = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces + ) + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring( + open_telemetry_logs_and_traces=otel_logs_cls( + enabled=True, http_port=4320, grpc_port=4319 + ), + ) + ) + dec.context.attach_mc(mc) + dec_mc = dec.update_azure_monitor_profile(mc) + + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces + self.assertFalse(otlp.enabled) + self.assertIsNone(otlp.http_port) + self.assertIsNone(otlp.grpc_port) + + def test_enable_opentelemetry_metrics_without_metrics_enabled_errors(self): + dec = self._update_dec({"enable_opentelemetry_metrics": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError): + dec.context.get_enable_opentelemetry_metrics() + + def test_enable_opentelemetry_logs_traces_without_logs_enabled_errors(self): + dec = self._update_dec({"enable_opentelemetry_logs_traces": True}) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError): + dec.context.get_enable_opentelemetry_logs_traces() + + def test_enable_opentelemetry_logs_traces_with_enable_azure_monitor_logs(self): + dec = self._update_dec( + {"enable_azure_monitor_logs": True, "enable_opentelemetry_logs_traces": True} + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + self.assertTrue(dec.context.get_enable_opentelemetry_logs_traces()) + + def test_opentelemetry_port_out_of_range_errors(self): + dec = self._update_dec( + { + "enable_opentelemetry_metrics": True, + "opentelemetry_metrics_port_grpc": 70000, + } + ) + mc = self._mc_with_monitoring() + dec.context.attach_mc(mc) + with self.assertRaises(InvalidArgumentValueError): + dec.update_azure_monitor_profile(mc) + + if __name__ == "__main__": unittest.main() diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py index 6f5421bc3bf..8cdc538ea6d 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_validators.py @@ -2046,5 +2046,241 @@ def test_no_ssh_key_still_skips(self): self.assertIsNone(namespace.ssh_key_value) +class TestAzureMonitorLogsValidators(unittest.TestCase): + def test_enable_azure_monitor_logs_with_monitoring_addon_errors(self): + namespace = SimpleNamespace(enable_azure_monitor_logs=True, enable_addons="monitoring") + with self.assertRaises(ArgumentUsageError): + validators.validate_azure_monitor_logs_and_enable_addons(namespace) + + def test_enable_azure_monitor_logs_with_other_addon_passes(self): + namespace = SimpleNamespace(enable_azure_monitor_logs=True, enable_addons="http_application_routing") + validators.validate_azure_monitor_logs_and_enable_addons(namespace) + + def test_monitoring_addon_without_azure_monitor_logs_passes(self): + namespace = SimpleNamespace(enable_azure_monitor_logs=False, enable_addons="monitoring") + validators.validate_azure_monitor_logs_and_enable_addons(namespace) + + def test_enable_and_disable_azure_monitor_logs_errors(self): + namespace = SimpleNamespace(enable_azure_monitor_logs=True, disable_azure_monitor_logs=True) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_azure_monitor_logs_enable_disable(namespace) + + def test_only_enable_azure_monitor_logs_passes(self): + namespace = SimpleNamespace( + enable_azure_monitor_logs=True, + disable_azure_monitor_logs=False, + enable_msi_auth_for_monitoring=None, + ) + validators.validate_azure_monitor_logs_enable_disable(namespace) + + def test_create_explicit_legacy_msi_auth_flag_errors(self): + namespace = SimpleNamespace( + enable_azure_monitor_logs=True, + enable_addons=None, + enable_msi_auth_for_monitoring=False, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_azure_monitor_logs_and_enable_addons(namespace) + + def test_create_default_msi_auth_flag_passes(self): + # On create the flag defaults to True, which is indistinguishable from explicit use. + namespace = SimpleNamespace( + enable_azure_monitor_logs=True, + enable_addons=None, + enable_msi_auth_for_monitoring=True, + ) + validators.validate_azure_monitor_logs_and_enable_addons(namespace) + + def test_update_explicit_msi_auth_flag_errors(self): + for value in (True, False): + namespace = SimpleNamespace( + enable_azure_monitor_logs=True, + disable_azure_monitor_logs=False, + enable_msi_auth_for_monitoring=value, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_azure_monitor_logs_enable_disable(namespace) + + def test_msi_auth_flag_without_azure_monitor_logs_passes(self): + namespace = SimpleNamespace( + enable_azure_monitor_logs=False, + disable_azure_monitor_logs=False, + enable_msi_auth_for_monitoring=False, + ) + validators.validate_azure_monitor_logs_enable_disable(namespace) + + +def _ci_settings_namespace(**kwargs): + defaults = { + "syslog_port": None, + "enable_prometheus_metrics_scraping": False, + "disable_prometheus_metrics_scraping": False, + "enable_azure_monitor_logs": False, + "disable_azure_monitor_logs": False, + } + defaults.update(kwargs) + return SimpleNamespace(**defaults) + + +class TestContainerInsightsSettingsValidators(unittest.TestCase): + def test_enable_and_disable_prometheus_scraping_errors(self): + namespace = _ci_settings_namespace( + enable_prometheus_metrics_scraping=True, + disable_prometheus_metrics_scraping=True, + enable_azure_monitor_logs=True, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_container_insights_settings_for_create(namespace) + + def test_syslog_port_out_of_range_errors(self): + namespace = _ci_settings_namespace(syslog_port=70000, enable_azure_monitor_logs=True) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_container_insights_settings_for_create(namespace) + + def test_syslog_port_zero_errors(self): + namespace = _ci_settings_namespace(syslog_port=0, enable_azure_monitor_logs=True) + with self.assertRaises(InvalidArgumentValueError): + validators.validate_container_insights_settings_for_create(namespace) + + def test_settings_without_enable_azure_monitor_logs_errors_on_create(self): + namespace = _ci_settings_namespace(syslog_port=28330) + with self.assertRaises(ArgumentUsageError): + validators.validate_container_insights_settings_for_create(namespace) + + def test_settings_with_enable_azure_monitor_logs_passes_on_create(self): + namespace = _ci_settings_namespace( + syslog_port=28330, + enable_prometheus_metrics_scraping=True, + enable_azure_monitor_logs=True, + ) + validators.validate_container_insights_settings_for_create(namespace) + + def test_settings_with_disable_azure_monitor_logs_errors(self): + namespace = _ci_settings_namespace(syslog_port=28330, disable_azure_monitor_logs=True) + with self.assertRaises(ArgumentUsageError): + validators.validate_container_insights_settings_for_update(namespace) + + def test_settings_on_update_defer_enablement_check(self): + namespace = _ci_settings_namespace(syslog_port=28330) + validators.validate_container_insights_settings_for_update(namespace) + + def test_no_settings_passes(self): + validators.validate_container_insights_settings_for_create(_ci_settings_namespace()) + + +def _otel_namespace(**kwargs): + defaults = { + "enable_opentelemetry_metrics": False, + "disable_opentelemetry_metrics": False, + "opentelemetry_metrics_port_http": None, + "opentelemetry_metrics_port_grpc": None, + "enable_opentelemetry_logs_traces": False, + "disable_opentelemetry_logs_traces": False, + "opentelemetry_logs_traces_port_http": None, + "opentelemetry_logs_traces_port_grpc": None, + "enable_azure_monitor_metrics": False, + "enable_azure_monitor_logs": False, + "enable_addons": None, + } + defaults.update(kwargs) + return SimpleNamespace(**defaults) + + +class TestOpenTelemetryValidators(unittest.TestCase): + def test_port_out_of_range_errors(self): + namespace = _otel_namespace(opentelemetry_metrics_port_http=70000) + with self.assertRaises(ArgumentUsageError): + validators.validate_opentelemetry_ports(namespace) + + def test_duplicate_ports_error(self): + namespace = _otel_namespace( + opentelemetry_metrics_port_http=4318, + opentelemetry_logs_traces_port_http=4318, + ) + with self.assertRaises(ArgumentUsageError): + validators.validate_opentelemetry_ports(namespace) + + def test_distinct_ports_pass(self): + namespace = _otel_namespace( + opentelemetry_metrics_port_http=4318, + opentelemetry_metrics_port_grpc=4317, + opentelemetry_logs_traces_port_http=4320, + opentelemetry_logs_traces_port_grpc=4319, + ) + validators.validate_opentelemetry_ports(namespace) + + def test_enable_and_disable_otel_metrics_errors(self): + namespace = _otel_namespace( + enable_opentelemetry_metrics=True, + disable_opentelemetry_metrics=True, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_opentelemetry_metrics_dependencies(namespace) + + def test_otel_metrics_without_azure_monitor_metrics_errors_on_create(self): + namespace = _otel_namespace(enable_opentelemetry_metrics=True) + with self.assertRaises(ArgumentUsageError): + validators.validate_opentelemetry_metrics_dependencies(namespace) + + def test_otel_metrics_with_azure_monitor_metrics_passes_on_create(self): + namespace = _otel_namespace( + enable_opentelemetry_metrics=True, + enable_azure_monitor_metrics=True, + ) + validators.validate_opentelemetry_metrics_dependencies(namespace) + + def test_otel_metrics_on_update_defers_dependency_check(self): + namespace = _otel_namespace(enable_opentelemetry_metrics=True) + validators.validate_opentelemetry_metrics_dependencies_for_update(namespace) + + def test_enable_and_disable_otel_logs_traces_errors(self): + namespace = _otel_namespace( + enable_opentelemetry_logs_traces=True, + disable_opentelemetry_logs_traces=True, + ) + with self.assertRaises(MutuallyExclusiveArgumentError): + validators.validate_opentelemetry_logs_traces_dependencies(namespace) + + def test_otel_logs_traces_without_azure_monitor_logs_errors_on_create(self): + namespace = _otel_namespace(enable_opentelemetry_logs_traces=True) + with self.assertRaises(ArgumentUsageError): + validators.validate_opentelemetry_logs_traces_dependencies(namespace) + + def test_otel_logs_traces_with_azure_monitor_logs_passes_on_create(self): + namespace = _otel_namespace( + enable_opentelemetry_logs_traces=True, + enable_azure_monitor_logs=True, + ) + validators.validate_opentelemetry_logs_traces_dependencies(namespace) + + def test_otel_logs_traces_with_monitoring_addon_passes_on_create(self): + namespace = _otel_namespace( + enable_opentelemetry_logs_traces=True, + enable_addons="monitoring", + ) + validators.validate_opentelemetry_logs_traces_dependencies(namespace) + + def test_otel_logs_traces_on_update_defers_dependency_check(self): + namespace = _otel_namespace(enable_opentelemetry_logs_traces=True) + validators.validate_opentelemetry_logs_traces_dependencies_for_update(namespace) + + def test_aggregate_create_validator_runs_all_checks(self): + namespace = _otel_namespace( + enable_opentelemetry_metrics=True, + enable_azure_monitor_metrics=True, + opentelemetry_metrics_port_http=4318, + opentelemetry_metrics_port_grpc=4318, + ) + with self.assertRaises(ArgumentUsageError): + validators.validate_azure_monitor_and_opentelemetry_for_create(namespace) + + def test_aggregate_update_validator_passes_for_valid_namespace(self): + namespace = _otel_namespace( + enable_opentelemetry_metrics=True, + opentelemetry_metrics_port_grpc=4317, + ) + validators.validate_azure_monitor_and_opentelemetry_for_update(namespace) + + if __name__ == "__main__": unittest.main() From e01014e0c7b07b883a11d8c5ac82e0b6e0863ad5 Mon Sep 17 00:00:00 2001 From: Sunil Yadav Date: Thu, 17 Sep 2026 02:40:25 +0000 Subject: [PATCH 2/2] Create dcr/dcra for logs --- linter_exclusions.yml | 75 +++ src/azure-cli/HISTORY.rst | 6 + .../azure/cli/command_modules/acs/_consts.py | 2 + .../azure/cli/command_modules/acs/_help.py | 10 + .../acs/managed_cluster_decorator.py | 232 +++++++- .../latest/test_managed_cluster_decorator.py | 561 +++++++++++++++++- 6 files changed, 845 insertions(+), 41 deletions(-) diff --git a/linter_exclusions.yml b/linter_exclusions.yml index ccaa256820d..723306f2dbd 100644 --- a/linter_exclusions.yml +++ b/linter_exclusions.yml @@ -297,6 +297,39 @@ aks create: container_storage_version: rule_exclusions: - option_length_too_long + enable_azure_monitor_logs: + rule_exclusions: + - option_length_too_long + enable_prometheus_metrics_scraping: + rule_exclusions: + - option_length_too_long + disable_prometheus_metrics_scraping: + rule_exclusions: + - option_length_too_long + enable_opentelemetry_metrics: + rule_exclusions: + - option_length_too_long + disable_opentelemetry_metrics: + rule_exclusions: + - option_length_too_long + opentelemetry_metrics_port_http: + rule_exclusions: + - option_length_too_long + opentelemetry_metrics_port_grpc: + rule_exclusions: + - option_length_too_long + enable_opentelemetry_logs_traces: + rule_exclusions: + - option_length_too_long + disable_opentelemetry_logs_traces: + rule_exclusions: + - option_length_too_long + opentelemetry_logs_traces_port_http: + rule_exclusions: + - option_length_too_long + opentelemetry_logs_traces_port_grpc: + rule_exclusions: + - option_length_too_long aks enable-addons: parameters: workspace_resource_id: @@ -386,6 +419,48 @@ aks update: container_storage_version: rule_exclusions: - option_length_too_long + enable_azure_monitor_logs: + rule_exclusions: + - option_length_too_long + disable_azure_monitor_logs: + rule_exclusions: + - option_length_too_long + enable_msi_auth_for_monitoring: + rule_exclusions: + - option_length_too_long + data_collection_settings: + rule_exclusions: + - option_length_too_long + enable_prometheus_metrics_scraping: + rule_exclusions: + - option_length_too_long + disable_prometheus_metrics_scraping: + rule_exclusions: + - option_length_too_long + enable_opentelemetry_metrics: + rule_exclusions: + - option_length_too_long + disable_opentelemetry_metrics: + rule_exclusions: + - option_length_too_long + opentelemetry_metrics_port_http: + rule_exclusions: + - option_length_too_long + opentelemetry_metrics_port_grpc: + rule_exclusions: + - option_length_too_long + enable_opentelemetry_logs_traces: + rule_exclusions: + - option_length_too_long + disable_opentelemetry_logs_traces: + rule_exclusions: + - option_length_too_long + opentelemetry_logs_traces_port_http: + rule_exclusions: + - option_length_too_long + opentelemetry_logs_traces_port_grpc: + rule_exclusions: + - option_length_too_long aks update-credentials: parameters: aad_server_app_secret: diff --git a/src/azure-cli/HISTORY.rst b/src/azure-cli/HISTORY.rst index 22d2a03c0e8..1ec462bedfe 100644 --- a/src/azure-cli/HISTORY.rst +++ b/src/azure-cli/HISTORY.rst @@ -31,6 +31,12 @@ Release History * `az aks create`, `az aks update`: Add `--enable-opentelemetry-logs-traces`, `--disable-opentelemetry-logs-traces`, `--opentelemetry-logs-traces-port-http` and `--opentelemetry-logs-traces-port-grpc` for the OpenTelemetry logs and traces receiver * `az aks create`, `az aks update`: Write container network logs to `azureMonitorProfile.containerInsights.containerNetworkLogs` instead of the monitoring addon configuration, and reject `--enable-container-network-logs` on clusters using legacy shared key authentication * `az aks create`, `az aks update`, `az aks enable-addons`: Deprecate `--enable-msi-auth-for-monitoring` in favor of `--enable-azure-monitor-logs` +* `az aks create`, `az aks update`: Reject `--enable-azure-monitor-logs` on clusters using service principal authentication, since the Azure Monitor profile onboards with managed identity only +* `az aks update`: Reject `--enable-azure-monitor-logs` when Azure Monitor logs is already enabled on the cluster, matching `az aks enable-addons -a monitoring`. Run `--disable-azure-monitor-logs` first to change the configuration +* `az aks update`: `--disable-azure-monitor-logs` now removes the data collection rule association and resets the Container Insights settings (syslog port, Prometheus scraping and container network logs) back to their defaults, and asks for confirmation when OpenTelemetry logs and traces are enabled +* `az aks update`: Fix `--enable-azure-monitor-logs` not creating the data collection rule and association unless the Log Analytics workspace changed, which left the agent running with no data collection rule attached so no logs were ingested +* `az aks update`: Create the data collection rule and association before the cluster update when enabling with `--enable-azure-monitor-logs`, matching `az aks enable-addons -a monitoring`. Provisioning them afterwards meant the agent started before the association existed and then stayed idle for several minutes before restarting once the configuration arrived +* `az aks update`: `--disable-azure-monitor-metrics` now also disables OpenTelemetry metrics, since they are collected through the managed Prometheus pipeline, and asks for confirmation first unless `--yes` is specified **App Config** diff --git a/src/azure-cli/azure/cli/command_modules/acs/_consts.py b/src/azure-cli/azure/cli/command_modules/acs/_consts.py index 46e6ca8f2a8..0d1482bc8b0 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_consts.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_consts.py @@ -166,6 +166,8 @@ # legacy omsagent addon config key, superseded by containerNetworkLogs on the Azure Monitor # profile path. Only read, to keep recognizing clusters onboarded before the switch. CONST_MONITORING_ENABLE_RETINA_NETWORK_FLAGS = "enableRetinaNetworkFlags" +# server-side default for azureMonitorProfile.containerInsights.syslogPort +CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT = 28330 # virtual node CONST_VIRTUAL_NODE_ADDON_NAME = "aciConnector" diff --git a/src/azure-cli/azure/cli/command_modules/acs/_help.py b/src/azure-cli/azure/cli/command_modules/acs/_help.py index 685bfa18a28..b7fe426eec5 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/_help.py +++ b/src/azure-cli/azure/cli/command_modules/acs/_help.py @@ -336,6 +336,7 @@ long-summary: | Configures Container Insights through the cluster's Azure Monitor profile instead of the monitoring addon. Cannot be combined with "--enable-addons monitoring" or with "--enable-msi-auth-for-monitoring". + Requires the cluster to use a managed identity; clusters created with service principal authentication are not supported. - name: --syslog-port type: int short-summary: TCP port that the Azure Monitor agent listens on for syslog data. Requires --enable-azure-monitor-logs. @@ -1155,6 +1156,10 @@ - name: --disable-azure-monitor-metrics type: bool short-summary: Disable Azure Monitor Metrics Profile. This will delete all DCRA's associated with the cluster, any linked DCRs with the data stream = prometheus-stream and the recording rule groups created by the addon for this AKS cluster. + long-summary: | + If OpenTelemetry metrics are enabled, they are disabled as well, since they are collected + through the managed Prometheus pipeline. Confirmation is requested first unless "--yes" is + specified. - name: --enable-control-plane-metrics --enable-cp-metrics type: bool short-summary: Enable collection of Azure Monitor managed Prometheus control plane metrics for managed cluster components (controlplane-apiserver and controlplane-etcd targets by default). Requires Azure Monitor metrics to be enabled (already enabled or via --enable-azure-monitor-metrics). @@ -1173,9 +1178,14 @@ long-summary: | Configures Container Insights through the cluster's Azure Monitor profile instead of the monitoring addon. Clusters still using legacy shared key authentication must first migrate to managed identity authentication. + Requires the cluster to use a managed identity; clusters using service principal authentication are not supported. + Fails if Azure Monitor logs is already enabled on the cluster. To change the configuration, run "az aks update --disable-azure-monitor-logs" first. - name: --disable-azure-monitor-logs type: bool short-summary: Disable Azure Monitor logs (Container Insights) for the cluster. + long-summary: | + Disables Container Insights, removes the data collection rule association, and resets the Container Insights settings (syslog port, Prometheus scraping and container network logs) back to their defaults. The workspace is left recorded on the profile but is unused while disabled, and is replaced on the next enable. + If OpenTelemetry logs and traces are enabled they are disabled as well, and confirmation is requested first unless "--yes" is specified. - name: --workspace-resource-id type: string short-summary: The resource ID of an existing Log Analytics Workspace to use for storing monitoring data. If not specified, uses the default Log Analytics Workspace if it exists, otherwise creates one. diff --git a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py index 92d4d572549..30b01055e31 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/managed_cluster_decorator.py @@ -57,6 +57,7 @@ CONST_CONTAINER_NETWORK_LOGS_ENABLED, CONST_CONTAINER_NETWORK_LOGS_DISABLED, CONST_MONITORING_ENABLE_RETINA_NETWORK_FLAGS, + CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT, ) from azure.cli.command_modules.acs.azurecontainerstorage._consts import ( CONST_ACSTOR_EXT_INSTALLATION_NAME, @@ -216,6 +217,59 @@ def _apply_container_insights_settings(container_insights, syslog_port, disable_ container_insights.disable_prometheus_metrics_scraping = disable_prometheus_scraping +def _reset_container_insights_to_defaults(container_insights): + """Reset the AMP containerInsights settings back to their documented defaults. + + The RP copies a containerInsights field from the request onto the cluster only when the field + is present (see ``ApplyAzureMonitorProfileContainerInsights``), so leaving a field as ``None`` + preserves whatever the cluster already has. Disabling therefore has to write the defaults + explicitly, otherwise a later re-enable silently inherits the old syslog port, scraping choice + and container network logs setting. + + ``logAnalyticsWorkspaceResourceId`` is deliberately left alone. It is a resource-id typed + field, and blanking it makes the RP mirror the empty string into + ``addonProfiles.omsagent.config.logAnalyticsWorkspaceResourceID``; ARM then rejects every later + write of the cluster with ``LinkedInvalidPropertyId``, which would break unrelated + ``az aks update`` calls too. The stale id is inert once ``enabled`` is false, and the enable + path always overwrites it with a freshly resolved workspace, so nothing is inherited. + """ + container_insights.enabled = False + container_insights.syslog_port = CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT + container_insights.disable_prometheus_metrics_scraping = False + container_insights.container_network_logs = CONST_CONTAINER_NETWORK_LOGS_DISABLED + + +def _is_service_principal_cluster(mc): + """Whether the cluster authenticates to Azure with a service principal instead of an identity. + + Managed identity clusters report ``servicePrincipalProfile.clientId == "msi"``, so any other + non-empty client id means a real service principal. A missing profile means managed identity. + """ + service_principal_profile = getattr(mc, "service_principal_profile", None) if mc is not None else None + if service_principal_profile is None: + return False + client_id = getattr(service_principal_profile, "client_id", None) + if not client_id: + return False + return client_id.lower() != "msi" + + +def _raise_if_service_principal_cluster(mc): + """Reject --enable-azure-monitor-logs on a service principal cluster. + + The Azure Monitor profile has no shared key/``useAADAuth`` concept: the agent authenticates to + the Log Analytics workspace with the cluster's managed identity. A service principal cluster + has no such identity, so the onboarding cannot work and is rejected up front. + """ + if _is_service_principal_cluster(mc): + raise ArgumentUsageError( + "'--enable-azure-monitor-logs' cannot be used on clusters with service principal " + "authentication. Azure Monitor logs onboards through the Azure Monitor profile, " + "which requires the cluster to use a managed identity. Update the cluster to use a " + "managed identity with 'az aks update --enable-managed-identity', then retry." + ) + + def _get_addon_config_value(config, key): """Case-insensitive lookup of an addon config value. @@ -8396,6 +8450,11 @@ def _ensure_app_monitoring_profile(self, mc: ManagedCluster) -> None: def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: """Set up Azure Monitor logs on the Azure Monitor profile.""" + # The Azure Monitor profile is managed identity only, so a cluster created with a service + # principal can never authenticate to the workspace. Reject before a default workspace is + # created on the user's behalf. + _raise_if_service_principal_cluster(mc) + workspace_resource_id = self.context.raw_param.get("workspace_resource_id") if not workspace_resource_id: workspace_resource_id = self.context.external_functions.ensure_default_log_analytics_workspace_for_monitoring( # pylint: disable=line-too-long @@ -10635,9 +10694,7 @@ def update_azure_monitor_profile(self, mc: ManagedCluster) -> ManagedCluster: metric_annotations_allow_list=str(ksm_metric_annotations_allow_list)) if self.context.get_disable_azure_monitor_metrics(): - if mc.azure_monitor_profile is None: - mc.azure_monitor_profile = self.models.ManagedClusterAzureMonitorProfile() - mc.azure_monitor_profile.metrics = self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=False) + self._disable_azure_monitor_metrics(mc) if ( self.context.raw_param.get("enable_azure_monitor_metrics") or @@ -10790,15 +10847,17 @@ def _disable_opentelemetry_logs_traces(self, mc: ManagedCluster) -> None: def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: """Set up Azure Monitor logs on the Azure Monitor profile.""" addon_consts = self.context.get_addon_consts() - CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID = addon_consts.get( - "CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID" - ) + + # The Azure Monitor profile is managed identity only, so a service principal cluster can + # never authenticate to the workspace. Reject before a default workspace is created. + _raise_if_service_principal_cluster(mc) # --enable-azure-monitor-logs onboards through the Azure Monitor profile, which is managed # identity only. A cluster already onboarded with legacy (shared key) authentication keeps # that authentication mode on the server side, so this flag cannot be honoured as asked. # Reject it up front, before a default workspace is created, rather than silently leaving - # the cluster on legacy auth. + # the cluster on legacy auth. This is checked before the "already enabled" guard below so + # the more actionable migration message wins for a legacy-auth cluster. if not _is_monitoring_aad_auth(mc, addon_consts): raise ArgumentUsageError( "Azure Monitor logs is already enabled on this cluster using legacy " @@ -10809,6 +10868,17 @@ def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: "container-insights-authentication?tabs=cli#migrate-to-managed-identity-authentication" ) + # Re-onboarding an already onboarded cluster is rejected, matching the behaviour of + # 'az aks enable-addons -a monitoring'. Silently re-running would otherwise recreate the + # default workspace and re-provision DCR/DCRA artifacts for a cluster that is already set + # up, which hides configuration mistakes such as a mistyped --workspace-resource-id. + if _is_monitoring_enabled_on_mc(mc, addon_consts): + raise ArgumentUsageError( + "Azure Monitor logs is already enabled for this managed cluster.\n" + "To change the Azure Monitor logs configuration, run " + "'az aks update --disable-azure-monitor-logs' before enabling it again." + ) + workspace_resource_id = self.context.raw_param.get("workspace_resource_id") if not workspace_resource_id: workspace_resource_id = self.context.external_functions.ensure_default_log_analytics_workspace_for_monitoring( # pylint: disable=line-too-long @@ -10818,25 +10888,10 @@ def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: ) workspace_resource_id = "/" + workspace_resource_id.strip(" /") - # Detect a workspace change so the DCR destination gets rewritten in postprocessing. The - # previous workspace may live on the AMP profile or, for clusters onboarded before the AMP - # switch, on the legacy addon. - container_insights = self._ensure_container_insights(mc) - old_workspace = container_insights.log_analytics_workspace_resource_id or "" - if not old_workspace: - addon_profile = _get_monitoring_addon_profile(mc, addon_consts) - if addon_profile: - old_workspace = _get_addon_config_value( - addon_profile.config, CONST_MONITORING_LOG_ANALYTICS_WORKSPACE_RESOURCE_ID - ) or "" - if old_workspace and old_workspace.lower() != workspace_resource_id.lower(): - self.context.set_intermediate( - "monitoring_addon_postprocessing_required", True, overwrite_exists=True - ) - # Write the Azure Monitor profile rather than the legacy omsagent addon. No auth mode is # recorded here: the RP derives it, defaulting new onboardings (and re-enables of a disabled # addon) to managed identity. + container_insights = self._ensure_container_insights(mc) container_insights.enabled = True container_insights.log_analytics_workspace_resource_id = workspace_resource_id @@ -10857,9 +10912,60 @@ def _setup_azure_monitor_logs(self, mc: ManagedCluster) -> None: self.context.get_disable_prometheus_metrics_scraping(), ) + # Reaching here means a genuine onboarding: the guards above reject service principal + # clusters, legacy-auth clusters and clusters that are already enabled. The DCR and the + # DCRA therefore always have to be provisioned, otherwise the agent is deployed with no + # data collection rule attached and no logs are ever ingested. Provisioned once the profile + # above is fully built, so the DCR reflects the final shape. + self._provision_azure_monitor_logs_dcr(mc, addon_consts) + + # monitoring_addon_postprocessing_required is deliberately not set: the DCR and the DCRA + # have already been provisioned above, and setting it would repeat the same work after the + # cluster PUT. self.context.set_intermediate("monitoring_addon_enabled", True, overwrite_exists=True) - self.context.set_intermediate( - "monitoring_addon_postprocessing_required", True, overwrite_exists=True + + def _provision_azure_monitor_logs_dcr(self, mc: ManagedCluster, addon_consts: dict) -> None: + """Create the DCR and the DCRA before the cluster PUT. + + 'az aks enable-addons -a monitoring' provisions these artifacts first and only then updates + the cluster, so by the time the RP rolls out the ama-logs DaemonSet the data collection + rule is already associated and mdsd downloads it within seconds. + + Deferring the work to postprocessing_after_mc_created inverts that order: the agent starts + before the DCRA exists, finds no configuration to download, and then backs off for several + minutes before retrying. The agent ingests nothing for the whole of that window and + restarts once the configuration finally arrives, because its liveness probe treats the + newly appeared DCR as a configuration change. Provisioning up front keeps the flag's + behaviour identical to the addon it replaces. + """ + monitoring_profile = _build_monitoring_addon_shim(mc, self.models, addon_consts) + if not (monitoring_profile and monitoring_profile.enabled): + return + + data_collection_settings = self.context.get_data_collection_settings() + # Oversized settings are dropped rather than sent, to avoid the DCR call failing with + # "Request Header Fields Too Large". + if data_collection_settings and len(str(data_collection_settings)) > 10000: + data_collection_settings = None + + self.context.external_functions.ensure_container_insights_for_monitoring( + self.cmd, + monitoring_profile, + self.context.get_subscription_id(), + self.context.get_resource_group_name(), + self.context.get_name(), + mc.location or self.context.get_location(), + remove_monitoring=False, + # The legacy-auth guard in _setup_azure_monitor_logs has already rejected anything that + # is not managed identity, so the AAD route is the only reachable one here. + aad_route=True, + create_dcr=True, + create_dcra=True, + enable_syslog=self.context.get_enable_syslog(), + data_collection_settings=data_collection_settings, + is_private_cluster=self.context.get_enable_private_cluster(), + ampls_resource_id=self.context.get_ampls_resource_id(), + enable_high_log_scale_mode=self.context.get_enable_high_log_scale_mode(), ) def _disable_azure_monitor_logs(self, mc: ManagedCluster) -> None: @@ -10872,6 +10978,24 @@ def _disable_azure_monitor_logs(self, mc: ManagedCluster) -> None: if not _is_monitoring_enabled_on_mc(mc, addon_consts): return + # OpenTelemetry logs and traces are collected by the Container Insights agent, so disabling + # Azure Monitor logs necessarily turns them off too. Confirm before doing that. + opentelemetry_logs_enabled = ( + mc.azure_monitor_profile and + mc.azure_monitor_profile.app_monitoring and + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces and + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled + ) + + if opentelemetry_logs_enabled and not self.context.get_yes(): + msg = ( + "OpenTelemetry logs and traces are enabled on this cluster and are collected by " + "Azure Monitor logs. Disabling Azure Monitor logs will also disable OpenTelemetry " + "logs and traces. Do you want to continue?" + ) + if not prompt_y_n(msg, default="n"): + raise DecoratorEarlyExitException() + # Perform DCR/DCRA cleanup BEFORE disabling, the same way aks_disable_addons does. Only # managed identity clusters have a DCR/DCRA to clean up, so decide from local state first # to avoid an ARM round trip when there is nothing to do. The auth mode has to come from @@ -10905,11 +11029,59 @@ def _disable_azure_monitor_logs(self, mc: ManagedCluster) -> None: pass # Disable through the AMP profile. The RP keeps the legacy addon in sync, so the addon - # object is intentionally left untouched here. - container_insights = self._ensure_container_insights(mc) - container_insights.enabled = False - # Reset container network logs so a later re-enable does not silently carry them forward. - container_insights.container_network_logs = CONST_CONTAINER_NETWORK_LOGS_DISABLED + # object is intentionally left untouched here. Every containerInsights field is reset to + # its default so a later --enable-azure-monitor-logs starts from a clean profile instead of + # silently inheriting the old syslog port, scraping choice or container network logs + # setting. + _reset_container_insights_to_defaults(self._ensure_container_insights(mc)) + + # OpenTelemetry logs and traces ride on the Container Insights agent, so they go down with + # it. The confirmation for this was taken above. + if opentelemetry_logs_enabled: + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled = False + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.grpc_port = None + + def _disable_azure_monitor_metrics(self, mc: ManagedCluster) -> None: + """Disable Azure Monitor metrics on the Azure Monitor profile.""" + azure_monitor_metrics_enabled = ( + mc.azure_monitor_profile and + mc.azure_monitor_profile.metrics and + mc.azure_monitor_profile.metrics.enabled + ) + + # Nothing to turn off, so the payload is left untouched rather than writing a redundant + # disabled metrics profile. Any leftover DCR/DCRA and recording rule cleanup still runs in + # update_azure_monitor_profile, which is driven by the raw flag rather than cluster state. + if not azure_monitor_metrics_enabled: + return + + # OpenTelemetry metrics are ingested through the managed Prometheus pipeline that Azure + # Monitor metrics sets up, so disabling the parent necessarily turns them off too. Confirm + # before doing that, mirroring the --disable-azure-monitor-logs behaviour. + opentelemetry_metrics_enabled = ( + mc.azure_monitor_profile.app_monitoring and + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics and + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled + ) + + if opentelemetry_metrics_enabled and not self.context.get_yes(): + msg = ( + "OpenTelemetry metrics are enabled on this cluster and are collected by Azure " + "Monitor metrics. Disabling Azure Monitor metrics will also disable OpenTelemetry " + "metrics. Do you want to continue?" + ) + if not prompt_y_n(msg, default="n"): + raise DecoratorEarlyExitException() + + mc.azure_monitor_profile.metrics = self.models.ManagedClusterAzureMonitorProfileMetrics(enabled=False) + + # OpenTelemetry metrics ride on the managed Prometheus pipeline, so they go down with it. + # The confirmation for this was taken above. + if opentelemetry_metrics_enabled: + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled = False + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.http_port = None + mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.grpc_port = None def update_azure_monitor_logs(self, mc: ManagedCluster) -> ManagedCluster: """Update Azure Monitor logs (Container Insights) for the ManagedCluster object. diff --git a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py index 10513ecfed8..ef037e6d861 100644 --- a/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py +++ b/src/azure-cli/azure/cli/command_modules/acs/tests/latest/test_managed_cluster_decorator.py @@ -40,6 +40,7 @@ CONST_MONITORING_USING_AAD_MSI_AUTH, CONST_CONTAINER_NETWORK_LOGS_ENABLED, CONST_CONTAINER_NETWORK_LOGS_DISABLED, + CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT, CONST_LOAD_BALANCER_SKU_STANDARD, CONST_LOAD_BALANCER_SKU_BASIC, CONST_APP_ROUTING_ISTIO_MODE_ENABLED, @@ -17503,6 +17504,63 @@ def test_enable_azure_monitor_logs_creates_default_workspace(self): "/test_default_workspace_resource_id", ) + def test_enable_azure_monitor_logs_rejected_on_service_principal_cluster(self): + """On create the service principal profile is populated from --service-principal before the + addon profiles are set up, so the rejection applies there too.""" + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="00000000-0000-0000-0000-000000000001", secret="secret" + ), + ) + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_default_log_analytics_workspace_for_monitoring", + ) as ensure_workspace_mock: + with self.assertRaises(ArgumentUsageError) as ctx: + dec._setup_azure_monitor_logs(mc) + + self.assertIn("service principal", str(ctx.exception)) + ensure_workspace_mock.assert_not_called() + + def test_enable_azure_monitor_logs_allowed_on_msi_cluster(self): + """Managed identity clusters report clientId 'msi'.""" + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="msi" + ), + ) + dec.context.attach_mc(mc) + dec._setup_azure_monitor_logs(mc) + self.assertTrue(mc.azure_monitor_profile.container_insights.enabled) + + def test_enable_azure_monitor_logs_allowed_without_service_principal_profile(self): + """A managed identity cluster may have no service principal profile at all.""" + dec = self._create_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + dec._setup_azure_monitor_logs(mc) + self.assertTrue(mc.azure_monitor_profile.container_insights.enabled) + def test_enable_azure_monitor_logs_applies_settings(self): dec = self._create_dec( { @@ -17690,14 +17748,58 @@ def test_enable_azure_monitor_logs_on_fresh_cluster(self): ) mc = self.models.ManagedCluster(location="test_location") dec.context.attach_mc(mc) - dec_mc = dec.update_azure_monitor_logs(mc) + with patch.object( + dec.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ) as ensure_mock: + dec_mc = dec.update_azure_monitor_logs(mc) container_insights = dec_mc.azure_monitor_profile.container_insights self.assertTrue(container_insights.enabled) self.assertEqual( container_insights.log_analytics_workspace_resource_id, "/test_workspace_resource_id" ) - self.assertTrue(dec.context.get_intermediate("monitoring_addon_postprocessing_required")) + # A fresh enable must provision the DCR and the DCRA, otherwise the agent is deployed with + # no data collection rule attached and nothing is ingested. + ensure_mock.assert_called_once() + self.assertTrue(ensure_mock.call_args.kwargs["create_dcr"]) + self.assertTrue(ensure_mock.call_args.kwargs["create_dcra"]) + # And not queued for postprocessing, which would repeat the same work after the PUT. + self.assertFalse( + dec.context.get_intermediate( + "monitoring_addon_postprocessing_required", default_value=False + ) + ) + + def test_enable_azure_monitor_logs_provisions_dcr_before_cluster_put(self): + """The DCR and the DCRA are provisioned while the profile is being built, not deferred to + postprocessing_after_mc_created. + + Postprocessing runs after the cluster PUT, so the RP has already rolled out the ama-logs + DaemonSet by the time the DCRA appears. The agent then starts with nothing to download, + backs off for several minutes before retrying, ingests nothing for that whole window and + restarts once the configuration finally lands. 'az aks enable-addons -a monitoring' creates + the artifacts before its PUT, and this flag has to match that ordering. + """ + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster(location="test_location") + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ) as ensure_mock: + dec.update_azure_monitor_logs(mc) + + ensure_mock.assert_called_once() + self.assertTrue(ensure_mock.call_args.kwargs["aad_route"]) + self.assertFalse(ensure_mock.call_args.kwargs["remove_monitoring"]) def test_enable_azure_monitor_logs_on_legacy_auth_cluster_errors(self): dec = self._update_dec( @@ -17733,10 +17835,17 @@ def test_enable_azure_monitor_logs_on_disabled_legacy_addon_is_allowed(self): }, ) dec.context.attach_mc(mc) - dec_mc = dec.update_azure_monitor_logs(mc) + with patch.object( + dec.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ): + dec_mc = dec.update_azure_monitor_logs(mc) self.assertTrue(dec_mc.azure_monitor_profile.container_insights.enabled) - def test_enable_azure_monitor_logs_detects_workspace_change(self): + def test_enable_azure_monitor_logs_rejected_when_already_enabled(self): + """Re-onboarding an already onboarded cluster is rejected, matching + 'az aks enable-addons -a monitoring'.""" dec = self._update_dec( { "enable_azure_monitor_logs": True, @@ -17753,12 +17862,150 @@ def test_enable_azure_monitor_logs_detects_workspace_change(self): ), ) dec.context.attach_mc(mc) - dec_mc = dec.update_azure_monitor_logs(mc) - self.assertTrue(dec.context.get_intermediate("monitoring_addon_postprocessing_required")) + with self.assertRaises(ArgumentUsageError) as ctx: + dec.update_azure_monitor_logs(mc) + + message = str(ctx.exception) + self.assertIn("already enabled for this managed cluster", message) + self.assertIn("--disable-azure-monitor-logs", message) + # the original workspace is left untouched self.assertEqual( - dec_mc.azure_monitor_profile.container_insights.log_analytics_workspace_resource_id, - "/new_workspace_resource_id", + mc.azure_monitor_profile.container_insights.log_analytics_workspace_resource_id, + "/old_workspace_resource_id", + ) + + def test_enable_azure_monitor_logs_rejected_before_provisioning_default_workspace(self): + """The already-enabled rejection must happen before a default workspace is created.""" + dec = self._update_dec({"enable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/old_workspace_resource_id", + ), + ), + ) + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_default_log_analytics_workspace_for_monitoring", + ) as ensure_workspace_mock: + with self.assertRaises(ArgumentUsageError): + dec.update_azure_monitor_logs(mc) + + ensure_workspace_mock.assert_not_called() + + def test_enable_azure_monitor_logs_allowed_when_container_insights_disabled(self): + """A disabled profile is a fresh onboarding, not a re-onboarding.""" + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "/new_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=False, + log_analytics_workspace_resource_id="/old_workspace_resource_id", + ), + ), + ) + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ) as ensure_mock: + dec_mc = dec.update_azure_monitor_logs(mc) + + container_insights = dec_mc.azure_monitor_profile.container_insights + self.assertTrue(container_insights.enabled) + self.assertEqual( + container_insights.log_analytics_workspace_resource_id, "/new_workspace_resource_id" + ) + # The DCR destination is rewritten up front, before the cluster PUT. + ensure_mock.assert_called_once() + self.assertTrue(ensure_mock.call_args.kwargs["create_dcr"]) + + def test_legacy_auth_rejection_wins_over_already_enabled_rejection(self): + """A legacy-auth cluster is also 'already enabled', but the migration message is the + actionable one, so it must be the error the user sees.""" + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + addon_profiles={ + CONST_MONITORING_ADDON_NAME: self.models.ManagedClusterAddonProfile( + enabled=True, + config={CONST_MONITORING_USING_AAD_MSI_AUTH: "false"}, + ) + }, + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + ), + ), + ) + dec.context.attach_mc(mc) + with self.assertRaises(ArgumentUsageError) as ctx: + dec.update_azure_monitor_logs(mc) + + self.assertIn("legacy", str(ctx.exception)) + + def test_enable_azure_monitor_logs_rejected_on_service_principal_cluster(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } ) + mc = self.models.ManagedCluster( + location="test_location", + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="00000000-0000-0000-0000-000000000001" + ), + ) + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_default_log_analytics_workspace_for_monitoring", + ) as ensure_workspace_mock: + with self.assertRaises(ArgumentUsageError) as ctx: + dec.update_azure_monitor_logs(mc) + + self.assertIn("service principal", str(ctx.exception)) + # rejected before any workspace is provisioned on the user's behalf + ensure_workspace_mock.assert_not_called() + + def test_enable_azure_monitor_logs_allowed_on_msi_cluster(self): + dec = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "test_workspace_resource_id", + } + ) + mc = self.models.ManagedCluster( + location="test_location", + service_principal_profile=self.models.ManagedClusterServicePrincipalProfile( + client_id="msi" + ), + ) + dec.context.attach_mc(mc) + with patch.object( + dec.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ): + dec_mc = dec.update_azure_monitor_logs(mc) + + self.assertTrue(dec_mc.azure_monitor_profile.container_insights.enabled) def test_disable_azure_monitor_logs(self): dec = self._update_dec({"disable_azure_monitor_logs": True}) @@ -17769,6 +18016,8 @@ def test_disable_azure_monitor_logs(self): enabled=True, log_analytics_workspace_resource_id="/test_workspace_resource_id", container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + syslog_port=29000, + disable_prometheus_metrics_scraping=True, ), ), ) @@ -17783,11 +18032,195 @@ def test_disable_azure_monitor_logs(self): ensure_mock.assert_called_once() self.assertEqual(ensure_mock.call_args.kwargs["remove_monitoring"], True) self.assertEqual(ensure_mock.call_args.kwargs["aad_route"], True) - self.assertFalse(dec_mc.azure_monitor_profile.container_insights.enabled) + + container_insights = dec_mc.azure_monitor_profile.container_insights + self.assertFalse(container_insights.enabled) + # Every behavioural field is reset, so a later re-enable starts from a clean profile. self.assertEqual( - dec_mc.azure_monitor_profile.container_insights.container_network_logs, - CONST_CONTAINER_NETWORK_LOGS_DISABLED, + container_insights.container_network_logs, CONST_CONTAINER_NETWORK_LOGS_DISABLED + ) + self.assertEqual( + container_insights.syslog_port, CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT + ) + self.assertFalse(container_insights.disable_prometheus_metrics_scraping) + # The workspace id is deliberately preserved: blanking it makes the RP mirror an empty + # string into the omsagent addon config, and ARM then rejects every later write of the + # cluster with LinkedInvalidPropertyId. + self.assertEqual( + container_insights.log_analytics_workspace_resource_id, "/test_workspace_resource_id" + ) + + def test_disable_azure_monitor_logs_emits_every_default_field(self): + """Every reset field must be present in the payload: the RP only overwrites fields it + receives, so a field left out would survive the disable.""" + dec = self._update_dec({"disable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/test_workspace_resource_id", + syslog_port=29000, + ), + ), + ) + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ): + dec_mc = dec.update_azure_monitor_logs(mc) + + payload = dict(dec_mc.azure_monitor_profile.container_insights) + for field in ( + "enabled", + "syslogPort", + "disablePrometheusMetricsScraping", + "containerNetworkLogs", + ): + self.assertIn(field, payload) + + def test_disable_azure_monitor_logs_reset_survives_reenable(self): + """After a disable, re-enabling starts from a clean profile.""" + dec_disable = self._update_dec({"disable_azure_monitor_logs": True}) + mc = self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/test_workspace_resource_id", + container_network_logs=CONST_CONTAINER_NETWORK_LOGS_ENABLED, + syslog_port=29000, + disable_prometheus_metrics_scraping=True, + ), + ), + ) + dec_disable.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ): + dec_disable.update_azure_monitor_logs(mc) + + dec_enable = self._update_dec( + { + "enable_azure_monitor_logs": True, + "workspace_resource_id": "/new_workspace_resource_id", + } + ) + dec_enable.context.attach_mc(mc) + with patch.object( + dec_enable.context.external_functions, + "ensure_container_insights_for_monitoring", + return_value=None, + ): + dec_enable.update_azure_monitor_logs(mc) + + container_insights = mc.azure_monitor_profile.container_insights + self.assertTrue(container_insights.enabled) + self.assertEqual( + container_insights.log_analytics_workspace_resource_id, "/new_workspace_resource_id" + ) + self.assertEqual( + container_insights.container_network_logs, CONST_CONTAINER_NETWORK_LOGS_DISABLED ) + self.assertEqual( + container_insights.syslog_port, CONST_CONTAINER_INSIGHTS_DEFAULT_SYSLOG_PORT + ) + self.assertFalse(container_insights.disable_prometheus_metrics_scraping) + + def _mc_with_otlp_logs_traces(self, enabled=True): + return self.models.ManagedCluster( + location="test_location", + azure_monitor_profile=self.models.ManagedClusterAzureMonitorProfile( + container_insights=self.models.ManagedClusterAzureMonitorProfileContainerInsights( + enabled=True, + log_analytics_workspace_resource_id="/test_workspace_resource_id", + ), + app_monitoring=self.models.ManagedClusterAzureMonitorProfileAppMonitoring( + open_telemetry_logs_and_traces=self.models. + ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryLogsAndTraces( + enabled=enabled, http_port=8080, grpc_port=8081 + ) + ), + ), + ) + + def test_disable_azure_monitor_logs_prompts_when_otlp_logs_and_traces_enabled(self): + dec = self._update_dec({"disable_azure_monitor_logs": True, "yes": False}) + mc = self._mc_with_otlp_logs_traces() + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ): + dec_mc = dec.update_azure_monitor_logs(mc) + + prompt_mock.assert_called_once() + self.assertIn("OpenTelemetry logs and traces", prompt_mock.call_args[0][0]) + self.assertFalse(dec_mc.azure_monitor_profile.container_insights.enabled) + otlp_logs = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces + self.assertFalse(otlp_logs.enabled) + self.assertIsNone(otlp_logs.http_port) + self.assertIsNone(otlp_logs.grpc_port) + + def test_disable_azure_monitor_logs_aborts_when_confirmation_declined(self): + dec = self._update_dec({"disable_azure_monitor_logs": True, "yes": False}) + mc = self._mc_with_otlp_logs_traces() + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=False + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ) as ensure_mock: + with self.assertRaises(DecoratorEarlyExitException): + dec.update_azure_monitor_logs(mc) + + # nothing was torn down and the profile is untouched + ensure_mock.assert_not_called() + self.assertTrue(mc.azure_monitor_profile.container_insights.enabled) + self.assertTrue( + mc.azure_monitor_profile.app_monitoring.open_telemetry_logs_and_traces.enabled + ) + + def test_disable_azure_monitor_logs_skips_prompt_with_yes(self): + dec = self._update_dec({"disable_azure_monitor_logs": True, "yes": True}) + mc = self._mc_with_otlp_logs_traces() + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ): + dec_mc = dec.update_azure_monitor_logs(mc) + + prompt_mock.assert_not_called() + self.assertFalse(dec_mc.azure_monitor_profile.container_insights.enabled) + + def test_disable_azure_monitor_logs_no_prompt_when_otlp_logs_disabled(self): + dec = self._update_dec({"disable_azure_monitor_logs": True, "yes": False}) + mc = self._mc_with_otlp_logs_traces(enabled=False) + dec.context.attach_mc(mc) + self.client.get = Mock(return_value=mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_container_insights_for_monitoring", + ): + dec_mc = dec.update_azure_monitor_logs(mc) + + prompt_mock.assert_not_called() + self.assertFalse(dec_mc.azure_monitor_profile.container_insights.enabled) def test_disable_azure_monitor_logs_noop_when_not_enabled(self): dec = self._update_dec({"disable_azure_monitor_logs": True}) @@ -18135,5 +18568,111 @@ def test_opentelemetry_port_out_of_range_errors(self): dec.update_azure_monitor_profile(mc) + def _mc_with_otlp_metrics(self): + mc = self._mc_with_monitoring() + mc.azure_monitor_profile.app_monitoring = ( + self.models.ManagedClusterAzureMonitorProfileAppMonitoring( + open_telemetry_metrics=( + self.models.ManagedClusterAzureMonitorProfileAppMonitoringOpenTelemetryMetrics( + enabled=True, http_port=4318, grpc_port=4317 + ) + ), + ) + ) + return mc + + def test_disable_azure_monitor_metrics_also_disables_opentelemetry_metrics(self): + # OpenTelemetry metrics are ingested through the managed Prometheus pipeline that Azure + # Monitor metrics provisions, so disabling the parent has to take them down too. + dec = self._update_dec({"disable_azure_monitor_metrics": True, "yes": True}) + mc = self._mc_with_otlp_metrics() + dec.context.attach_mc(mc) + dec.context.set_intermediate("subscription_id", "test_sub_id") + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_azure_monitor_profile_prerequisites" + ): + dec_mc = dec.update_azure_monitor_profile(mc) + + self.assertFalse(dec_mc.azure_monitor_profile.metrics.enabled) + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics + self.assertFalse(otlp.enabled) + self.assertIsNone(otlp.http_port) + self.assertIsNone(otlp.grpc_port) + + def test_disable_azure_monitor_metrics_prompts_when_opentelemetry_metrics_enabled(self): + dec = self._update_dec({"disable_azure_monitor_metrics": True, "yes": False}) + mc = self._mc_with_otlp_metrics() + dec.context.attach_mc(mc) + dec.context.set_intermediate("subscription_id", "test_sub_id") + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_azure_monitor_profile_prerequisites" + ): + dec_mc = dec.update_azure_monitor_profile(mc) + + prompt_mock.assert_called_once() + self.assertIn("OpenTelemetry metrics", prompt_mock.call_args[0][0]) + self.assertFalse(dec_mc.azure_monitor_profile.metrics.enabled) + self.assertFalse(dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled) + + def test_disable_azure_monitor_metrics_aborts_when_confirmation_declined(self): + dec = self._update_dec({"disable_azure_monitor_metrics": True, "yes": False}) + mc = self._mc_with_otlp_metrics() + dec.context.attach_mc(mc) + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=False + ), patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_azure_monitor_profile_prerequisites" + ), self.assertRaises(DecoratorEarlyExitException): + dec.update_azure_monitor_profile(mc) + + # Nothing is mutated when the user declines. + self.assertTrue(mc.azure_monitor_profile.metrics.enabled) + self.assertTrue(mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics.enabled) + + def test_disable_azure_monitor_metrics_no_prompt_when_opentelemetry_metrics_disabled(self): + dec = self._update_dec({"disable_azure_monitor_metrics": True, "yes": False}) + mc = self._mc_with_monitoring() + dec.context.attach_mc(mc) + dec.context.set_intermediate("subscription_id", "test_sub_id") + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_azure_monitor_profile_prerequisites" + ): + dec_mc = dec.update_azure_monitor_profile(mc) + + prompt_mock.assert_not_called() + self.assertFalse(dec_mc.azure_monitor_profile.metrics.enabled) + + def test_disable_azure_monitor_metrics_noop_when_metrics_already_disabled(self): + # Nothing to turn off, so the OpenTelemetry metrics settings are left alone and no + # confirmation is asked for. + dec = self._update_dec({"disable_azure_monitor_metrics": True, "yes": False}) + mc = self._mc_with_otlp_metrics() + mc.azure_monitor_profile.metrics.enabled = False + dec.context.attach_mc(mc) + dec.context.set_intermediate("subscription_id", "test_sub_id") + with patch( + "azure.cli.command_modules.acs.managed_cluster_decorator.prompt_y_n", return_value=True + ) as prompt_mock, patch( + "azure.cli.command_modules.acs.managed_cluster_decorator." + "ensure_azure_monitor_profile_prerequisites" + ) as prereq_mock: + dec_mc = dec.update_azure_monitor_profile(mc) + + prompt_mock.assert_not_called() + otlp = dec_mc.azure_monitor_profile.app_monitoring.open_telemetry_metrics + self.assertTrue(otlp.enabled) + self.assertEqual(otlp.http_port, 4318) + # Leftover DCR/DCRA cleanup is still driven by the flag, not by cluster state. + prereq_mock.assert_called_once() + + if __name__ == "__main__": unittest.main()