[data views] data views + rollup index referenced by alias (#212592)

## Summary

Upgrading to 9.x involves reindexing indices created in 7.x, which does
include rollup indices. Reindexing means relying on aliases to preserve
existing index names. As it turns out, our existing code did not work
with rollups that referenced aliases, rather than indices. This is
because the index name is used as an object key even if it was retrieved
via alias.


Note - I need to verify this on 9.0 from scratch. I used upgraded data
and need to verify the steps to make this work when testing.

To test
1. Add sample data
2. Create a rollup job that references the sample data. 
3. Create a data view that references the rollup index. It may take a
few minutes for the rollup index to be populated.
4. Create an alias from the dev console, like such - 

```
POST _aliases
{
  "actions": [
    {
      "add": {
        "index": "rollup",
        "alias": "my-alias"
      }
    }
  ]
}
``` 
5. Create a rollup data view based in the alias you just created.

Part of https://github.com/elastic/kibana/issues/211850

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Matthew Kime 2025-03-10 20:16:04 -05:00 committed by GitHub
parent 85baab2431
commit 5b6dbf2b27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 109 additions and 14 deletions

View file

@ -142,7 +142,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
params: { params: {
rollup_index: rollupIndex, rollup_index: rollupIndex,
}, },
aggs: rollupIndicesCapabilities[rollupIndex].aggs, aggs: rollupCaps?.aggs,
}; };
} }
@ -176,6 +176,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
const isLoadingSources = useObservable(dataViewEditorService.isLoadingSources$, true); const isLoadingSources = useObservable(dataViewEditorService.isLoadingSources$, true);
const existingDataViewNames = useObservable(dataViewEditorService.dataViewNames$); const existingDataViewNames = useObservable(dataViewEditorService.dataViewNames$);
const rollupIndex = useObservable(dataViewEditorService.rollupIndex$); const rollupIndex = useObservable(dataViewEditorService.rollupIndex$);
const rollupCaps = useObservable(dataViewEditorService.rollupCaps$);
const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {}); const rollupIndicesCapabilities = useObservable(dataViewEditorService.rollupIndicesCaps$, {});
useDebounce( useDebounce(
@ -194,7 +195,7 @@ const IndexPatternEditorFlyoutContentComponent = ({
dataViewEditorService.setType(type); dataViewEditorService.setType(type);
}, [dataViewEditorService, type]); }, [dataViewEditorService, type]);
const getRollupIndices = (rollupCaps: RollupIndicesCapsResponse) => Object.keys(rollupCaps); const getRollupIndices = (rollupCapsRes: RollupIndicesCapsResponse) => Object.keys(rollupCapsRes);
const onTypeChange = useCallback( const onTypeChange = useCallback(
(newType: INDEX_PATTERN_TYPE) => { (newType: INDEX_PATTERN_TYPE) => {

View file

@ -80,8 +80,15 @@ const createMatchesIndicesValidator = ({
} }
// A rollup index pattern needs to match one and only one rollup index. // A rollup index pattern needs to match one and only one rollup index.
const rollupIndexMatches = matchedIndices.exactMatchedIndices.filter((matchedIndex) => const rollupIndexMatches = matchedIndices.exactMatchedIndices.filter(
rollupIndices.includes(matchedIndex.name) (matchedIndex) =>
rollupIndices.includes(matchedIndex.name) ||
// matched item is alias
(matchedIndex.item.indices?.length === 1 &&
rollupIndices.includes(matchedIndex.item.indices[0])) ||
// matched item is an index referenced by an alias
(matchedIndex.item.aliases?.length === 1 &&
rollupIndices.includes(matchedIndex.item.aliases[0]))
); );
if (!rollupIndexMatches.length) { if (!rollupIndexMatches.length) {

View file

@ -27,7 +27,12 @@ import {
DataViewField, DataViewField,
} from '@kbn/data-views-plugin/public'; } from '@kbn/data-views-plugin/public';
import { RollupIndicesCapsResponse, MatchedIndicesSet, TimestampOption } from './types'; import {
RollupIndicesCapsResponse,
RollupIndiciesCapability,
MatchedIndicesSet,
TimestampOption,
} from './types';
import { getMatchedIndices, ensureMinimumTime, extractTimeFields, removeSpaces } from './lib'; import { getMatchedIndices, ensureMinimumTime, extractTimeFields, removeSpaces } from './lib';
import { GetFieldsOptions } from './shared_imports'; import { GetFieldsOptions } from './shared_imports';
@ -70,6 +75,7 @@ interface DataViewEditorState {
loadingTimestampFields: boolean; loadingTimestampFields: boolean;
timestampFieldOptions: TimestampOption[]; timestampFieldOptions: TimestampOption[];
rollupIndexName?: string | null; rollupIndexName?: string | null;
rollupCaps?: RollupIndiciesCapability;
} }
const defaultDataViewEditorState: DataViewEditorState = { const defaultDataViewEditorState: DataViewEditorState = {
@ -119,6 +125,7 @@ export class DataViewEditorService {
this.loadingTimestampFields$ = stateSelector((state) => state.loadingTimestampFields); this.loadingTimestampFields$ = stateSelector((state) => state.loadingTimestampFields);
this.timestampFieldOptions$ = stateSelector((state) => state.timestampFieldOptions); this.timestampFieldOptions$ = stateSelector((state) => state.timestampFieldOptions);
this.rollupIndex$ = stateSelector((state) => state.rollupIndexName); this.rollupIndex$ = stateSelector((state) => state.rollupIndexName);
this.rollupCaps$ = stateSelector((state) => state.rollupCaps);
// when list of matched indices is updated always update timestamp fields // when list of matched indices is updated always update timestamp fields
this.loadTimestampFieldsSub = this.matchedIndices$.subscribe(() => this.loadTimestampFields()); this.loadTimestampFieldsSub = this.matchedIndices$.subscribe(() => this.loadTimestampFields());
@ -162,6 +169,8 @@ export class DataViewEditorService {
// current matched rollup index // current matched rollup index
rollupIndex$: Observable<string | undefined | null>; rollupIndex$: Observable<string | undefined | null>;
// current matched rollup capabilities
rollupCaps$: Observable<RollupIndiciesCapability | undefined>;
// alernates between value and undefined so validation can treat new value as thought its a promise // alernates between value and undefined so validation can treat new value as thought its a promise
private rollupIndexForProvider$ = new Subject<string | undefined | null>(); private rollupIndexForProvider$ = new Subject<string | undefined | null>();
@ -244,11 +253,27 @@ export class DataViewEditorService {
// verify we're looking at the current result // verify we're looking at the current result
if (currentLoadingMatchedIndicesIdx === this.currentLoadingMatchedIndices) { if (currentLoadingMatchedIndicesIdx === this.currentLoadingMatchedIndices) {
if (type === INDEX_PATTERN_TYPE.ROLLUP) { if (type === INDEX_PATTERN_TYPE.ROLLUP) {
const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); const rollupIndices = exactMatched.filter(
(index) =>
isRollupIndex(index.name) ||
// if its an alias
(index.item.indices?.length === 1 && isRollupIndex(index.item.indices[0])) ||
// if its an index referenced by an alias
(index.item.aliases?.length === 1 && isRollupIndex(index.item.aliases[0]))
);
newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : null; newRollupIndexName = rollupIndices.length === 1 ? rollupIndices[0].name : null;
this.updateState({ rollupIndexName: newRollupIndexName }); const newRollupCaps = await this.rollupCapsResponse.then((response) => {
return (
response[newRollupIndexName || ''] ||
// if its an alias
response[rollupIndices[0]?.item.indices?.[0] || '']
);
});
this.updateState({ rollupIndexName: newRollupIndexName, rollupCaps: newRollupCaps });
} else { } else {
this.updateState({ rollupIndexName: null }); this.updateState({ rollupIndexName: null, rollupCaps: undefined });
} }
this.updateState({ matchedIndices }); this.updateState({ matchedIndices });

View file

@ -21,7 +21,7 @@ export const successfulResolveResponse = {
aliases: [ aliases: [
{ {
name: 'f-alias', name: 'f-alias',
indices: ['freeze-index', 'my-index'], indices: ['my-index'],
}, },
], ],
data_streams: [ data_streams: [
@ -69,6 +69,16 @@ describe('getIndices', () => {
expect(result[2].name).toBe('remoteCluster1:bar-01'); expect(result[2].name).toBe('remoteCluster1:bar-01');
}); });
it('should work with rollup indices based on aliases', async () => {
const isRollupIdx = (indexName: string) => indexName === 'my-index';
const result = await getIndices({
http,
pattern: 'kibana',
isRollupIndex: isRollupIdx,
});
expect(result[0].tags[1].key).toBe('rollup');
});
it('should ignore ccs query-all', async () => { it('should ignore ccs query-all', async () => {
expect((await getIndices({ http, pattern: '*:', isRollupIndex })).length).toBe(0); expect((await getIndices({ http, pattern: '*:', isRollupIndex })).length).toBe(0);
}); });

View file

@ -119,6 +119,9 @@ export const responseToItemArray = (
const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN); const isFrozen = (index.attributes || []).includes(ResolveIndexResponseItemIndexAttrs.FROZEN);
tags.push(...getTags(index.name)); tags.push(...getTags(index.name));
index.aliases?.forEach((alias) => {
tags.push(...getTags(alias));
});
if (isFrozen) { if (isFrozen) {
tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' }); tags.push({ name: frozenLabel, key: 'frozen', color: 'danger' });
} }
@ -130,11 +133,15 @@ export const responseToItemArray = (
}); });
}); });
(response.aliases || []).forEach((alias) => { (response.aliases || []).forEach((alias) => {
source.push({ const item = {
name: alias.name, name: alias.name,
tags: [{ key: 'alias', name: aliasLabel, color: 'default' }], tags: [{ key: 'alias', name: aliasLabel, color: 'default' }],
item: alias, item: alias,
}); };
// we only need to check the first index to see if its a rollup since there can only be one alias match
item.tags.push(...getTags(alias.indices[0]));
item.tags.push(...getTags(alias.name));
source.push(item);
}); });
(response.data_streams || []).forEach((dataStream) => { (response.data_streams || []).forEach((dataStream) => {
source.push({ source.push({

View file

@ -71,6 +71,19 @@ describe('Index Pattern Fetcher - server', () => {
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1); expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1);
}); });
it("works with index aliases - when rollup response doesn't have index as key", async () => {
esClient.rollup.getRollupIndexCaps.mockResponse(
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse
);
indexPatterns = new IndexPatternsFetcher(esClient, optionalParams);
await indexPatterns.getFieldsForWildcard({
pattern: patternList,
type: DataViewType.ROLLUP,
rollupIndex: 'foo',
});
expect(esClient.rollup.getRollupIndexCaps).toHaveBeenCalledTimes(1);
});
it("doesn't call rollup api when given rollup data view and rollups are disabled", async () => { it("doesn't call rollup api when given rollup data view and rollups are disabled", async () => {
esClient.rollup.getRollupIndexCaps.mockResponse( esClient.rollup.getRollupIndexCaps.mockResponse(
rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse rollupResponse as unknown as estypes.RollupGetRollupIndexCapsResponse

View file

@ -122,11 +122,15 @@ export class IndexPatternsFetcher {
if (this.rollupsEnabled && type === DataViewType.ROLLUP && rollupIndex) { if (this.rollupsEnabled && type === DataViewType.ROLLUP && rollupIndex) {
const rollupFields: FieldDescriptor[] = []; const rollupFields: FieldDescriptor[] = [];
const capabilityCheck = getCapabilitiesForRollupIndices( const capabilities = getCapabilitiesForRollupIndices(
await this.elasticsearchClient.rollup.getRollupIndexCaps({ await this.elasticsearchClient.rollup.getRollupIndexCaps({
index: rollupIndex, index: rollupIndex,
}) })
)[rollupIndex]; );
const capabilityCheck =
// use the rollup index name BUT if its an alias, we'll take the first one
capabilities[rollupIndex] || capabilities[Object.keys(capabilities)[0]];
if (capabilityCheck.error) { if (capabilityCheck.error) {
throw new Error(capabilityCheck.error); throw new Error(capabilityCheck.error);

View file

@ -7,6 +7,7 @@
import { IScopedClusterClient } from '@kbn/core/server'; import { IScopedClusterClient } from '@kbn/core/server';
import { Index } from '@kbn/index-management-plugin/server'; import { Index } from '@kbn/index-management-plugin/server';
import { isArray } from 'lodash';
export const rollupDataEnricher = async (indicesList: Index[], client: IScopedClusterClient) => { export const rollupDataEnricher = async (indicesList: Index[], client: IScopedClusterClient) => {
if (!indicesList || !indicesList.length) { if (!indicesList || !indicesList.length) {
@ -19,7 +20,10 @@ export const rollupDataEnricher = async (indicesList: Index[], client: IScopedCl
}); });
return indicesList.map((index) => { return indicesList.map((index) => {
const isRollupIndex = !!rollupJobData[index.name]; let isRollupIndex = !!rollupJobData[index.name];
if (!isRollupIndex && isArray(index.aliases)) {
isRollupIndex = index.aliases.some((alias) => !!rollupJobData[alias]);
}
return { return {
...index, ...index,
isRollupIndex, isRollupIndex,

View file

@ -113,6 +113,30 @@ export default function ({ getService, getPageObjects }) {
expect(fields).to.eql(['@timestamp', '_id', '_ignored', '_index', '_score', '_source']); expect(fields).to.eql(['@timestamp', '_id', '_ignored', '_index', '_score', '_source']);
}); });
it('create hybrid index pattern - with alias to rollup index', async () => {
const rollupAlias = 'rollup-alias';
await es.indices.putAlias({
index: rollupTargetIndexName,
name: rollupAlias,
});
await PageObjects.common.navigateToApp('settings');
await PageObjects.settings.createIndexPattern(rollupAlias, '@timestamp', false);
await PageObjects.settings.clickKibanaIndexPatterns();
const indexPatternNames = await PageObjects.settings.getAllIndexPatternNames();
//The assertion is going to check that the string has the right name and that the text Rollup
//is included (since there is a Rollup tag).
const filteredIndexPatternNames = indexPatternNames.filter(
(i) => i.includes(rollupIndexPatternName) && i.includes('Rollup')
);
expect(filteredIndexPatternNames.length).to.be(1);
// ensure all fields are available
await PageObjects.settings.clickIndexPatternByName(rollupAlias);
const fields = await PageObjects.settings.getFieldNames();
expect(fields).to.eql(['@timestamp', '_id', '_ignored', '_index', '_score', '_source']);
});
after(async () => { after(async () => {
// Delete the rollup job. // Delete the rollup job.
await es.rollup.deleteJob({ id: rollupJobName }); await es.rollup.deleteJob({ id: rollupJobName });