[Fleet] Add ability to specify which integration variables should be configurable (#97163)

This commit is contained in:
Zacqary Adam Xeper 2021-04-14 17:00:18 -05:00 committed by GitHub
parent d72a7afbf4
commit deaa7794d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 211 additions and 10 deletions

View file

@ -14,6 +14,7 @@ export interface PackagePolicyPackage {
export interface PackagePolicyConfigRecordEntry {
type?: string;
value?: any;
frozen?: boolean;
}
export type PackagePolicyConfigRecord = Record<string, PackagePolicyConfigRecordEntry>;

View file

@ -105,12 +105,13 @@ export const PackagePolicyInputConfig: React.FunctionComponent<{
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packagePolicyInput.vars![varName].value;
const { value, frozen } = packagePolicyInput.vars![varName];
return (
<EuiFlexItem key={varName}>
<PackagePolicyInputVarField
varDef={varDef}
value={value}
frozen={frozen}
onChange={(newValue: any) => {
updatePackagePolicyInput({
vars: {

View file

@ -106,12 +106,13 @@ export const PackagePolicyInputStreamConfig: React.FunctionComponent<{
<EuiFlexGroup direction="column" gutterSize="m">
{requiredVars.map((varDef) => {
const { name: varName, type: varType } = varDef;
const value = packagePolicyInputStream.vars![varName].value;
const { value, frozen } = packagePolicyInputStream.vars![varName];
return (
<EuiFlexItem key={varName}>
<PackagePolicyInputVarField
varDef={varDef}
value={value}
frozen={frozen}
onChange={(newValue: any) => {
updatePackagePolicyInputStream({
vars: {

View file

@ -15,6 +15,7 @@ import {
EuiComboBox,
EuiText,
EuiCodeEditor,
EuiTextArea,
EuiFieldPassword,
} from '@elastic/eui';
@ -29,7 +30,8 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
onChange: (newValue: any) => void;
errors?: string[] | null;
forceShowErrors?: boolean;
}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors }) => {
frozen?: boolean;
}> = memo(({ varDef, value, onChange, errors: varErrors, forceShowErrors, frozen }) => {
const [isDirty, setIsDirty] = useState<boolean>(false);
const { multi, required, type, title, name, description } = varDef;
const isInvalid = (isDirty || forceShowErrors) && !!varErrors;
@ -50,12 +52,20 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
onChange(newVals.map((val) => val.label));
}}
onBlur={() => setIsDirty(true)}
isDisabled={frozen}
/>
);
}
switch (type) {
case 'yaml':
return (
return frozen ? (
<EuiTextArea
className="ace_editor"
disabled
value={value}
style={{ height: '175px', padding: '4px', whiteSpace: 'pre', resize: 'none' }}
/>
) : (
<EuiCodeEditor
width="100%"
mode="yaml"
@ -79,6 +89,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
showLabel={false}
onChange={(e) => onChange(e.target.checked)}
onBlur={() => setIsDirty(true)}
disabled={frozen}
/>
);
case 'password':
@ -89,6 +100,7 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
value={value === undefined ? '' : value}
onChange={(e) => onChange(e.target.value)}
onBlur={() => setIsDirty(true)}
disabled={frozen}
/>
);
default:
@ -98,10 +110,11 @@ export const PackagePolicyInputVarField: React.FunctionComponent<{
value={value === undefined ? '' : value}
onChange={(e) => onChange(e.target.value)}
onBlur={() => setIsDirty(true)}
disabled={frozen}
/>
);
}
}, [isInvalid, multi, onChange, type, value, fieldLabel]);
}, [isInvalid, multi, onChange, type, value, fieldLabel, frozen]);
// Boolean cannot be optional by default set to false
const isOptional = useMemo(() => type !== 'bool' && !required, [required, type]);

View file

@ -11,10 +11,10 @@ import {
httpServerMock,
} from 'src/core/server/mocks';
import type { SavedObjectsUpdateResponse } from 'src/core/server';
import type { SavedObjectsClient, SavedObjectsUpdateResponse } from 'src/core/server';
import type { KibanaRequest } from 'kibana/server';
import type { PackageInfo, PackagePolicySOAttributes } from '../types';
import type { PackageInfo, PackagePolicySOAttributes, AgentPolicySOAttributes } from '../types';
import { createPackagePolicyMock } from '../../common/mocks';
import type { ExternalCallback } from '..';
@ -68,6 +68,26 @@ jest.mock('./epm/registry', () => {
};
});
jest.mock('./agent_policy', () => {
return {
agentPolicyService: {
get: async (soClient: SavedObjectsClient, id: string) => {
const agentPolicySO = await soClient.get<AgentPolicySOAttributes>(
'ingest-agent-policies',
id
);
if (!agentPolicySO) {
return null;
}
const agentPolicy = { id: agentPolicySO.id, ...agentPolicySO.attributes };
agentPolicy.package_policies = [];
return agentPolicy;
},
bumpRevision: () => {},
},
};
});
describe('Package policy service', () => {
describe('compilePackagePolicyInputs', () => {
it('should work with config variables from the stream', async () => {
@ -346,8 +366,8 @@ describe('Package policy service', () => {
});
savedObjectsClient.update.mockImplementation(
async (
type: string,
id: string
_type: string,
_id: string
): Promise<SavedObjectsUpdateResponse<PackagePolicySOAttributes>> => {
throw savedObjectsClient.errors.createConflictError('abc', '123');
}
@ -362,6 +382,131 @@ describe('Package policy service', () => {
)
).rejects.toThrow('Saved object [abc/123] conflict');
});
it('should only update input vars that are not frozen', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
const mockPackagePolicy = createPackagePolicyMock();
const mockInputs = [
{
config: {},
enabled: true,
type: 'endpoint',
vars: {
dog: {
type: 'text',
value: 'dalmatian',
},
cat: {
type: 'text',
value: 'siamese',
frozen: true,
},
},
streams: [
{
data_stream: {
type: 'birds',
dataset: 'migratory.patterns',
},
enabled: false,
id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
vars: {
paths: {
value: ['north', 'south'],
type: 'text',
frozen: true,
},
period: {
value: '6mo',
type: 'text',
},
},
},
],
},
];
const inputsUpdate = [
{
config: {},
enabled: true,
type: 'endpoint',
vars: {
dog: {
type: 'text',
value: 'labrador',
},
cat: {
type: 'text',
value: 'tabby',
},
},
streams: [
{
data_stream: {
type: 'birds',
dataset: 'migratory.patterns',
},
enabled: false,
id: `endpoint-migratory.patterns-${mockPackagePolicy.id}`,
vars: {
paths: {
value: ['east', 'west'],
type: 'text',
},
period: {
value: '12mo',
type: 'text',
},
},
},
],
},
];
const attributes = {
...mockPackagePolicy,
inputs: mockInputs,
};
savedObjectsClient.get.mockResolvedValue({
id: 'test',
type: 'abcd',
references: [],
version: 'test',
attributes,
});
savedObjectsClient.update.mockImplementation(
async (
type: string,
id: string,
attrs: any
): Promise<SavedObjectsUpdateResponse<PackagePolicySOAttributes>> => {
savedObjectsClient.get.mockResolvedValue({
id: 'test',
type: 'abcd',
references: [],
version: 'test',
attributes: attrs,
});
return attrs;
}
);
const elasticsearchClient = elasticsearchServiceMock.createClusterClient().asInternalUser;
const result = await packagePolicyService.update(
savedObjectsClient,
elasticsearchClient,
'the-package-policy-id',
{ ...mockPackagePolicy, inputs: inputsUpdate }
);
const [modifiedInput] = result.inputs;
expect(modifiedInput.vars!.dog.value).toEqual('labrador');
expect(modifiedInput.vars!.cat.value).toEqual('siamese');
const [modifiedStream] = modifiedInput.streams;
expect(modifiedStream.vars!.paths.value).toEqual(expect.arrayContaining(['north', 'south']));
expect(modifiedStream.vars!.period.value).toEqual('12mo');
});
});
describe('runExternalCallbacks', () => {

View file

@ -23,6 +23,7 @@ import type {
DeletePackagePoliciesResponse,
PackagePolicyInput,
NewPackagePolicyInput,
PackagePolicyConfigRecordEntry,
PackagePolicyInputStream,
PackageInfo,
ListWithKuery,
@ -346,6 +347,8 @@ class PackagePolicyService {
assignStreamIdToInput(oldPackagePolicy.id, input)
);
inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs);
if (packagePolicy.package?.name) {
const pkgInfo = await getPackageInfo({
savedObjectsClient: soClient,
@ -602,6 +605,42 @@ async function _compilePackageStream(
return { ...stream };
}
function enforceFrozenInputs(oldInputs: PackagePolicyInput[], newInputs: PackagePolicyInput[]) {
const resultInputs = [...newInputs];
for (const input of resultInputs) {
const oldInput = oldInputs.find((i) => i.type === input.type);
if (input.vars && oldInput?.vars) {
input.vars = _enforceFrozenVars(oldInput.vars, input.vars);
}
if (input.streams && oldInput?.streams) {
for (const stream of input.streams) {
const oldStream = oldInput.streams.find((s) => s.id === stream.id);
if (stream.vars && oldStream?.vars) {
stream.vars = _enforceFrozenVars(oldStream.vars, stream.vars);
}
}
}
}
return resultInputs;
}
function _enforceFrozenVars(
oldVars: Record<string, PackagePolicyConfigRecordEntry>,
newVars: Record<string, PackagePolicyConfigRecordEntry>
) {
const resultVars: Record<string, PackagePolicyConfigRecordEntry> = {};
for (const [key, val] of Object.entries(oldVars)) {
if (val.frozen) {
resultVars[key] = val;
} else {
resultVars[key] = newVars[key];
}
}
return resultVars;
}
export type PackagePolicyServiceInterface = PackagePolicyService;
export const packagePolicyService = new PackagePolicyService();

View file

@ -16,7 +16,8 @@ const varsSchema = schema.maybe(
schema.object({
name: schema.string(),
type: schema.maybe(schema.string()),
value: schema.oneOf([schema.string(), schema.number()]),
value: schema.maybe(schema.oneOf([schema.string(), schema.number()])),
frozen: schema.maybe(schema.boolean()),
})
)
);