Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fleet] Make upload and registry package info consistent #126915

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
cf96755
Update docker image + set up initial validation test
kpollich Mar 3, 2022
625f582
Get validation test passing
kpollich Mar 4, 2022
5ff6ee6
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 4, 2022
c564592
Remove erroneous test load call
kpollich Mar 4, 2022
586c08c
Address PR review + improve comments + rename validation.ts -> parse.ts
kpollich Mar 7, 2022
6a9c1e9
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 7, 2022
62a45cc
Replace packages in fleet_packages.json
kpollich Mar 7, 2022
cae823e
Add temp debug log to debug CI failures
kpollich Mar 7, 2022
37f60f3
Use a non-colliding package in bundled package tests
kpollich Mar 7, 2022
4212c27
(debug) Add logging output
kpollich Mar 7, 2022
0f779d9
(debug) More logging
kpollich Mar 7, 2022
000f3a1
(debug) Log bundled package dir in module
kpollich Mar 7, 2022
6fdfd1f
Use absolute path for bundled packages
kpollich Mar 8, 2022
8656915
Remove debug logs + use KIBANA_BUILD_LOCATION if it exists
kpollich Mar 8, 2022
29d903c
Add support for developer.bundledPackageLocation config value
kpollich Mar 8, 2022
a316ab1
(debug) Try some more logs
kpollich Mar 8, 2022
0ba1902
(debug) Try some more logs
kpollich Mar 9, 2022
14e7b87
Fix test hopefully 🤞
kpollich Mar 9, 2022
eadab35
Fix other failing tests
kpollich Mar 9, 2022
1da1eba
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 9, 2022
a1254e7
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 9, 2022
dadfe20
Move default for bundled package dir to schema definition
kpollich Mar 9, 2022
5d97961
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 9, 2022
9122a63
Merge branch 'main' into 126695-make-upload-and-registry-package-info…
kibanamachine Mar 9, 2022
bd2e6c2
Fix schema default value for bundledPackageLocation
kpollich Mar 9, 2022
bfffa79
Merge branch '126695-make-upload-and-registry-package-info-consistent…
kpollich Mar 9, 2022
f0e9da1
Fix snapshot
kpollich Mar 9, 2022
29a30e4
Fix regression in bundled packages fetch
kpollich Mar 9, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions x-pack/plugins/fleet/common/types/models/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ export enum RegistryDataStreamKeys {
}

