Skip to content

Commit 1a9800c

Browse files
dflemstrfrankfarzan
authored andcommitted
Add a basic demo function using kubeval
1 parent e58df16 commit 1a9800c

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
FROM node:10-alpine as builder
2+
3+
RUN mkdir -p /home/node/app && \
4+
chown -R node:node /home/node/app
5+
6+
USER node
7+
8+
WORKDIR /home/node/app
9+
10+
# Install dependencies and cache them.
11+
COPY --chown=node:node package*.json ./
12+
RUN npm ci
13+
14+
# Build the source.
15+
COPY --chown=node:node tsconfig.json .
16+
COPY --chown=node:node src src
17+
RUN npm run build && \
18+
npm prune --production && \
19+
rm -r src tsconfig.json
20+
21+
#############################################
22+
23+
FROM node:10-alpine
24+
25+
RUN apk add curl && \
26+
curl -sSLf https://github.com/instrumenta/kubeval/releases/download/0.14.0/kubeval-linux-amd64.tar.gz | \
27+
tar xzf - -C /usr/local/bin
28+
29+
# Run as non-root user as a best-practices:
30+
# https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md
31+
USER node
32+
33+
WORKDIR /home/node/app
34+
35+
COPY --from=builder /home/node/app /home/node/app
36+
37+
ENTRYPOINT ["node", "/home/node/app/dist/kubeval_run.js"]

