diff --git a/packages/plugin-aas/src/__tests__/common.ts b/packages/plugin-aas/src/__tests__/common.ts index d4bcceb07..2c98b6dac 100644 --- a/packages/plugin-aas/src/__tests__/common.ts +++ b/packages/plugin-aas/src/__tests__/common.ts @@ -16,6 +16,8 @@ export const DEFAULT_CONFIG: AasConfigOptions = { extraTags: undefined, } +export const NULL_SUBSCRIPTION_ID = '00000000-0000-0000-0000-000000000000' + export const WEB_APP_ID = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my-resource-group/providers/Microsoft.Web/sites/my-web-app' diff --git a/packages/plugin-aas/src/__tests__/instrument.test.ts b/packages/plugin-aas/src/__tests__/instrument.test.ts index b0f8a7660..8ff821e21 100644 --- a/packages/plugin-aas/src/__tests__/instrument.test.ts +++ b/packages/plugin-aas/src/__tests__/instrument.test.ts @@ -34,12 +34,6 @@ const webAppsOperations = { restart: jest.fn(), } -jest.mock('@azure/arm-appservice', () => ({ - WebSiteManagementClient: jest.fn().mockImplementation(() => ({ - webApps: webAppsOperations, - })), -})) - const updateTags = jest.fn().mockResolvedValue({}) jest.mock('@azure/arm-resources', () => ({ @@ -54,7 +48,14 @@ import {makeRunCLI} from '@datadog/datadog-ci-base/helpers/__tests__/testing-too import {PluginCommand as InstrumentCommand} from '../commands/instrument' -import {CONTAINER_WEB_APP, DEFAULT_INSTRUMENT_ARGS, DEFAULT_CONFIG, WEB_APP_ID} from './common' +import {CONTAINER_WEB_APP, DEFAULT_INSTRUMENT_ARGS, DEFAULT_CONFIG, WEB_APP_ID, NULL_SUBSCRIPTION_ID} from './common' + +jest.mock('@azure/arm-appservice', () => ({ + WebSiteManagementClient: jest.fn().mockImplementation(() => ({ + subscriptionId: NULL_SUBSCRIPTION_ID, + webApps: webAppsOperations, + })), +})) async function* asyncIterable(...items: T[]): AsyncGenerator { for (const item of items) { @@ -62,6 +63,11 @@ async function* asyncIterable(...items: T[]): AsyncGenerator { } } +const DEFAULT_CONFIG_WITH_DEFAULT_SERVICE = { + ...DEFAULT_CONFIG, + service: DEFAULT_CONFIG.aasName, +} + describe('aas instrument', () => { const runCLI = makeRunCLI(InstrumentCommand, ['aas', 'instrument']) @@ -82,11 +88,12 @@ describe('aas instrument', () => { .mockResolvedValue('git.commit.sha:test-sha,git.repository_url:test-remote') }) - test('Adds a sidecar and updates the application settings', async () => { + test('Adds a sidecar and updates the application settings and tags', async () => { const {code, context} = await runCLI(DEFAULT_INSTRUMENT_ARGS) expect(context.stdout.toString()).toEqual(`🐶 Beginning instrumentation of Azure App Service(s) Creating sidecar container datadog-sidecar on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app Restarting Azure App Service my-web-app 🐶 Instrumentation completed successfully! `) @@ -102,6 +109,7 @@ Restarting Azure App Service my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], image: 'index.docker.io/datadog/serverless-init:latest', @@ -113,11 +121,12 @@ Restarting Azure App Service my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', + DD_SERVICE: 'my-web-app', DD_API_KEY: 'PLACEHOLDER', DD_SITE: 'datadoghq.com', }, }) - expect(updateTags).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {service: 'my-web-app'}}}) expect(webAppsOperations.restart).toHaveBeenCalled() }) @@ -126,6 +135,7 @@ Restarting Azure App Service my-web-app expect(context.stdout.toString()).toEqual(`[Dry Run] 🐶 Beginning instrumentation of Azure App Service(s) [Dry Run] Creating sidecar container datadog-sidecar on my-web-app [Dry Run] Updating Application Settings for my-web-app +[Dry Run] Updating tags for my-web-app [Dry Run] Restarting Azure App Service my-web-app [Dry Run] 🐶 Instrumentation completed successfully! `) @@ -145,6 +155,7 @@ Restarting Azure App Service my-web-app expect(context.stdout.toString()).toEqual(`🐶 Beginning instrumentation of Azure App Service(s) Creating sidecar container datadog-sidecar on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app 🐶 Instrumentation completed successfully! `) expect(code).toEqual(0) @@ -159,6 +170,7 @@ Updating Application Settings for my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], image: 'index.docker.io/datadog/serverless-init:latest', @@ -170,11 +182,12 @@ Updating Application Settings for my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', + DD_SERVICE: 'my-web-app', DD_API_KEY: 'PLACEHOLDER', DD_SITE: 'datadoghq.com', }, }) - expect(updateTags).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {service: 'my-web-app'}}}) expect(webAppsOperations.restart).not.toHaveBeenCalled() }) @@ -257,6 +270,7 @@ Creating sidecar container datadog-sidecar on my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], image: 'index.docker.io/datadog/serverless-init:latest', @@ -312,6 +326,8 @@ Creating sidecar container datadog-sidecar on my-web-app Creating sidecar container datadog-sidecar on my-web-app2 Updating Application Settings for my-web-app Updating Application Settings for my-web-app2 +Updating tags for my-web-app +Updating tags for my-web-app2 Restarting Azure App Service my-web-app Restarting Azure App Service my-web-app2 🐶 Instrumentation completed successfully! @@ -332,6 +348,7 @@ Restarting Azure App Service my-web-app2 environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], image: 'index.docker.io/datadog/serverless-init:latest', @@ -347,6 +364,7 @@ Restarting Azure App Service my-web-app2 environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], image: 'index.docker.io/datadog/serverless-init:latest', @@ -361,6 +379,7 @@ Restarting Azure App Service my-web-app2 expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', + DD_SERVICE: 'my-web-app', DD_API_KEY: 'PLACEHOLDER', DD_SITE: 'datadoghq.com', }, @@ -368,11 +387,12 @@ Restarting Azure App Service my-web-app2 expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app2', { properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', + DD_SERVICE: 'my-web-app2', DD_API_KEY: 'PLACEHOLDER', DD_SITE: 'datadoghq.com', }, }) - expect(updateTags).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID + '2', {properties: {tags: {service: 'my-web-app2'}}}) expect(webAppsOperations.restart).toHaveBeenCalledTimes(2) expect(webAppsOperations.restart).toHaveBeenCalledWith('my-resource-group', 'my-web-app') expect(webAppsOperations.restart).toHaveBeenCalledWith('my-resource-group', 'my-web-app2') @@ -404,14 +424,14 @@ Restarting Azure App Service my-web-app 'my-web-app', 'datadog-sidecar', { - environmentVariables: [ + environmentVariables: expect.arrayContaining([ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_ENV', value: 'DD_ENV'}, {name: 'DD_VERSION', value: 'DD_VERSION'}, - ], + ]), image: 'index.docker.io/datadog/serverless-init:latest', isMain: false, targetPort: '8126', @@ -452,6 +472,7 @@ Restarting Azure App Service my-web-app expect(context.stdout.toString()).toEqual(`🐶 Beginning instrumentation of Azure App Service(s) Creating sidecar container datadog-sidecar on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app Restarting Azure App Service my-web-app 🐶 Instrumentation completed successfully! `) @@ -466,6 +487,7 @@ Restarting Azure App Service my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, {name: 'CUSTOM_VAR1', value: 'CUSTOM_VAR1'}, {name: 'CUSTOM_VAR2', value: 'CUSTOM_VAR2'}, @@ -480,12 +502,13 @@ Restarting Azure App Service my-web-app properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', DD_API_KEY: 'PLACEHOLDER', + DD_SERVICE: 'my-web-app', DD_SITE: 'datadoghq.com', CUSTOM_VAR1: 'value1', CUSTOM_VAR2: 'value2', }, }) - expect(updateTags).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {service: 'my-web-app'}}}) expect(webAppsOperations.restart).toHaveBeenCalled() }) @@ -509,6 +532,7 @@ Restarting Azure App Service my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, {name: 'CUSTOM_VAR1', value: 'CUSTOM_VAR1'}, ], @@ -522,11 +546,12 @@ Restarting Azure App Service my-web-app properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'true', DD_API_KEY: 'PLACEHOLDER', + DD_SERVICE: 'my-web-app', DD_SITE: 'datadoghq.com', CUSTOM_VAR1: 'value1', }, }) - expect(updateTags).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {service: 'my-web-app'}}}) expect(webAppsOperations.restart).toHaveBeenCalled() }) @@ -536,6 +561,7 @@ Restarting Azure App Service my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', + DD_SERVICE: 'my-web-app', DD_API_KEY: 'PLACEHOLDER', DD_SITE: 'datadoghq.com', DD_TAGS: 'git.commit.sha:test-sha,git.repository_url:test-remote', @@ -555,6 +581,7 @@ Restarting Azure App Service my-web-app properties: { DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', DD_API_KEY: 'PLACEHOLDER', + DD_SERVICE: 'my-web-app', DD_SITE: 'datadoghq.com', DD_TAGS: 'custom:tag,another:value', }, @@ -581,6 +608,7 @@ Restarting Azure App Service my-web-app This flag is only applicable for containerized .NET apps (on musl-based distributions like Alpine Linux), and will be ignored. Creating sidecar container datadog-sidecar on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app Restarting Azure App Service my-web-app 🐶 Instrumentation completed successfully! `) @@ -597,7 +625,7 @@ Restarting Azure App Service my-web-app command.context = {stdout: {write: jest.fn()}} as any command.dryRun = false - client = new WebSiteManagementClient(new DefaultAzureCredential(), '00000000-0000-0000-0000-000000000000') + client = new WebSiteManagementClient(new DefaultAzureCredential(), NULL_SUBSCRIPTION_ID) jest.resetModules() getToken.mockClear().mockResolvedValue({token: 'token'}) @@ -611,7 +639,7 @@ Restarting Azure App Service my-web-app }) test('creates sidecar if not present and updates app settings', async () => { - await command.instrumentSidecar(client, DEFAULT_CONFIG, 'rg', 'app') + await command.instrumentSidecar(client, DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, 'rg', 'app') expect(webAppsOperations.createOrUpdateSiteContainer).toHaveBeenCalledWith('rg', 'app', 'datadog-sidecar', { image: 'index.docker.io/datadog/serverless-init:latest', @@ -620,6 +648,7 @@ Restarting Azure App Service my-web-app environmentVariables: expect.arrayContaining([ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ]), }) @@ -627,13 +656,14 @@ Restarting Azure App Service my-web-app properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', }, }) }) test('adds .NET settings when the config option is specified', async () => { - await command.instrumentSidecar(client, {...DEFAULT_CONFIG, isDotnet: true}, 'rg', 'app') + await command.instrumentSidecar(client, {...DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, isDotnet: true}, 'rg', 'app') expect(webAppsOperations.createOrUpdateSiteContainer).toHaveBeenCalledWith('rg', 'app', 'datadog-sidecar', { image: 'index.docker.io/datadog/serverless-init:latest', @@ -642,6 +672,7 @@ Restarting Azure App Service my-web-app environmentVariables: expect.arrayContaining([ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, {name: 'DD_DOTNET_TRACER_HOME', value: 'DD_DOTNET_TRACER_HOME'}, {name: 'DD_TRACE_LOG_DIRECTORY', value: 'DD_TRACE_LOG_DIRECTORY'}, @@ -654,6 +685,7 @@ Restarting Azure App Service my-web-app properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', CORECLR_ENABLE_PROFILING: '1', CORECLR_PROFILER: '{846F5F1C-F9AE-4B07-969E-05C26BC060D8}', @@ -665,7 +697,12 @@ Restarting Azure App Service my-web-app }) test('adds musl .NET settings when the config options are specified', async () => { - await command.instrumentSidecar(client, {...DEFAULT_CONFIG, isDotnet: true, isMusl: true}, 'rg', 'app') + await command.instrumentSidecar( + client, + {...DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, isDotnet: true, isMusl: true}, + 'rg', + 'app' + ) expect(webAppsOperations.createOrUpdateSiteContainer).toHaveBeenCalledWith('rg', 'app', 'datadog-sidecar', { image: 'index.docker.io/datadog/serverless-init:latest', @@ -674,6 +711,7 @@ Restarting Azure App Service my-web-app environmentVariables: expect.arrayContaining([ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, {name: 'DD_DOTNET_TRACER_HOME', value: 'DD_DOTNET_TRACER_HOME'}, {name: 'DD_TRACE_LOG_DIRECTORY', value: 'DD_TRACE_LOG_DIRECTORY'}, @@ -686,6 +724,7 @@ Restarting Azure App Service my-web-app properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', CORECLR_ENABLE_PROFILING: '1', CORECLR_PROFILER: '{846F5F1C-F9AE-4B07-969E-05C26BC060D8}', @@ -713,13 +752,14 @@ Restarting Azure App Service my-web-app webAppsOperations.createOrUpdateSiteContainer.mockResolvedValue({}) webAppsOperations.updateApplicationSettings.mockResolvedValue({}) - await command.instrumentSidecar(client, DEFAULT_CONFIG, 'rg', 'app') + await command.instrumentSidecar(client, DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, 'rg', 'app') expect(webAppsOperations.createOrUpdateSiteContainer).toHaveBeenCalled() expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalled() }) test('does not update sidecar if config is correct', async () => { + webAppsOperations.get.mockResolvedValue({...CONTAINER_WEB_APP, tags: {service: 'my-web-app'}}) webAppsOperations.listSiteContainers.mockReturnValue( asyncIterable({ name: 'datadog-sidecar', @@ -728,6 +768,7 @@ Restarting Azure App Service my-web-app environmentVariables: [ {name: 'DD_API_KEY', value: 'DD_API_KEY'}, {name: 'DD_SITE', value: 'DD_SITE'}, + {name: 'DD_SERVICE', value: 'DD_SERVICE'}, {name: 'DD_AAS_INSTANCE_LOGGING_ENABLED', value: 'DD_AAS_INSTANCE_LOGGING_ENABLED'}, ], }) @@ -736,13 +777,15 @@ Restarting Azure App Service my-web-app properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', }, }) - await command.instrumentSidecar(client, DEFAULT_CONFIG, 'rg', 'app') + await command.instrumentSidecar(client, DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, 'rg', 'app') expect(webAppsOperations.createOrUpdateSiteContainer).not.toHaveBeenCalled() expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() + expect(updateTags).not.toHaveBeenCalled() }) test('does not call Azure APIs in dry run mode', async () => { @@ -750,7 +793,7 @@ Restarting Azure App Service my-web-app webAppsOperations.listSiteContainers.mockReturnValue(asyncIterable()) webAppsOperations.listApplicationSettings.mockResolvedValue({properties: {}}) - await command.instrumentSidecar(client, DEFAULT_CONFIG, 'rg', 'app') + await command.instrumentSidecar(client, DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, 'rg', 'app') expect(webAppsOperations.createOrUpdateSiteContainer).not.toHaveBeenCalled() expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() @@ -762,11 +805,12 @@ Restarting Azure App Service my-web-app properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', }, }) - await command.instrumentSidecar(client, DEFAULT_CONFIG, 'rg', 'app') + await command.instrumentSidecar(client, DEFAULT_CONFIG_WITH_DEFAULT_SERVICE, 'rg', 'app') expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() }) diff --git a/packages/plugin-aas/src/__tests__/uninstrument.test.ts b/packages/plugin-aas/src/__tests__/uninstrument.test.ts index db4b6150c..05ff826c6 100644 --- a/packages/plugin-aas/src/__tests__/uninstrument.test.ts +++ b/packages/plugin-aas/src/__tests__/uninstrument.test.ts @@ -20,9 +20,11 @@ const webAppsOperations = { updateApplicationSettings: jest.fn(), } -jest.mock('@azure/arm-appservice', () => ({ - WebSiteManagementClient: jest.fn().mockImplementation(() => ({ - webApps: webAppsOperations, +const updateTags = jest.fn().mockResolvedValue({}) + +jest.mock('@azure/arm-resources', () => ({ + ResourceManagementClient: jest.fn().mockImplementation(() => ({ + tagsOperations: {beginCreateOrUpdateAtScopeAndWait: updateTags}, })), })) @@ -30,7 +32,14 @@ import {makeRunCLI} from '@datadog/datadog-ci-base/helpers/__tests__/testing-too import {PluginCommand as UninstrumentCommand} from '../commands/uninstrument' -import {CONTAINER_WEB_APP, DEFAULT_ARGS} from './common' +import {CONTAINER_WEB_APP, DEFAULT_ARGS, NULL_SUBSCRIPTION_ID, WEB_APP_ID} from './common' + +jest.mock('@azure/arm-appservice', () => ({ + WebSiteManagementClient: jest.fn().mockImplementation(() => ({ + subscriptionId: NULL_SUBSCRIPTION_ID, + webApps: webAppsOperations, + })), +})) describe('aas instrument', () => { const runCLI = makeRunCLI(UninstrumentCommand, ['aas', 'uninstrument']) @@ -39,10 +48,14 @@ describe('aas instrument', () => { beforeEach(() => { jest.resetModules() getToken.mockClear().mockResolvedValue({token: 'token'}) - webAppsOperations.get.mockReset().mockResolvedValue(CONTAINER_WEB_APP) + webAppsOperations.get.mockReset().mockResolvedValue({ + ...CONTAINER_WEB_APP, + tags: {service: CONTAINER_WEB_APP.name}, + }) webAppsOperations.deleteSiteContainer.mockReset().mockResolvedValue(undefined) webAppsOperations.listApplicationSettings.mockReset().mockResolvedValue({properties: {}}) webAppsOperations.updateApplicationSettings.mockReset().mockResolvedValue(undefined) + updateTags.mockClear().mockResolvedValue({}) }) test('Fails if not authenticated with Azure', async () => { @@ -63,10 +76,12 @@ Please ensure that you have the Azure CLI installed (https://aka.ms/azure-cli) a }) test('Dry run uninstrumenting doesnt change settings', async () => { + updateTags.mockResolvedValue({service: 'my-web-app'}) webAppsOperations.listApplicationSettings.mockReset().mockResolvedValue({ properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', hello: 'world', // existing setting to ensure we don't remove it }, @@ -76,6 +91,7 @@ Please ensure that you have the Azure CLI installed (https://aka.ms/azure-cli) a [Dry Run] Removing sidecar container datadog-sidecar from my-web-app (if it exists) [Dry Run] Checking Application Settings on my-web-app [Dry Run] Updating Application Settings for my-web-app +[Dry Run] Updating tags for my-web-app [Dry Run] 🐶 Uninstrumentation completed successfully! `) expect(code).toEqual(0) @@ -87,10 +103,15 @@ Please ensure that you have the Azure CLI installed (https://aka.ms/azure-cli) a }) test('Uninstrument sidecar and updates app settings', async () => { + webAppsOperations.get.mockResolvedValue({ + ...CONTAINER_WEB_APP, + tags: {service: 'my-service', env: 'staging', version: '1.0', ava: 'true'}, + }) webAppsOperations.listApplicationSettings.mockReset().mockResolvedValue({ properties: { DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', hello: 'world', // existing setting to ensure we don't remove it }, @@ -100,6 +121,7 @@ Please ensure that you have the Azure CLI installed (https://aka.ms/azure-cli) a Removing sidecar container datadog-sidecar from my-web-app (if it exists) Checking Application Settings on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app 🐶 Uninstrumentation completed successfully! `) expect(code).toEqual(0) @@ -114,15 +136,18 @@ Updating Application Settings for my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: {hello: 'world'}, // ensure existing settings are preserved }) + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {ava: 'true'}}}) }) test('Uninstrument sidecar and updates app settings with .NET settings', async () => { + updateTags.mockResolvedValue({service: 'my-web-app'}) webAppsOperations.listApplicationSettings.mockReset().mockResolvedValue({ properties: { hello: 'world', foo: 'bar', DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', CORECLR_ENABLE_PROFILING: '1', CORECLR_PROFILER: '{846F5F1C-F9AE-4B07-969E-05C26BC060D8}', @@ -136,6 +161,7 @@ Updating Application Settings for my-web-app Removing sidecar container datadog-sidecar from my-web-app (if it exists) Checking Application Settings on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app 🐶 Uninstrumentation completed successfully! `) expect(code).toEqual(0) @@ -150,15 +176,18 @@ Updating Application Settings for my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: {hello: 'world', foo: 'bar'}, }) + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {}}}) }) test('Uninstrument sidecar and updates custom app settings from config', async () => { + updateTags.mockResolvedValue({service: 'my-web-app'}) webAppsOperations.listApplicationSettings.mockReset().mockResolvedValue({ properties: { hello: 'world', foo: 'bar', DD_API_KEY: process.env.DD_API_KEY, DD_SITE: 'datadoghq.com', + DD_SERVICE: 'my-web-app', DD_AAS_INSTANCE_LOGGING_ENABLED: 'false', DD_SOME_FEATURE: 'true', }, @@ -168,6 +197,7 @@ Updating Application Settings for my-web-app Removing sidecar container datadog-sidecar from my-web-app (if it exists) Checking Application Settings on my-web-app Updating Application Settings for my-web-app +Updating tags for my-web-app 🐶 Uninstrumentation completed successfully! `) expect(code).toEqual(0) @@ -182,6 +212,7 @@ Updating Application Settings for my-web-app expect(webAppsOperations.updateApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app', { properties: {hello: 'world', foo: 'bar'}, }) + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {}}}) }) test('Warns and exits if App Service is not Linux', async () => { @@ -200,6 +231,7 @@ https://docs.datadoghq.com/serverless/azure_app_services/azure_app_services_wind expect(webAppsOperations.deleteSiteContainer).not.toHaveBeenCalled() expect(webAppsOperations.listApplicationSettings).not.toHaveBeenCalled() expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() + expect(updateTags).not.toHaveBeenCalled() }) test('Exits properly if the AAS does not exist', async () => { @@ -217,6 +249,7 @@ https://docs.datadoghq.com/serverless/azure_app_services/azure_app_services_wind expect(webAppsOperations.deleteSiteContainer).not.toHaveBeenCalled() expect(webAppsOperations.listApplicationSettings).not.toHaveBeenCalled() expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() + expect(updateTags).not.toHaveBeenCalled() }) test('Handles errors during sidecar uninstrumentation', async () => { @@ -240,6 +273,7 @@ Checking Application Settings on my-web-app expect(webAppsOperations.listApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app') // the last operations never get called due to the above failure expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() + expect(updateTags).not.toHaveBeenCalled() }) test('Errors if no Azure App Service is specified', async () => { @@ -284,6 +318,8 @@ Checking Application Settings on my-web-app Checking Application Settings on my-web-app2 No Application Settings changes needed for my-web-app. No Application Settings changes needed for my-web-app2. +Updating tags for my-web-app +Updating tags for my-web-app2 🐶 Uninstrumentation completed successfully! `) expect(getToken).toHaveBeenCalledTimes(1) @@ -305,6 +341,8 @@ No Application Settings changes needed for my-web-app2. expect(webAppsOperations.listApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app') expect(webAppsOperations.listApplicationSettings).toHaveBeenCalledWith('my-resource-group', 'my-web-app2') expect(webAppsOperations.updateApplicationSettings).not.toHaveBeenCalled() + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID, {properties: {tags: {}}}) + expect(updateTags).toHaveBeenCalledWith(WEB_APP_ID + '2', {properties: {tags: {}}}) }) }) }) diff --git a/packages/plugin-aas/src/commands/instrument.ts b/packages/plugin-aas/src/commands/instrument.ts index 1a9e14f11..e3c94d708 100644 --- a/packages/plugin-aas/src/commands/instrument.ts +++ b/packages/plugin-aas/src/commands/instrument.ts @@ -25,6 +25,9 @@ import { } from '../common' export class PluginCommand extends AasInstrumentCommand { + private cred!: DefaultAzureCredential + private tagClient!: TagsOperations + public async execute(): Promise<0 | 1> { this.enableFips() const [appServicesToInstrument, config, errors] = await this.ensureConfig() @@ -56,11 +59,11 @@ export class PluginCommand extends AasInstrumentCommand { return 1 } - const cred = new DefaultAzureCredential() - if (!(await ensureAzureAuth(this.context.stdout.write, cred))) { + this.cred = new DefaultAzureCredential() + if (!(await ensureAzureAuth(this.context.stdout.write, this.cred))) { return 1 } - const tagClient = new ResourceManagementClient(cred).tagsOperations + this.tagClient = new ResourceManagementClient(this.cred).tagsOperations if (config.sourceCodeIntegration) { config.extraTags = await handleSourceCodeIntegration( @@ -73,7 +76,7 @@ export class PluginCommand extends AasInstrumentCommand { this.context.stdout.write(`${this.dryRunPrefix}🐶 Beginning instrumentation of Azure App Service(s)\n`) const results = await Promise.all( Object.entries(appServicesToInstrument).map(([subscriptionId, resourceGroupToNames]) => - this.processSubscription(cred, tagClient, subscriptionId, resourceGroupToNames, config) + this.processSubscription(subscriptionId, resourceGroupToNames, config) ) ) const success = results.every((result) => result) @@ -87,16 +90,14 @@ export class PluginCommand extends AasInstrumentCommand { } public async processSubscription( - cred: DefaultAzureCredential, - tagClient: TagsOperations, subscriptionId: string, resourceGroupToNames: Record, config: AasConfigOptions ): Promise { - const aasClient = new WebSiteManagementClient(cred, subscriptionId, {apiVersion: '2024-11-01'}) + const aasClient = new WebSiteManagementClient(this.cred, subscriptionId, {apiVersion: '2024-11-01'}) const results = await Promise.all( Object.entries(resourceGroupToNames).flatMap(([resourceGroup, aasNames]) => - aasNames.map((aasName) => this.processAas(aasClient, tagClient, config, subscriptionId, resourceGroup, aasName)) + aasNames.map((aasName) => this.processAas(aasClient, config, resourceGroup, aasName)) ) ) @@ -109,9 +110,7 @@ export class PluginCommand extends AasInstrumentCommand { */ public async processAas( aasClient: WebSiteManagementClient, - tagClient: TagsOperations, config: AasConfigOptions, - subscriptionId: string, resourceGroup: string, aasName: string ): Promise { @@ -130,17 +129,14 @@ This flag is only applicable for containerized .NET apps (on musl-based distribu ) ) } - await this.instrumentSidecar( - aasClient, - { - ...config, - isDotnet: config.isDotnet || isDotnet(site), - isMusl: config.isMusl && config.isDotnet && isContainer, - }, - resourceGroup, - aasName - ) - await this.addTags(tagClient, config, subscriptionId, resourceGroup, aasName, site.tags ?? {}) + config = { + ...config, + isDotnet: config.isDotnet || isDotnet(site), + isMusl: config.isMusl && config.isDotnet && isContainer, + service: config.service ?? aasName, + } + await this.instrumentSidecar(aasClient, config, resourceGroup, aasName) + await this.addTags(config, aasClient.subscriptionId!, resourceGroup, aasName, site.tags ?? {}) } catch (error) { this.context.stdout.write(renderError(`Failed to instrument ${aasName}: ${formatError(error)}`)) @@ -162,18 +158,15 @@ This flag is only applicable for containerized .NET apps (on musl-based distribu return true } + public async addTags( - tagClient: TagsOperations, config: AasConfigOptions, subscriptionId: string, resourceGroup: string, aasName: string, tags: Record ): Promise { - const updatedTags = {...tags} - if (config.service) { - updatedTags.service = config.service - } + const updatedTags: Record = {...tags, service: config.service!} if (config.environment) { updatedTags.env = config.environment } @@ -184,7 +177,7 @@ This flag is only applicable for containerized .NET apps (on musl-based distribu this.context.stdout.write(`${this.dryRunPrefix}Updating tags for ${chalk.bold(aasName)}\n`) if (!this.dryRun) { try { - await tagClient.beginCreateOrUpdateAtScopeAndWait( + await this.tagClient.beginCreateOrUpdateAtScopeAndWait( `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${aasName}`, {properties: {tags: updatedTags}} ) diff --git a/packages/plugin-aas/src/commands/uninstrument.ts b/packages/plugin-aas/src/commands/uninstrument.ts index d58eea3c1..8da3570e8 100644 --- a/packages/plugin-aas/src/commands/uninstrument.ts +++ b/packages/plugin-aas/src/commands/uninstrument.ts @@ -1,9 +1,11 @@ import {WebSiteManagementClient} from '@azure/arm-appservice' +import {ResourceManagementClient, TagsOperations} from '@azure/arm-resources' import {DefaultAzureCredential} from '@azure/identity' import {AasConfigOptions} from '@datadog/datadog-ci-base/commands/aas/common' import {AasUninstrumentCommand} from '@datadog/datadog-ci-base/commands/aas/uninstrument' import {renderError} from '@datadog/datadog-ci-base/helpers/renderer' import chalk from 'chalk' +import equal from 'fast-deep-equal' import { AAS_DD_SETTING_NAMES, @@ -16,6 +18,9 @@ import { } from '../common' export class PluginCommand extends AasUninstrumentCommand { + private cred!: DefaultAzureCredential + private tagClient!: TagsOperations + public async execute(): Promise<0 | 1> { this.enableFips() const [appServicesToUninstrument, config, errors] = await this.ensureConfig() @@ -27,14 +32,15 @@ export class PluginCommand extends AasUninstrumentCommand { return 1 } - const cred = new DefaultAzureCredential() - if (!(await ensureAzureAuth(this.context.stdout.write, cred))) { + this.cred = new DefaultAzureCredential() + if (!(await ensureAzureAuth(this.context.stdout.write, this.cred))) { return 1 } + this.tagClient = new ResourceManagementClient(this.cred).tagsOperations this.context.stdout.write(`${this.dryRunPrefix}🐶 Beginning uninstrumentation of Azure App Service(s)\n`) const results = await Promise.all( Object.entries(appServicesToUninstrument).map(([subscriptionId, resourceGroupToNames]) => - this.processSubscription(cred, subscriptionId, resourceGroupToNames, config) + this.processSubscription(subscriptionId, resourceGroupToNames, config) ) ) const success = results.every((result) => result) @@ -48,12 +54,11 @@ export class PluginCommand extends AasUninstrumentCommand { } public async processSubscription( - cred: DefaultAzureCredential, subscriptionId: string, resourceGroupToNames: Record, config: AasConfigOptions ): Promise { - const client = new WebSiteManagementClient(cred, subscriptionId, {apiVersion: '2024-11-01'}) + const client = new WebSiteManagementClient(this.cred, subscriptionId, {apiVersion: '2024-11-01'}) const results = await Promise.all( Object.entries(resourceGroupToNames).flatMap(([resourceGroup, aasNames]) => aasNames.map((aasName) => this.processAas(client, config, resourceGroup, aasName)) @@ -81,10 +86,11 @@ export class PluginCommand extends AasUninstrumentCommand { await this.uninstrumentSidecar( client, - {...config, isDotnet: config.isDotnet || isDotnet(site)}, + {...config, isDotnet: config.isDotnet || isDotnet(site), service: config.service ?? aasName}, resourceGroup, aasName ) + await this.removeTags(client.subscriptionId!, resourceGroup, aasName, site.tags ?? {}) } catch (error) { this.context.stdout.write(renderError(`Failed to uninstrument ${chalk.bold(aasName)}: ${formatError(error)}`)) @@ -126,4 +132,30 @@ export class PluginCommand extends AasUninstrumentCommand { ) } } + public async removeTags( + subscriptionId: string, + resourceGroup: string, + aasName: string, + tags: Record + ) { + const updatedTags = {...tags} + delete updatedTags.service + delete updatedTags.env + delete updatedTags.version + if (!equal(tags, updatedTags)) { + this.context.stdout.write(`${this.dryRunPrefix}Updating tags for ${chalk.bold(aasName)}\n`) + if (!this.dryRun) { + try { + await this.tagClient.beginCreateOrUpdateAtScopeAndWait( + `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Web/sites/${aasName}`, + {properties: {tags: updatedTags}} + ) + } catch (error) { + this.context.stdout.write( + renderError(`Failed to update tags for ${chalk.bold(aasName)}: ${formatError(error)}`) + ) + } + } + } + } } diff --git a/packages/plugin-aas/src/common.ts b/packages/plugin-aas/src/common.ts index b72ebb28e..71551cd99 100644 --- a/packages/plugin-aas/src/common.ts +++ b/packages/plugin-aas/src/common.ts @@ -96,12 +96,10 @@ export const getEnvVars = (config: AasConfigOptions): Record => let envVars: Record = { DD_API_KEY: process.env.DD_API_KEY!, DD_SITE: process.env.DD_SITE ?? DATADOG_SITE_US1, + DD_SERVICE: config.service!, DD_AAS_INSTANCE_LOGGING_ENABLED: (config.isInstanceLoggingEnabled ?? false).toString(), ...parseEnvVars(config.envVars), } - if (config.service) { - envVars.DD_SERVICE = config.service - } if (config.environment) { envVars.DD_ENV = config.environment }