export interface RegistryDataStream {
[key: string]: any;
[RegistryDataStreamKeys.type]: string;
[RegistryDataStreamKeys.ilm_policy]?: string;
[RegistryDataStreamKeys.hidden]?: boolean;
Expand All @@ -323,6 +324,7 @@ export interface RegistryElasticsearch {
privileges?: RegistryDataStreamPrivileges;
'index_template.settings'?: estypes.IndicesIndexSettings;
'index_template.mappings'?: estypes.MappingTypeMapping;
'ingest_pipeline.name'?: string;
}

export interface RegistryDataStreamPrivileges {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/models/package_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,5 @@ export interface PackageSpecScreenshot {
title: string;
size?: string;
type?: string;
path?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function useDockerRegistry() {

let dockerProcess: ChildProcess | undefined;
async function startDockerRegistryServer() {
const dockerImage = `docker.elastic.co/package-registry/distribution@sha256:8b4ce36ecdf86e6cfdf781d9df8d564a014add9afc9aec21cf2c5a68ff82d3ab`;
const dockerImage = `docker.elastic.co/package-registry/distribution@sha256:b3dfc6a11ff7dce82ba8689ea9eeb54e353c6b4bfd2d28127b20ef72fd8883e9`;

const args = ['run', '--rm', '-p', `${packageRegistryPort}:8080`, dockerImage];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import path from 'path';
import fs from 'fs/promises';

import JSON5 from 'json5';
import { REPO_ROOT } from '@kbn/utils';

import * as Registry from '../services/epm/registry';
import { parseAndVerifyArchiveEntries } from '../services/epm/archive';

import { createAppContextStartContractMock } from '../mocks';
import { appContextService } from '../services';

import { useDockerRegistry } from './helpers';

describe('validate bundled packages', () => {
const registryUrl = useDockerRegistry();
let mockContract: ReturnType<typeof createAppContextStartContractMock>;

beforeEach(() => {
mockContract = createAppContextStartContractMock({ registryUrl });
appContextService.start(mockContract);
});

async function getBundledPackageEntries() {
const configFilePath = path.resolve(REPO_ROOT, 'fleet_packages.json');
const configFile = await fs.readFile(configFilePath, 'utf8');
const bundledPackages = JSON5.parse(configFile);

return bundledPackages as Array<{ name: string; version: string }>;
}

async function setupPackageObjects() {
const bundledPackages = await getBundledPackageEntries();

const packageObjects = await Promise.all(
bundledPackages.map(async (bundledPackage) => {
const registryPackage = await Registry.getRegistryPackage(
bundledPackage.name,
bundledPackage.version
);

const packageArchive = await Registry.fetchArchiveBuffer(
bundledPackage.name,
bundledPackage.version
);

return { registryPackage, packageArchive };
})
);

return packageObjects;
}

it('generates matching package info objects for uploaded and registry packages', async () => {
const packageObjects = await setupPackageObjects();

for (const packageObject of packageObjects) {
const { registryPackage, packageArchive } = packageObject;

const archivePackageInfo = await parseAndVerifyArchiveEntries(
packageArchive.archiveBuffer,
'application/zip'
);

expect(archivePackageInfo.packageInfo.data_streams).toEqual(
Copy link
Member

Choose a reason for hiding this comment

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

can we test more things than the data_streams property here?

Copy link
Member Author

Choose a reason for hiding this comment

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

I am taking a look at other properties to place under test here and there don't seem to be any good candidates. policy_templates and vars were my first thoughts, but the current registry logic "fills out" variable values that we infer default for in Fleet, so these fields are unlikely to match for now. I am going to merge this PR for now, and these tests will likely change in #115032

registryPackage.packageInfo.data_streams
);
}
});
});
6 changes: 5 additions & 1 deletion x-pack/plugins/fleet/server/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { PackagePolicyServiceInterface } from '../services/package_policy';
import type { AgentPolicyServiceInterface } from '../services';
import type { FleetAppContext } from '../plugin';
import { createMockTelemetryEventsSender } from '../telemetry/__mocks__';
import type { FleetConfigType } from '../../common';
import { createFleetAuthzMock } from '../../common';
import { agentServiceMock } from '../services/agents/agent_service.mock';
import type { FleetRequestHandlerContext } from '../types';
Expand All @@ -39,11 +40,14 @@ export interface MockedFleetAppContext extends FleetAppContext {
logger: ReturnType<ReturnType<typeof loggingSystemMock.create>['get']>;
}

export const createAppContextStartContractMock = (): MockedFleetAppContext => {
export const createAppContextStartContractMock = (
configOverrides: Partial<FleetConfigType> = {}
): MockedFleetAppContext => {
const config = {
agents: { enabled: true, elasticsearch: {} },
enabled: true,
agentIdVerificationEnabled: true,
...configOverrides,
};

const config$ = of(config);
Expand Down
164 changes: 128 additions & 36 deletions x-pack/plugins/fleet/server/services/epm/archive/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/

import { merge } from '@kbn/std';
import yaml from 'js-yaml';
import { pick, uniq } from 'lodash';

Expand Down Expand Up @@ -32,6 +33,42 @@ import { unpackBufferEntries } from './index';
const MANIFESTS: Record<string, Buffer> = {};
const MANIFEST_NAME = 'manifest.yml';

const DEFAULT_RELEASE_VALUE = 'ga';

const DEFAULT_INGEST_PIPELINE_VALUE = 'default';
const DEFAULT_INGEST_PIPELINE_FILE_NAME_YML = 'default.yml';
const DEFAULT_INGEST_PIPELINE_FILE_NAME_JSON = 'default.json';

// Borrowed from https://github.com/elastic/kibana/blob/main/x-pack/plugins/security_solution/common/utils/expand_dotted.ts
// With some alterations around non-object values
const expandDottedField = (dottedFieldName: string, val: unknown): object => {
const parts = dottedFieldName.split('.');

if (parts.length === 1) {
return { [parts[0]]: val };
} else {
return { [parts[0]]: expandDottedField(parts.slice(1).join('.'), val) };
}
};

export const expandDottedObject = (dottedObj: object) => {
if (typeof dottedObj !== 'object' || Array.isArray(dottedObj)) {
return dottedObj;
}
return Object.entries(dottedObj).reduce(
(acc, [key, val]) => merge(acc, expandDottedField(key, val)),
{}
);
};

export const expandDottedEntries = (obj: object) => {
return Object.entries<any>(obj).reduce<any>((acc, [key, value]) => {
acc[key] = expandDottedObject(value);

return acc;
}, {} as Record<string, any>);
};

// not sure these are 100% correct but they do the job here
// keeping them local until others need them
type OptionalPropertyOf<T extends object> = Exclude<
Expand Down Expand Up @@ -144,8 +181,13 @@ function parseAndVerifyArchive(paths: string[]): ArchivePackage {
);
}

parsed.data_streams = parseAndVerifyDataStreams(paths, parsed.name, parsed.version);
const parsedDataStreams = parseAndVerifyDataStreams(paths, parsed.name, parsed.version);
if (parsedDataStreams.length) {
parsed.data_streams = parsedDataStreams;
}

parsed.policy_templates = parseAndVerifyPolicyTemplates(manifest);

// add readme if exists
const readme = parseAndVerifyReadme(paths, parsed.name, parsed.version);
if (readme) {
Expand Down Expand Up @@ -202,40 +244,87 @@ export function parseAndVerifyDataStreams(

const {
title: dataStreamTitle,
release,
release = DEFAULT_RELEASE_VALUE,
type,
dataset,
ingest_pipeline: ingestPipeline,
streams: manifestStreams,
elasticsearch,
...restOfProps
} = manifest;
if (!(dataStreamTitle && type)) {
throw new PackageInvalidArchiveError(
`Invalid manifest for data stream '${dataStreamPath}': one or more fields missing of 'title', 'type'`
);
}

let ingestPipeline;
const ingestPipelinePaths = paths.filter((path) =>
path.startsWith(`${pkgKey}/data_stream/${dataStreamPath}/elasticsearch/ingest_pipeline`)
);

if (
ingestPipelinePaths.length &&
(ingestPipelinePaths.some((ingestPipelinePath) =>
ingestPipelinePath.endsWith(DEFAULT_INGEST_PIPELINE_FILE_NAME_YML)
) ||
ingestPipelinePaths.some((ingestPipelinePath) =>
ingestPipelinePath.endsWith(DEFAULT_INGEST_PIPELINE_FILE_NAME_JSON)
))
) {
ingestPipeline = DEFAULT_INGEST_PIPELINE_VALUE;
}
joshdover marked this conversation as resolved.
Show resolved Hide resolved

const streams = parseAndVerifyStreams(manifestStreams, dataStreamPath);

const parsedElasticsearchEntry: Record<string, any> = {};

if (ingestPipeline) {
parsedElasticsearchEntry['ingest_pipeline.name'] = DEFAULT_INGEST_PIPELINE_VALUE;
}

if (elasticsearch?.privileges) {
parsedElasticsearchEntry.privileges = elasticsearch.privileges;
}

if (elasticsearch?.index_template?.mappings) {
parsedElasticsearchEntry['index_template.mappings'] = expandDottedEntries(
Copy link
Member Author

Choose a reason for hiding this comment

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

These mappings/settings fields come through the manifest with dotted.key.names, but the package info object expects them to be expanded to nested object structures.

elasticsearch.index_template.mappings
);
}

if (elasticsearch?.index_template?.settings) {
parsedElasticsearchEntry['index_template.settings'] = expandDottedEntries(
elasticsearch.index_template.settings
);
}

// Build up the stream object here so we can conditionally insert nullable fields. The package registry omits undefined
Copy link
Member Author

Choose a reason for hiding this comment

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

A lot of the changes here are related to this concept. The registry service omits undefined values. We could probably get away with not doing this and more loosely comparing values, treating an explicit undefined the same as an omitted value, but I opted to be as one-to-one as possible here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense to be conservative for now. When we're done with this PR, could we add notes to #115032 about potential any cleanup like this?

// fields, so we're mimicking that behavior here.
const dataStreamObject: RegistryDataStream = {
title: dataStreamTitle,
release,
type,
package: pkgName,
dataset: dataset || `${pkgName}.${dataStreamPath}`,
path: dataStreamPath,
elasticsearch: parsedElasticsearchEntry,
};

if (ingestPipeline) {
dataStreamObject.ingest_pipeline = ingestPipeline;
}

if (streams.length) {
dataStreamObject.streams = streams;
}

dataStreams.push(
Object.entries(restOfProps).reduce(
(validatedDataStream, [key, value]) => {
if (registryDataStreamProps.includes(key as RegistryDataStreamKeys)) {
// @ts-expect-error
validatedDataStream[key] = value;
}
return validatedDataStream;
},
{
title: dataStreamTitle,
release,
type,
package: pkgName,
dataset: dataset || `${pkgName}.${dataStreamPath}`,
ingest_pipeline: ingestPipeline,
path: dataStreamPath,
streams,
Object.entries(restOfProps).reduce((validatedDataStream, [key, value]) => {
if (registryDataStreamProps.includes(key as RegistryDataStreamKeys)) {
validatedDataStream[key] = value;
}
)
return validatedDataStream;
}, dataStreamObject)
);
});

Expand All @@ -261,25 +350,28 @@ export function parseAndVerifyStreams(
`Invalid manifest for data stream ${dataStreamPath}: stream is missing one or more fields of: input, title`
);
}

const vars = parseAndVerifyVars(manifestVars, `data stream ${dataStreamPath}`);

const streamObject: RegistryStream = {
input,
title: streamTitle,
template_path: templatePath || 'stream.yml.hbs',
};

if (vars.length) {
streamObject.vars = vars;
}

// default template path name see https://github.com/elastic/package-registry/blob/master/util/dataset.go#L143
kpollich marked this conversation as resolved.
Show resolved Hide resolved
streams.push(
Object.entries(restOfProps).reduce(
(validatedStream, [key, value]) => {
if (registryStreamProps.includes(key as RegistryStreamKeys)) {
// @ts-expect-error
validatedStream[key] = value;
}
return validatedStream;
},
{
input,
title: streamTitle,
vars,
template_path: templatePath || 'stream.yml.hbs',
} as RegistryStream
)
Object.entries(restOfProps).reduce((validatedStream, [key, value]) => {
if (registryStreamProps.includes(key as RegistryStreamKeys)) {
// @ts-expect-error
validatedStream[key] = value;
}
return validatedStream;
}, streamObject)
);
});
}
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/services/epm/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ export async function ensureCachedArchiveInfo(
}
}

async function fetchArchiveBuffer(
export async function fetchArchiveBuffer(
pkgName: string,
pkgVersion: string
): Promise<{ archiveBuffer: Buffer; archivePath: string }> {
Expand Down
3 changes: 2 additions & 1 deletion x-pack/test/fleet_api_integration/apis/epm/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./file'));
loadTestFile(require.resolve('./template'));
loadTestFile(require.resolve('./ilm'));
// loadTestFile(require.resolve('./install_bundled'));
loadTestFile(require.resolve('./install_bundled'));
Copy link
Member Author

Choose a reason for hiding this comment

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

This was somehow commented out and missed in a previous PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I had to make some additional changes to get these tests passing. Since they were commented out incidentally, there were some gotchas to sort out with bundled package tests in CI. Namely, we needed a way to place fixture test files in the bundled package directory, which proved difficult in an integration test environment where we'd need to mutate the build/ directory in order to properly copy over fixture files.

The solution I landed on with some input from the ops folks was to introduce a developer.bundledPackageLocation option that allows us to override Fleet's default bundled package location with one /tmp to prevent mutation of the build artifact during CI. It was quite the runaround (the commit log for this PR is a sight to behold), but I think this is probably the cleanest solution we can have to support this very specific use case in tests.

loadTestFile(require.resolve('./install_by_upload'));
loadTestFile(require.resolve('./install_endpoint'));
loadTestFile(require.resolve('./install_overrides'));
Expand All @@ -29,5 +29,6 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./package_install_complete'));
loadTestFile(require.resolve('./install_error_rollback'));
loadTestFile(require.resolve('./final_pipeline'));
loadTestFile(require.resolve('./validate_bundled_packages'));
});
}
2 changes: 1 addition & 1 deletion x-pack/test/fleet_api_integration/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { defineDockerServersConfig } from '@kbn/test';
// example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1.
// It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry.
export const dockerImage =
'docker.elastic.co/package-registry/distribution@sha256:8b4ce36ecdf86e6cfdf781d9df8d564a014add9afc9aec21cf2c5a68ff82d3ab';
'docker.elastic.co/package-registry/distribution@sha256:b3dfc6a11ff7dce82ba8689ea9eeb54e353c6b4bfd2d28127b20ef72fd8883e9';

export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
Expand Down
2 changes: 1 addition & 1 deletion x-pack/test/functional/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { pageObjects } from './page_objects';
// example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1.
// It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry.
export const dockerImage =
'docker.elastic.co/package-registry/distribution@sha256:8b4ce36ecdf86e6cfdf781d9df8d564a014add9afc9aec21cf2c5a68ff82d3ab';
'docker.elastic.co/package-registry/distribution@sha256:b3dfc6a11ff7dce82ba8689ea9eeb54e353c6b4bfd2d28127b20ef72fd8883e9';

// the default export of config files must be a config provider
// that returns an object with the projects config values
Expand Down
Loading