ts/demo-functions/src/kubeval.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import {
2+
Configs,
3+
KubernetesObject,
4+
kubernetesObjectResult,
5+
Result,
6+
} from 'kpt-functions';
7+
import { ChildProcess, spawn } from 'child_process';
8+
import { Writable } from 'stream';
9+
10+
const SCHEMA_LOCATION = 'schema_location';
11+
const ADDITIONAL_SCHEMA_LOCATIONS = 'additional_schema_locations';
12+
const IGNORE_MISSING_SCHEMAS = 'ignore_missing_schemas';
13+
const SKIP_KINDS = 'skip_kinds';
14+
const STRICT = 'strict';
15+
16+
type Feedback = FeedbackItem[];
17+
18+
interface FeedbackItem {
19+
filename: string;
20+
kind: string;
21+
status: 'valid' | 'invalid';
22+
errors: string[];
23+
}
24+
25+
export async function kubeval(configs: Configs): Promise<void> {
26+
const schemaLocation = configs.getFunctionConfigValue(SCHEMA_LOCATION);
27+
const additionalSchemaLocationsStr = configs.getFunctionConfigValue(
28+
ADDITIONAL_SCHEMA_LOCATIONS
29+
);
30+
const additionalSchemaLocations = additionalSchemaLocationsStr
31+
? additionalSchemaLocationsStr.split(',')
32+
: [];
33+
const ignoreMissingSchemas = JSON.parse(
34+
configs.getFunctionConfigValue(IGNORE_MISSING_SCHEMAS) || 'false'
35+
);
36+
const skipKindsStr = configs.getFunctionConfigValue(SKIP_KINDS);
37+
const skipKinds = skipKindsStr ? skipKindsStr.split(',') : [];
38+
const strict = JSON.parse(configs.getFunctionConfigValue(STRICT) || 'false');
39+
40+
const results: Result[] = [];
41+
42+
for (const object of configs.getAll()) {
43+
await runKubeval(
44+
object,
45+
results,
46+
schemaLocation,
47+
additionalSchemaLocations,
48+
ignoreMissingSchemas,
49+
skipKinds,
50+
strict
51+
);
52+
}
53+
54+
if (results.length > 0) {
55+
configs.addResults(...results);
56+
}
57+
}
58+
59+
async function runKubeval(
60+
object: KubernetesObject,
61+
results: Result[],
62+
schemaLocation?: string,
63+
additionalSchemaLocations?: string[],
64+
ignoreMissingSchemas?: boolean,
65+
skipKinds?: string[],
66+
strict?: boolean
67+
): Promise<void> {
68+
const args = ['--output', 'json'];
69+
70+
if (schemaLocation) {
71+
args.push('--schema-location');
72+
args.push(schemaLocation);
73+
}
74+
75+
if (additionalSchemaLocations) {
76+
args.push('--additional-schema-locations');
77+
args.push(additionalSchemaLocations.join(','));
78+
}
79+
80+
if (ignoreMissingSchemas) {
81+
args.push('--ignore-missing-schemas');
82+
}
83+
84+
if (skipKinds) {
85+
args.push('--skip-kinds');
86+
args.push(skipKinds.join(','));
87+
}
88+
89+
if (strict) {
90+
args.push('--strict');
91+
}
92+
93+
const kubevalProcess = spawn('kubeval', args, {
94+
stdio: ['pipe', 'pipe', process.stderr],
95+
});
96+
const serializedObject = JSON.stringify(object);
97+
await writeToStream(kubevalProcess.stdin, serializedObject);
98+
kubevalProcess.stdin.end();
99+
const rawOutput = await readStdoutToString(kubevalProcess);
100+
try {
101+
const feedback = JSON.parse(rawOutput) as Feedback;
102+
103+
for (const { status, errors } of feedback) {
104+
if (status !== 'valid') {
105+
for (const error of errors) {
106+
const [path, ...rest] = error.split(':');
107+
let result;
108+
if (rest.length > 0) {
109+
result = kubernetesObjectResult(
110+
rest.join(':').trim(),
111+
object,
112+
{
113+
path,
114+
},
115+
'error'
116+
);
117+
} else {
118+
result = kubernetesObjectResult(error, object, undefined, 'error');
119+
}
120+
results.push(result);
121+
}
122+
}
123+
}
124+
} catch (error) {
125+
results.push(
126+
kubernetesObjectResult(
127+
'Failed to parse raw kubeval output:\n' +
128+
error.message +
129+
'\n\n' +
130+
rawOutput,
131+
object
132+
)
133+
);
134+
}
135+
}
136+
137+
function writeToStream(stream: Writable, data: string): Promise<void> {
138+
return new Promise((resolve, reject) =>
139+
stream.write(data, 'utf-8', err => (err ? reject(err) : resolve()))
140+
);
141+
}
142+
143+
function readStdoutToString(childProcess: ChildProcess): Promise<string> {
144+
return new Promise<string>(resolve => {
145+
let result = '';
146+
childProcess.stdout!!.on('data', data => {
147+
result += data.toString();
148+
});
149+
childProcess.on('close', () => {
150+
resolve(result);
151+
});
152+
});
153+
}
154+
155+
kubeval.usage = `
156+
Validates configuration using kubeval.
157+
158+
Configured using a ConfigMap with the following keys:
159+
${SCHEMA_LOCATION}: Comma-seperated list of secondary base URLs used to download schemas.
160+
${ADDITIONAL_SCHEMA_LOCATIONS}: List of secondary base URLs used to download schemas.
161+
${IGNORE_MISSING_SCHEMAS}: Skip validation for resource definitions without a schema.
162+
${SKIP_KINDS}: Comma-separated list of case-sensitive kinds to skip when validating against schemas.
163+
${STRICT}: Disallow additional properties not in schema.
164+
`;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { kubeval } from './kubeval';
2+
import { run } from 'kpt-functions';
3+
4+
run(kubeval);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Configs, kubernetesObjectResult, TestRunner } from 'kpt-functions';
2+
import { kubeval } from './kubeval';
3+
import { Namespace, PodTemplateSpec } from './gen/io.k8s.api.core.v1';
4+
import { Deployment, DeploymentSpec } from './gen/io.k8s.api.apps.v1';
5+
import {
6+
LabelSelector,
7+
ObjectMeta,
8+
} from './gen/io.k8s.apimachinery.pkg.apis.meta.v1';
9+
10+
const RUNNER = new TestRunner(kubeval);
11+
12+
describe('kubeval', () => {
13+
it('handles objects without errors', async () => {
14+
await RUNNER.assert(
15+
new Configs([Namespace.named('something')]),
16+
new Configs([Namespace.named('something')])
17+
);
18+
});
19+
it('reacts on errors', async () => {
20+
const deployment = new Deployment({
21+
metadata: new ObjectMeta({
22+
name: 'something',
23+
}),
24+
spec: new DeploymentSpec({
25+
selector: new LabelSelector(),
26+
template: new PodTemplateSpec(),
27+
// schema violation:
28+
paused: ('horse' as unknown) as boolean,
29+
}),
30+
});
31+
await RUNNER.assert(
32+
new Configs([deployment]),
33+
new Configs([deployment], undefined, [
34+
kubernetesObjectResult(
35+
'Invalid type. Expected: [boolean,null], given: string',
36+
deployment,
37+
{
38+
path: 'spec.paused',
39+
}
40+
),
41+
])
42+
);
43+
});
44+
});

0 commit comments

Comments
 (0)