Skip to content
This repository was archived by the owner on Jun 18, 2025. It is now read-only.

Commit 66ac511

Browse files
feat: add validate resources step on add package page (#164)
This change adds the Validate Resources step to the Add Package Page. This step allows users to have their package resources validated against their schema by adding the kubeval validator function to the Kptfile.
1 parent fe73d0c commit 66ac511

File tree

5 files changed

+312
-15
lines changed

5 files changed

+312
-15
lines changed

plugins/cad/src/components/AddPackagePage/AddPackagePage.tsx

Lines changed: 192 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,27 @@ import {
2525
import { useApi, useRouteRef } from '@backstage/core-plugin-api';
2626
import { makeStyles, TextField, Typography } from '@material-ui/core';
2727
import { Alert } from '@material-ui/lab';
28+
import { dump } from 'js-yaml';
2829
import React, { Fragment, useEffect, useRef, useState } from 'react';
2930
import { useNavigate, useParams } from 'react-router-dom';
3031
import useAsync from 'react-use/lib/useAsync';
3132
import { ConfigAsDataApi, configAsDataApiRef } from '../../apis';
3233
import { packageRouteRef } from '../../routes';
34+
import { ConfigMap } from '../../types/ConfigMap';
3335
import { Kptfile } from '../../types/Kptfile';
36+
import { KubernetesResource } from '../../types/KubernetesResource';
3437
import {
3538
PackageRevision,
3639
PackageRevisionLifecycle,
3740
} from '../../types/PackageRevision';
3841
import { PackageRevisionResourcesMap } from '../../types/PackageRevisionResource';
3942
import { Repository } from '../../types/Repository';
43+
import {
44+
findKptfileFunction,
45+
getLatestFunction,
46+
GroupFunctionsByName,
47+
groupFunctionsByName,
48+
} from '../../utils/function';
4049
import {
4150
canCloneRevision,
4251
getCloneTask,
@@ -50,6 +59,7 @@ import {
5059
getRootKptfile,
5160
PackageResource,
5261
updateResourceInResourcesMap,
62+
updateResourcesMap,
5363
} from '../../utils/packageRevisionResources';
5464
import {
5565
ContentSummary,
@@ -60,7 +70,7 @@ import {
6070
import { sortByLabel } from '../../utils/selectItem';
6171
import { emptyIfUndefined, toLowerCase } from '../../utils/string';
6272
import { dumpYaml, loadYaml } from '../../utils/yaml';
63-
import { Select } from '../Controls/Select';
73+
import { Checkbox, Select } from '../Controls';
6474
import { PackageLink, RepositoriesLink, RepositoryLink } from '../Links';
6575

6676
const useStyles = makeStyles(() => ({
@@ -96,6 +106,10 @@ type KptfileState = {
96106
site: string;
97107
};
98108

109+
type ValidateResourcesState = {
110+
setKubeval: boolean;
111+
};
112+
99113
const mapPackageRevisionToSelectItem = (
100114
packageRevision: PackageRevision,
101115
): PackageRevisionSelectItem => ({
@@ -125,6 +139,44 @@ const getPackageResources = async (
125139
return [resources, resourcesMap];
126140
};
127141

142+
const createResource = (
143+
apiVersion: string,
144+
kind: string,
145+
name: string,
146+
localConfig: boolean = true,
147+
): KubernetesResource => {
148+
const resource: KubernetesResource = {
149+
apiVersion: apiVersion,
150+
kind: kind,
151+
metadata: {
152+
name: name,
153+
},
154+
};
155+
156+
if (localConfig) {
157+
resource.metadata.annotations = {
158+
'config.kubernetes.io/local-config': 'true',
159+
};
160+
}
161+
162+
return resource;
163+
};
164+
165+
const addPackageResource = (
166+
packageResources: PackageResource[],
167+
resource: KubernetesResource,
168+
filename: string,
169+
): PackageResource => {
170+
const packageResource: PackageResource = {
171+
filename: filename,
172+
yaml: dumpYaml(resource),
173+
} as PackageResource;
174+
175+
packageResources.push(packageResource);
176+
177+
return packageResource;
178+
};
179+
128180
export const AddPackagePage = ({ action }: AddPackagePageProps) => {
129181
const api = useApi(configAsDataApiRef);
130182
const classes = useStyles();
@@ -157,6 +209,11 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
157209
site: '',
158210
});
159211

212+
const [validateResourcesState, setValidateResourcesState] =
213+
useState<ValidateResourcesState>({
214+
setKubeval: true,
215+
});
216+
160217
const [isCreatingPackage, setIsCreatingPackage] = useState<boolean>(false);
161218

162219
const [
@@ -303,6 +360,15 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
303360
keywords: emptyIfUndefined(thisKptfile.info?.keywords?.join(', ')),
304361
site: emptyIfUndefined(thisKptfile.info?.site),
305362
});
363+
364+
const kubevalValidatorFn = findKptfileFunction(
365+
thisKptfile.pipeline?.validators || [],
366+
'kubeval',
367+
);
368+
369+
setValidateResourcesState({
370+
setKubeval: !!kubevalValidatorFn,
371+
});
306372
};
307373

308374
updateKptfileState(sourcePackageRevision.metadata.name);
@@ -313,6 +379,10 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
313379
keywords: '',
314380
site: '',
315381
});
382+
383+
setValidateResourcesState({
384+
setKubeval: true,
385+
});
316386
}
317387
}, [api, sourcePackageRevision]);
318388

@@ -346,17 +416,89 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
346416
return thisPackage?.spec.packageName || packageName;
347417
};
348418

349-
const updatePackageResources = async (
350-
thisPackageName: string,
351-
): Promise<void> => {
352-
const updateRequired = !!sourcePackageRevision;
419+
const applyValidateResourcesState = async (
420+
resourcesMap: PackageRevisionResourcesMap,
421+
kptFunctions: GroupFunctionsByName,
422+
): Promise<PackageRevisionResourcesMap> => {
423+
const resources = getPackageResourcesFromResourcesMap(resourcesMap);
353424

354-
if (!updateRequired) return;
425+
const kptfileResource = getRootKptfile(resources);
426+
const kptfileYaml = loadYaml(kptfileResource.yaml) as Kptfile;
427+
kptfileYaml.pipeline = kptfileYaml.pipeline || {};
428+
kptfileYaml.pipeline.validators = kptfileYaml.pipeline.validators || [];
429+
const validators = kptfileYaml.pipeline.validators;
430+
431+
const newPackageResources: PackageResource[] = [];
432+
const updatedPackageResources: PackageResource[] = [];
433+
const deletedPackgeResources: PackageResource[] = [];
434+
435+
const kubevalValidatorFn = findKptfileFunction(validators, 'kubeval');
436+
437+
if (validateResourcesState.setKubeval) {
438+
if (!kubevalValidatorFn) {
439+
const kubevalFn = getLatestFunction(kptFunctions, 'kubeval');
440+
441+
const kubevalConfigResource: ConfigMap = {
442+
...createResource('v1', 'ConfigMap', 'kubeval-config'),
443+
data: {
444+
ignore_missing_schemas: 'true',
445+
},
446+
};
447+
448+
const kubevalConfigPackageResource = addPackageResource(
449+
newPackageResources,
450+
kubevalConfigResource,
451+
'kubeval-config.yaml',
452+
);
453+
454+
validators.push({
455+
image: kubevalFn.spec.image,
456+
configPath: kubevalConfigPackageResource.filename,
457+
});
458+
459+
kptfileResource.yaml = dump(kptfileYaml);
460+
updatedPackageResources.push(kptfileResource);
461+
}
462+
} else {
463+
if (kubevalValidatorFn) {
464+
kptfileYaml.pipeline.validators = validators.filter(
465+
fn => fn !== kubevalValidatorFn,
466+
);
355467

356-
const [resources, resourcesMap] = await getPackageResources(
357-
api,
358-
thisPackageName,
468+
kptfileResource.yaml = dump(kptfileYaml);
469+
updatedPackageResources.push(kptfileResource);
470+
471+
if (kubevalValidatorFn.configPath) {
472+
const referenceResource = resources.find(
473+
resource =>
474+
resource.filename === kubevalValidatorFn.configPath &&
475+
!!resource.isLocalConfigResource,
476+
);
477+
if (referenceResource) {
478+
deletedPackgeResources.push(referenceResource);
479+
}
480+
}
481+
}
482+
}
483+
484+
return updateResourcesMap(
485+
resourcesMap,
486+
newPackageResources,
487+
updatedPackageResources,
488+
deletedPackgeResources,
359489
);
490+
};
491+
492+
const updateKptfileInfo = (
493+
resourcesMap: PackageRevisionResourcesMap,
494+
): PackageRevisionResourcesMap => {
495+
const isClonePackageAction = !!sourcePackageRevision;
496+
497+
if (!isClonePackageAction) {
498+
return resourcesMap;
499+
}
500+
501+
const resources = getPackageResourcesFromResourcesMap(resourcesMap);
360502

361503
const kptfileResource = getRootKptfile(resources);
362504

@@ -376,11 +518,32 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
376518
updatedKptfileYaml,
377519
);
378520

379-
const packageRevisionResources = getPackageRevisionResourcesResource(
380-
thisPackageName,
381-
updatedResourceMap,
521+
return updatedResourceMap;
522+
};
523+
524+
const updatePackageResources = async (
525+
newPackageName: string,
526+
): Promise<void> => {
527+
const allKptFunctions = await api.listCatalogFunctions();
528+
const kptFunctions = groupFunctionsByName(allKptFunctions);
529+
530+
const [_, resourcesMap] = await getPackageResources(api, newPackageName);
531+
532+
let updatedResourcesMap = await applyValidateResourcesState(
533+
resourcesMap,
534+
kptFunctions,
382535
);
383-
await api.replacePackageRevisionResources(packageRevisionResources);
536+
537+
updatedResourcesMap = updateKptfileInfo(updatedResourcesMap);
538+
539+
if (updatedResourcesMap !== resourcesMap) {
540+
const packageRevisionResources = getPackageRevisionResourcesResource(
541+
newPackageName,
542+
updatedResourcesMap,
543+
);
544+
545+
await api.replacePackageRevisionResources(packageRevisionResources);
546+
}
384547
};
385548

386549
const createPackage = async (): Promise<void> => {
@@ -581,6 +744,22 @@ export const AddPackagePage = ({ action }: AddPackagePageProps) => {
581744
</div>
582745
</SimpleStepperStep>
583746

747+
<SimpleStepperStep title="Validate Resources">
748+
<div className={classes.stepContent}>
749+
<Checkbox
750+
label="Validate resources for any OpenAPI schema errors"
751+
checked={validateResourcesState.setKubeval}
752+
onChange={isChecked =>
753+
setValidateResourcesState(s => ({
754+
...s,
755+
setKubeval: isChecked,
756+
}))
757+
}
758+
helperText="This validates each resource ensuring it is syntactically correct against its schema. These errors will cause a resource not to deploy to a cluster correctly otherwise. Validation is limited to kubernetes built-in types and GCP CRDs."
759+
/>
760+
</div>
761+
</SimpleStepperStep>
762+
584763
<SimpleStepperStep
585764
title="Confirm"
586765
actions={{
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Copyright 2022 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import {
18+
Checkbox as MaterialCheckbox,
19+
FormControlLabel,
20+
FormHelperText,
21+
makeStyles,
22+
} from '@material-ui/core';
23+
import React, { ChangeEvent } from 'react';
24+
25+
type CheckboxProps = {
26+
label: string;
27+
checked: boolean;
28+
onChange: (checked: boolean) => void;
29+
helperText?: JSX.Element | string;
30+
};
31+
32+
const useStyles = makeStyles({
33+
description: {
34+
marginLeft: '32px',
35+
marginTop: '0',
36+
},
37+
});
38+
39+
export const Checkbox = ({
40+
label,
41+
checked,
42+
onChange,
43+
helperText,
44+
}: CheckboxProps) => {
45+
const classes = useStyles();
46+
47+
return (
48+
<div>
49+
<FormControlLabel
50+
control={<MaterialCheckbox checked={checked} color="primary" />}
51+
label={label}
52+
onChange={(_: ChangeEvent<{}>, isChecked: boolean) =>
53+
onChange(isChecked)
54+
}
55+
/>
56+
{helperText && (
57+
<FormHelperText className={classes.description}>
58+
{helperText}
59+
</FormHelperText>
60+
)}
61+
</div>
62+
);
63+
};

plugins/cad/src/components/Controls/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
export { Autocomplete } from './Autocomplete';
1818
export { ConfirmationDialog } from './ConfirmationDialog';
19+
export { Checkbox } from './Checkbox';
1920
export { IconButton } from './IconButton';
2021
export { MultiSelect } from './MultiSelect';
2122
export { PackageIcon } from './PackageIcon';

0 commit comments

Comments
 (0)