From cfc67c0bbd4aa2040d4992ad28ca7e912e23fcd8 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Thu, 23 Jun 2022 16:48:10 -0400 Subject: [PATCH] CloudWatch: fix variable query migration with json template variables (#51207) (#51346) (cherry picked from commit 8ba8e1df8397eca400645703f958ef007a48cc35) --- .../datasource/cloudwatch/datasource.test.ts | 3 +- .../datasource/cloudwatch/datasource.ts | 14 ++++---- .../variableQueryMigrations.test.ts | 16 +++++++++ .../migrations/variableQueryMigrations.ts | 34 +++++++++++++++---- 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/datasource.test.ts index 20940a4ffad..fbee0eb5d6c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.test.ts @@ -500,8 +500,7 @@ describe('datasource', () => { describe('convertMultiFiltersFormat', () => { const ds = setupMockedDataSource({ variables: [labelsVariable, dimensionVariable], mockGetVariableName: false }); it('converts keys and values correctly', () => { - // the json in this line doesn't matter, but it makes sure that old queries will be parsed - const filters = { $dimension: ['b'], a: ['${labels:json}', 'bar'] }; + const filters = { $dimension: ['b'], a: ['$labels', 'bar'] }; const result = ds.datasource.convertMultiFilterFormat(filters); expect(result).toStrictEqual({ env: ['b'], diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index f227bf70d82..52bb8404d8b 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -237,7 +237,7 @@ export class CloudWatchDatasource options, this.timeSrv.timeRange(), this.replace.bind(this), - this.getVariableValue.bind(this), + this.expandVariableToArray.bind(this), this.getActualRegion.bind(this), this.tracingDataSourceUid ); @@ -650,7 +650,7 @@ export class CloudWatchDatasource if (Array.isArray(anyQuery[fieldName])) { anyQuery[fieldName] = anyQuery[fieldName].flatMap((val: string) => { if (fieldName === 'logGroupNames') { - return this.getVariableValue(val, options.scopedVars || {}); + return this.expandVariableToArray(val, options.scopedVars || {}); } return this.replace(val, options.scopedVars, true, fieldName); }); @@ -851,22 +851,20 @@ export class CloudWatchDatasource return { ...result, [key]: null }; } - const newValues = this.getVariableValue(value, scopedVars); + const newValues = this.expandVariableToArray(value, scopedVars); return { ...result, [key]: newValues }; }, {}); } // get the value for a given template variable - getVariableValue(value: string, scopedVars: ScopedVars): string[] { + expandVariableToArray(value: string, scopedVars: ScopedVars): string[] { const variableName = this.templateSrv.getVariableName(value); const valueVar = this.templateSrv.getVariables().find(({ name }) => { return name === variableName; }); if (variableName && valueVar) { if ((valueVar as unknown as VariableWithMultiSupport).multi) { - // rebuild the variable name to handle old migrated queries - const values = this.templateSrv.replace('$' + variableName, scopedVars, 'pipe').split('|'); - return values; + return this.templateSrv.replace(value, scopedVars, 'pipe').split('|'); } return [this.templateSrv.replace(value, scopedVars)]; } @@ -881,7 +879,7 @@ export class CloudWatchDatasource } const initialVal: string[] = []; const newValues = values.reduce((result, value) => { - const vals = this.getVariableValue(value, {}); + const vals = this.expandVariableToArray(value, {}); return [...result, ...vals]; }, initialVal); return { ...result, [key]: newValues }; diff --git a/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.test.ts b/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.test.ts index c322768f166..96569a9ce0a 100644 --- a/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.test.ts +++ b/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.test.ts @@ -55,6 +55,12 @@ describe('variableQueryMigrations', () => { expect(query.dimensionKey).toBe('DBInstanceIdentifier'); expect(query.dimensionFilters).toStrictEqual({ InstanceId: '$instance_id' }); }); + it('should migrate json template variables', () => { + const query = migrateVariableQuery( + 'dimension_values(us-east-1,AWS/RDS,CPUUtilization,DBInstanceIdentifier,{"role":${role:json},"pop":${pop:json}})' + ); + expect(query.dimensionFilters).toStrictEqual({ role: '$role', pop: '$pop' }); + }); }); }); }); @@ -68,6 +74,12 @@ describe('variableQueryMigrations', () => { expect(query.resourceType).toBe('elasticloadbalancing:loadbalancer'); expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['myApp-dev', 'myApp-prod'] }); }); + it('should migrate json template variables', () => { + const query = migrateVariableQuery( + 'resource_arns(eu-west-1,elasticloadbalancing:loadbalancer,{"elasticbeanstalk:environment-name":[${jsonVar:json},"test-$singleVar"]})' + ); + expect(query.tags).toStrictEqual({ 'elasticbeanstalk:environment-name': ['$jsonVar', 'test-$singleVar'] }); + }); it('should parse a empty array for tags', () => { const query = migrateVariableQuery('resource_arns(eu-west-1,elasticloadbalancing:loadbalancer, [])'); expect(query.tags).toStrictEqual({}); @@ -81,6 +93,10 @@ describe('variableQueryMigrations', () => { expect(query.attributeName).toBe('rds:db'); expect(query.ec2Filters).toStrictEqual({ environment: ['$environment'] }); }); + it('should migrate json template variables', () => { + const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,{"environment":${env:json}})'); + expect(query.ec2Filters).toStrictEqual({ environment: ['$env'] }); + }); it('should parse an empty array for filters', () => { const query = migrateVariableQuery('ec2_instance_attribute(us-east-1,rds:db,[])'); expect(query.ec2Filters).toStrictEqual({}); diff --git a/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.ts b/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.ts index 260a2a259df..74e1e6a63e2 100644 --- a/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.ts +++ b/public/app/plugins/datasource/cloudwatch/migrations/variableQueryMigrations.ts @@ -1,11 +1,29 @@ import { omit } from 'lodash'; -import { VariableQuery, VariableQueryType, OldVariableQuery } from '../types'; +import { Dimensions, VariableQuery, VariableQueryType, OldVariableQuery, MultiFilters } from '../types'; + +const jsonVariable = /\${(\w+):json}/g; function isVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): rawQuery is VariableQuery { return typeof rawQuery !== 'string' && typeof rawQuery.ec2Filters !== 'string' && typeof rawQuery.tags !== 'string'; } +function migrateMultiFilters(oldFilters: string): MultiFilters { + const tempFilters = oldFilters.replace(jsonVariable, '"$$$1"'); + const parsedFilters: Dimensions = JSON.parse(tempFilters); + const newFilters: MultiFilters = {}; + // if the old filter was {key:value} transform it to {key:[value]} + Object.keys(parsedFilters).forEach((key) => { + const value = parsedFilters[key]; + if (typeof value === 'string') { + newFilters[key] = [value]; + } else if (value !== undefined) { + newFilters[key] = value; + } + }); + return newFilters; +} + export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVariableQuery): VariableQuery { if (isVariableQuery(rawQuery)) { return rawQuery; @@ -19,22 +37,23 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVaria newQuery.tags = {}; if (rawQuery.dimensionFilters !== '' && rawQuery.ec2Filters !== '[]') { + const tempFilters = rawQuery.dimensionFilters.replace(jsonVariable, '"$$$1"'); try { - newQuery.dimensionFilters = JSON.parse(rawQuery.dimensionFilters); + newQuery.dimensionFilters = JSON.parse(tempFilters); } catch { throw new Error(`unable to migrate poorly formed filters: ${rawQuery.dimensionFilters}`); } } if (rawQuery.ec2Filters !== '' && rawQuery.ec2Filters !== '[]') { try { - newQuery.ec2Filters = JSON.parse(rawQuery.ec2Filters); + newQuery.ec2Filters = migrateMultiFilters(rawQuery.ec2Filters); } catch { throw new Error(`unable to migrate poorly formed filters: ${rawQuery.ec2Filters}`); } } if (rawQuery.tags !== '' && rawQuery.tags !== '[]') { try { - newQuery.tags = JSON.parse(rawQuery.tags); + newQuery.tags = migrateMultiFilters(rawQuery.tags); } catch { throw new Error(`unable to migrate poorly formed filters: ${rawQuery.tags}`); } @@ -94,8 +113,9 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVaria newQuery.dimensionKey = dimensionValuesQuery[4]; newQuery.dimensionFilters = {}; if (!!dimensionValuesQuery[6] && dimensionValuesQuery[6] !== '[]') { + const tempFilters = dimensionValuesQuery[6].replace(jsonVariable, '"$$$1"'); try { - newQuery.dimensionFilters = JSON.parse(dimensionValuesQuery[6]); + newQuery.dimensionFilters = JSON.parse(tempFilters); } catch { throw new Error(`unable to migrate poorly formed filters: ${dimensionValuesQuery[6]}`); } @@ -118,7 +138,7 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVaria newQuery.attributeName = ec2InstanceAttributeQuery[2]; if (ec2InstanceAttributeQuery[3] && ec2InstanceAttributeQuery[3] !== '[]') { try { - newQuery.ec2Filters = JSON.parse(ec2InstanceAttributeQuery[3]); + newQuery.ec2Filters = migrateMultiFilters(ec2InstanceAttributeQuery[3]); } catch { throw new Error(`unable to migrate poorly formed filters: ${ec2InstanceAttributeQuery[3]}`); } @@ -133,7 +153,7 @@ export function migrateVariableQuery(rawQuery: string | VariableQuery | OldVaria newQuery.resourceType = resourceARNsQuery[2]; if (resourceARNsQuery[3] && resourceARNsQuery[3] !== '[]') { try { - newQuery.tags = JSON.parse(resourceARNsQuery[3]); + newQuery.tags = migrateMultiFilters(resourceARNsQuery[3]); } catch { throw new Error(`unable to migrate poorly formed filters: ${resourceARNsQuery[3]}`); }