Skip to content

Commit

Permalink
feat: add k8skonfig.yaml file for declarative crd configs (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
shinebayar-g authored Dec 25, 2024
1 parent e202cbe commit 572272a
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 164 deletions.
8 changes: 8 additions & 0 deletions examples/k8skonfig.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
outDir: crds
crds:
argocd:
- https://raw.githubusercontent.com/argoproj/argo-cd/refs/tags/v2.13.2/manifests/crds/application-crd.yaml
- https://raw.githubusercontent.com/argoproj/argo-cd/refs/tags/v2.13.2/manifests/crds/applicationset-crd.yaml
- https://raw.githubusercontent.com/argoproj/argo-cd/refs/tags/v2.13.2/manifests/crds/appproject-crd.yaml
external-secrets:
- https://raw.githubusercontent.com/external-secrets/external-secrets/refs/tags/v0.12.1/deploy/crds/bundle.yaml
1 change: 1 addition & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"private": true,
"type": "module",
"dependencies": {
"@k8skonf/cli": "workspace:*",
"@k8skonf/core": "workspace:*"
}
}
215 changes: 215 additions & 0 deletions packages/cli/src/generateCRDs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { log } from 'node:console';
import * as crypto from 'node:crypto';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { compile } from 'json-schema-to-typescript';
import pc from 'picocolors';
import { ClassDeclarationStructure, Project, StructureKind } from 'ts-morph';
import * as yaml from 'yaml';
import { formatCode } from './utils.js';

interface CRD {
apiVersion: string;
kind: string;
metadata: {
name: string;
};
spec: {
group: string;
versions: {
name: string;
schema?: {
openAPIV3Schema?: any;
};
}[];
names: {
categories?: string[];
kind: string;
listKind?: string;
plural: string;
shortNames?: string[];
singular?: string;
};
scope: 'Namespaced' | 'Cluster';
};
}

interface K8sKonfig {
/**
* @default "./crds"
*/
outDir?: string;
crds?: {
[pkg: string]: string[];
};
}

const cacheHome = path.join(os.homedir(), '.cache', 'k8skonf');

export function readConfig(configPath: string) {
const config: K8sKonfig = yaml.parse(fs.readFileSync(configPath, 'utf8'));
return config;
}

async function fetchAndParseCRDs(crds: CRD[], url: string, cacheDir: string) {
const cacheId = crypto.createHash('sha1').update(url).digest('hex');
const yamlSavePath = path.join(cacheDir, `${cacheId}-${path.basename(url)}`);
if (!fs.existsSync(yamlSavePath)) {
log(`Fetching CRDs from ${pc.yellowBright(url.toString())}`);
const req = await fetch(url);
const data = await req.text();
fs.writeFileSync(yamlSavePath, data);
} else {
log(`Reading CRDs from ${pc.yellowBright(url)} (cached)`);
}
yaml.parseAllDocuments(fs.readFileSync(yamlSavePath, 'utf-8')).forEach((c) =>
crds.push(c.toJS()),
);
}

interface Output {
[dir: string]: CRD[];
}

export async function generateCRDs(crdPathOrUrl?: string, config?: K8sKonfig) {
const project = new Project();
const outDir = path.resolve(config?.outDir || './crds');
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(cacheHome, { recursive: true });
const output: Output = {};

if (crdPathOrUrl) {
output[''] = [];
try {
const url = new URL(crdPathOrUrl);
await fetchAndParseCRDs(output[''], url.toString(), cacheHome);
} catch (e) {
log(`Reading CRDs from ${pc.yellowBright(crdPathOrUrl)}`);
yaml.parseAllDocuments(fs.readFileSync(crdPathOrUrl, 'utf-8')).forEach((c) =>
output[''].push(c.toJS()),
);
}
} else if (config) {
for (const [pkg, urls] of Object.entries(config.crds || {})) {
output[pkg] = [];
const cacheDir = path.join(cacheHome, pkg);
fs.mkdirSync(cacheDir, { recursive: true });
for (const url of urls) {
await fetchAndParseCRDs(output[pkg], url, cacheDir);
}
}
}

for (const [pkg, crds] of Object.entries(output)) {
for (const crd of crds) {
for (const crdVersion of crd.spec.versions) {
if (crdVersion.schema?.openAPIV3Schema) {
const className = `${crd.spec.names.kind}${crdVersion.name}`;
const compiledInterface = await compile(
crdVersion.schema.openAPIV3Schema,
`${className}Args`,
{
bannerComment: '',
additionalProperties: false,
format: false,
},
);
const fileLocation = path.join(outDir, pkg, `${className}.ts`);
const sourceFile = project.createSourceFile(fileLocation, compiledInterface, {
overwrite: true,
});
const isNamespaced = crd.spec.scope === 'Namespaced';

const interfaceDecleration = sourceFile.getInterfaces()[0];
interfaceDecleration.getProperty('apiVersion')?.remove();
interfaceDecleration.getProperty('kind')?.remove();
interfaceDecleration.getProperty('status')?.remove();
const metadata = interfaceDecleration.getProperty('metadata');
metadata?.setIsReadonly(true);
metadata?.setHasQuestionToken(true);
const spec = interfaceDecleration.getProperty('spec');
spec?.setIsReadonly(true);
sourceFile.addImportDeclaration({
moduleSpecifier: '@k8skonf/core',
namedImports: ['K8sApp'],
});
if (isNamespaced) {
metadata?.setType('NamespacedObjectMetav1');
sourceFile.addImportDeclaration({
moduleSpecifier: '@k8skonf/core',
namedImports: ['NamespacedApiObject', 'NamespacedObjectMetav1'],
});
} else {
metadata?.setType('ObjectMetav1');
sourceFile.addImportDeclaration({
moduleSpecifier: '@k8skonf/core',
namedImports: ['ApiObject', 'ObjectMetav1'],
});
}

const classDecleration: ClassDeclarationStructure = {
kind: StructureKind.Class,
name: interfaceDecleration.getName().replace('Args', ''),
extends: isNamespaced ? 'NamespacedApiObject' : 'ApiObject',
isExported: true,
properties: [
{
name: 'apiVersion',
initializer: `'${crd.spec.group}/${crdVersion.name}'`,
isReadonly: true,
},
{
name: 'kind',
initializer: `'${crd.spec.names.kind}'`,
isReadonly: true,
},
{
name: 'metadata',
type: isNamespaced ? 'NamespacedObjectMetav1' : 'ObjectMetav1',
isReadonly: true,
},
{
name: 'spec',
type: `${interfaceDecleration.getName()}['spec']`,
isReadonly: true,
},
],
ctors: [
{
parameters: [
{
name: 'app',
type: 'K8sApp',
},
{
name: 'name',
type: 'string',
},
{
name: 'args',
type: interfaceDecleration.getName(),
},
],
statements: [
'super(args.metadata?.name || name);',
'this.metadata = {',
' name: args.metadata?.name || name,',
' ...args.metadata,',
'};',
'this.spec = args.spec;',
'app.addResource(this);',
],
},
],
};
sourceFile.addClass(classDecleration);
}
}
}
}

log(`Saving generated files to the ${pc.yellowBright(outDir)} directory`);
project.saveSync();
formatCode(outDir);
}
Loading

0 comments on commit 572272a

Please sign in to comment.