Skip to content

Commit

Permalink
Removed a bad flag, added tests
Browse files Browse the repository at this point in the history
* Remove `--doNotCorrect` option, I misunderstood some of the docs
* Added testing workflow & some tests
* Misc configuration management
  • Loading branch information
nikitawootten committed Apr 23, 2023
1 parent 4e513eb commit f977fc7
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 23 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lib/
24 changes: 24 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: CI/CD
on:
push:
branches:
- main
pull_request: {}
jobs:
lint-and-test:
name: Lint and Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 19
cache: npm
- name: Download dependencies
run: npm install
- name: Build
run: npm run build
- name: Lint
run: npm run lint
- name: Test
run: npm run test
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules/
lib/
.vscode/
scratch/
coverage/
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@ Thankfully OpenAPIv3 gets us 95% the way there, as it uses JSON Schema to define
## Road-map

- [x] Transform simple `CustomResourceDefinitions`
- [x] Fix malformed `CustomResourceDefinitions` that include the whole document (instead of from the `spec` property)
- [ ] Handle YAML files that contain multiple documents
- [ ] Create a GitHub page to store schemas for common CRDs
18 changes: 18 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Config } from 'jest';

export default {
preset: 'ts-jest',
collectCoverage: false,
coverageDirectory: '<rootDir>/coverage',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
coverageProvider: 'v8',
testEnvironment: 'node',
modulePathIgnorePatterns: ['<rootDir>/lib'],
} as Config;
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crd2jsonschema",
"version": "0.1.0",
"version": "0.2.0",
"description": "A simple utility that converts a Kubernetes CustomResourceDefinition to a JSON Schema",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand All @@ -14,7 +14,8 @@
"scripts": {
"build": "tsc",
"lint": "eslint .",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest",
"coverage": "jest --coverage",
"prepare": "npm run build"
},
"repository": {
Expand Down
11 changes: 4 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,16 @@ async function retrieveResource(ref: string): Promise<string> {
program
.name('crd2jsonSchema')
.description('A simple utility that converts a Kubernetes CustomResourceDefinition to a JSON Schema')
.version('0.1.0')
.argument('<crd>', 'URI of the CRD to pull')
.option('-d, --doNotCorrect', 'Correct CRDs that define schemas from the root (e.g. Traefik)');
.version('0.2.0')
.argument('<crd>', 'URI of the CRD to pull');
program.parse(process.argv);
const options = program.opts<{
doNotCorrect: boolean;
}>();
// const options = program.opts<{}>();
const crdRef = program.args[0];

retrieveResource(crdRef)
.then((crdString) => {
const crd: K8sCrd = YAML.parse(crdString);
const schema = convert(crd, !options.doNotCorrect);
const schema = convert(crd);
console.log(JSON.stringify(schema, null, 2));
})
.catch((err) => {
Expand Down
44 changes: 44 additions & 0 deletions src/convert.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import fs from 'fs';
import YAML from 'yaml';
import convertCrd from './convert';

describe('convertCrd', () => {
it('should generate a single document schema', () => {
const crdString = fs.readFileSync('./test_data/simpleCrd.yaml', 'utf-8');
const crd = YAML.parse(crdString);
const schema = convertCrd(crd);

if (typeof schema === 'boolean') {
fail('Schema should not be of type boolean (this is just a type assertion)');
}

expect(schema.title).toBe('crontabs.stable.example.com');
expect(schema.type).toBe('object');

// Test generated crd properties

if (typeof schema.properties?.apiVersion === 'boolean') {
fail('Schema should not be of type boolean (this is just a type assertion)');
}
expect(schema.properties?.apiVersion?.['enum']).toStrictEqual(['stable.example.com/v1']);

if (typeof schema.properties?.kind === 'boolean') {
fail('Schema should not be of type boolean (this is just a type assertion)');
}
expect(schema.properties?.kind?.['const']).toBe('CronTab');

// Test schema inclusion

if (schema.allOf?.length !== 1 || typeof schema.allOf[0] === 'boolean') {
fail('Schema should have exactly one generated version that is not of type boolean');
}

expect(schema.allOf[0].if).toStrictEqual({
properties: {
apiVersion: {
const: 'stable.example.com/v1',
},
},
});
});
});
21 changes: 8 additions & 13 deletions src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import { K8sCrd } from './crd';
import type { JSONSchema7Definition } from 'json-schema';

export default function convert(crd: K8sCrd, correctCrdSchema = false): JSONSchema7Definition {
export default function convert(crd: K8sCrd): JSONSchema7Definition {
// version -> schema
const schemaVersions = crd.spec.versions.reduce<Record<string, JSONSchema7Definition>>((acc, version) => {
let spec = version.schema.openAPIV3Schema;
// Some CRDs like Traefik seem to define the schema from the root of the document instead of from the "spec" element
if (
correctCrdSchema &&
typeof spec === 'object' && // JSONSchema7Definition says spec could just be a boolean
spec.properties &&
// Simple heuristic to check if the schema is defined from the root
'apiVersion' in spec.properties &&
'spec' in spec.properties
) {
spec = spec.properties.spec;
if (typeof version.schema.openAPIV3Schema === 'boolean') {
throw new Error('Cannot operate on an openAPIV3Schema that contains a single root boolean element');
}
acc[`${crd.spec.group}/${version.name}`] = version.schema.openAPIV3Schema;
const spec = version.schema.openAPIV3Schema.properties?.spec;
if (spec === undefined) {
throw new Error('Property "spec" must be defined on the openAPIV3Schema');
}
acc[`${crd.spec.group}/${version.name}`] = spec;
return acc;
}, {});

Expand Down
41 changes: 41 additions & 0 deletions test_data/simpleCrd.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# via https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#create-a-customresourcedefinition
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: crontabs.stable.example.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: stable.example.com
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1
# Each version can be enabled/disabled by Served flag.
served: true
# One and only one version must be marked as the storage version.
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
cronSpec:
type: string
image:
type: string
replicas:
type: integer
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: crontabs
# singular name to be used as an alias on the CLI and for display
singular: crontab
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: CronTab
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- ct

0 comments on commit f977fc7

Please sign in to comment.