@@ -25,18 +25,27 @@ import {
2525import { useApi , useRouteRef } from '@backstage/core-plugin-api' ;
2626import { makeStyles , TextField , Typography } from '@material-ui/core' ;
2727import { Alert } from '@material-ui/lab' ;
28+ import { dump } from 'js-yaml' ;
2829import React , { Fragment , useEffect , useRef , useState } from 'react' ;
2930import { useNavigate , useParams } from 'react-router-dom' ;
3031import useAsync from 'react-use/lib/useAsync' ;
3132import { ConfigAsDataApi , configAsDataApiRef } from '../../apis' ;
3233import { packageRouteRef } from '../../routes' ;
34+ import { ConfigMap } from '../../types/ConfigMap' ;
3335import { Kptfile } from '../../types/Kptfile' ;
36+ import { KubernetesResource } from '../../types/KubernetesResource' ;
3437import {
3538 PackageRevision ,
3639 PackageRevisionLifecycle ,
3740} from '../../types/PackageRevision' ;
3841import { PackageRevisionResourcesMap } from '../../types/PackageRevisionResource' ;
3942import { Repository } from '../../types/Repository' ;
43+ import {
44+ findKptfileFunction ,
45+ getLatestFunction ,
46+ GroupFunctionsByName ,
47+ groupFunctionsByName ,
48+ } from '../../utils/function' ;
4049import {
4150 canCloneRevision ,
4251 getCloneTask ,
@@ -50,6 +59,7 @@ import {
5059 getRootKptfile ,
5160 PackageResource ,
5261 updateResourceInResourcesMap ,
62+ updateResourcesMap ,
5363} from '../../utils/packageRevisionResources' ;
5464import {
5565 ContentSummary ,
@@ -60,7 +70,7 @@ import {
6070import { sortByLabel } from '../../utils/selectItem' ;
6171import { emptyIfUndefined , toLowerCase } from '../../utils/string' ;
6272import { dumpYaml , loadYaml } from '../../utils/yaml' ;
63- import { Select } from '../Controls/Select ' ;
73+ import { Checkbox , Select } from '../Controls' ;
6474import { PackageLink , RepositoriesLink , RepositoryLink } from '../Links' ;
6575
6676const useStyles = makeStyles ( ( ) => ( {
@@ -96,6 +106,10 @@ type KptfileState = {
96106 site : string ;
97107} ;
98108
109+ type ValidateResourcesState = {
110+ setKubeval : boolean ;
111+ } ;
112+
99113const 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+
128180export 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 = { {
0 commit comments