Skip to content

Commit e4ba415

Browse files
authored
feat(cli/gen-models): Allow generating models from external CRDs through a config file (#37)
2 parents a94bf0a + 70b04ef commit e4ba415

File tree

10 files changed

+163
-6
lines changed

10 files changed

+163
-6
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,12 @@ The CLI tool can be used to generate composition manifests from source files:
164164
functions/
165165
├── example1/
166166
│ ├── composition.fn.ts
167+
│ ├── xrd.yaml
167168
│ ├── package.json (optional)
168169
│ └── composition.yaml (optional)
169170
└── example2/
170171
├── composition.fn.ts
172+
├── xrd.yaml
171173
└── package.json
172174
```
173175

@@ -200,6 +202,33 @@ The CLI handles dependencies and yarn.lock files as follows:
200202

201203
This approach allows you to have function-specific dependencies or share dependencies across all functions.
202204

205+
### Generating models
206+
207+
To help in writing compositions, the CLI tool can generate type-safe models for
208+
the following:
209+
* Base Kubernetes resources
210+
* CRDs derived from the XRDs of your custom resources
211+
* External extra CRDs defined in a configuration file (optional)
212+
213+
Run the CLI tool:
214+
215+
```bash
216+
npx @crossplane-js/cli gen-models
217+
```
218+
219+
To generate a `models/` directory containing models that can be imported in
220+
your composition functions.
221+
222+
To generate models for extra CRDs, create a `config.yaml` in the base
223+
directory, next to the `functions/` directory with a `extraCrds` field
224+
containing an array of extra CRDs urls to use in model generation, e.g.:
225+
226+
```yaml
227+
extraCrds:
228+
- https://github.com/fluxcd/source-controller/releases/download/v1.7.0/source-controller.crds.yaml
229+
```
230+
231+
203232
## Testing
204233
205234
The project includes end-to-end tests that use a Kind cluster to verify functionality:

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"@crossplane-js/kubernetes-models-crd-generate": "5.0.2-sha.6721c6987b6a.r5.a1",
1313
"@crossplane-js/libs": "workspace:^",
14+
"@kubernetes-models/read-input": "^3.1.1",
1415
"commander": "^13.1.0",
1516
"esbuild": "^0.25.4",
1617
"fs-extra": "^11.3.0",

packages/cli/src/commands/gen-models/index.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import path from "path"
44
import { fileURLToPath } from "url"
55

66
import { createLogger } from "@crossplane-js/libs"
7-
import { Command } from "commander"
7+
import { readInput } from "@kubernetes-models/read-input"
8+
import { type Command } from "commander"
89
import fs from "fs-extra"
910
import YAML from "yaml"
1011

@@ -19,6 +20,20 @@ const __dirname = path.dirname(__filename)
1920
// packages/cli/src/commands/gen-models -> packages/cli
2021
const cliRoot = path.resolve(__dirname, "../../..")
2122

23+
/**
24+
* Fetch extra CRDs
25+
* @param urls Array of CRD urls
26+
* @returns Promise<string[]> Array of CRD paths
27+
*/
28+
async function fetchExtraCRDs(paths: string[]): Promise<string[]> {
29+
const extraDocuments: string[] = []
30+
for (const path of paths) {
31+
moduleLogger.info("Reading extra CRD: %s", path)
32+
extraDocuments.push(await readInput(path))
33+
}
34+
return extraDocuments
35+
}
36+
2237
/**
2338
* Find all XRD files in the functions directory
2439
* @returns Promise<string[]> Array of XRD file paths
@@ -72,7 +87,10 @@ async function runCrdGenerate(crdYaml: string, outputPath: string): Promise<void
7287
"--output",
7388
outputAbs,
7489
]
75-
const child = spawn("yarn", args, { cwd: cliRoot, stdio: ["ignore", "inherit", "inherit"] })
90+
const child = spawn("yarn", args, {
91+
cwd: cliRoot,
92+
stdio: ["ignore", "inherit", "inherit"],
93+
})
7694
child.on("error", err => reject(err))
7795
child.on("exit", code => {
7896
if (code === 0) resolve()
@@ -94,6 +112,8 @@ async function genModelsAction(): Promise<void> {
94112
try {
95113
moduleLogger.info("Starting model generation...")
96114

115+
const documents: string[] = []
116+
97117
// Find all XRD files
98118
const xrdFiles = await findXRDFiles()
99119
moduleLogger.info(`Found ${xrdFiles.length} XRD file(s): ${xrdFiles.join(", ")}`)
@@ -119,9 +139,7 @@ async function genModelsAction(): Promise<void> {
119139
// Convert XRD to CRD
120140
const crd = convertXRDtoCRD(xrd)
121141
const crdYaml = YAML.stringify(crd)
122-
123-
// Generate models using crd-generate (via child process)
124-
await runCrdGenerate(crdYaml, modelsDir)
142+
documents.push(crdYaml)
125143

126144
moduleLogger.info(`✓ Generated models for ${xrdPath}`)
127145
} catch (error) {
@@ -130,6 +148,27 @@ async function genModelsAction(): Promise<void> {
130148
}
131149
}
132150

151+
// Get extra CRD urls from config file
152+
if (await fs.exists("config.yaml")) {
153+
moduleLogger.info("Config file exists, searching extra CRDs...")
154+
const configFile = await fs.readFile("config.yaml", "utf8")
155+
const config = YAML.parse(configFile)
156+
if (!config.extraCrds) {
157+
moduleLogger.info("Config file has no extra CRDs")
158+
} else if (!Array.isArray(config.extraCrds)) {
159+
moduleLogger.warn("Config file extraCrds field is not an array!")
160+
} else {
161+
const crds = await fetchExtraCRDs(config.extraCrds)
162+
for (const crd of crds) {
163+
documents.push(crd)
164+
}
165+
}
166+
}
167+
168+
// Generate models using crd-generate (via child process)
169+
const allCrds = documents.join("\n---\n")
170+
await runCrdGenerate(allCrds, modelsDir)
171+
133172
moduleLogger.info(`✓ Model generation completed. Models saved to '${modelsDir}/' directory.`)
134173
} catch (error) {
135174
moduleLogger.error(`Error generating models: ${error}`)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
extraCrds:
2+
- https://github.com/fluxcd/source-controller/releases/download/v1.7.0/source-controller.crds.yaml

tests/fixtures/domain-sdk/functions/simpleconfigmaps/composition.fn.ts renamed to tests/fixtures/domain-sdk/functions/simpleconfigmaps.test.crossplane.io/composition.fn.ts

File renamed without changes.

tests/fixtures/domain-sdk/functions/simpleconfigmaps/xrd.yaml renamed to tests/fixtures/domain-sdk/functions/simpleconfigmaps.test.crossplane.io/xrd.yaml

File renamed without changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
logger,
3+
// FieldRef,
4+
v1,
5+
} from '@crossplane-js/sdk'
6+
import type {
7+
CrossplaneDesiredResources,
8+
// CrossplaneObservedResources
9+
} from '@crossplane-js/sdk'
10+
11+
import type { SimpleConfigMap } from '../../models/test.crossplane.io/v1beta1'
12+
13+
export default function (
14+
composite: SimpleConfigMap
15+
// resources: CrossplaneObservedResources
16+
): CrossplaneDesiredResources {
17+
logger.info('SimpleConfigMap composition function started')
18+
logger.info(`composite:${JSON.stringify(composite)}`)
19+
20+
const namespace = composite.getNamespace()
21+
const isReady = composite.isReady()
22+
23+
logger.info(`Namespace: ${namespace}, Ready: ${isReady}`)
24+
logger.info(composite)
25+
26+
// Transform data to uppercase for testing (same as original test logic)
27+
const transformedData: Record<string, string> = {}
28+
Object.entries(composite.spec.data).forEach(([key, value]) => {
29+
transformedData[key.toUpperCase()] = value.toUpperCase()
30+
})
31+
32+
const testConfigMap = new v1.ConfigMap({
33+
metadata: {
34+
name: 'generated-configmap',
35+
namespace: namespace || 'test-xfuncjs',
36+
},
37+
data: transformedData,
38+
})
39+
40+
const desired: CrossplaneDesiredResources = {
41+
resources: {
42+
configmap: {
43+
resource: testConfigMap,
44+
ready: true,
45+
},
46+
},
47+
}
48+
49+
logger.info('SimpleConfigMap composition function completed')
50+
logger.debug({ desired }, 'Generated output')
51+
52+
return desired
53+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
apiVersion: apiextensions.crossplane.io/v2
2+
kind: CompositeResourceDefinition
3+
metadata:
4+
name: uppercaseconfigmaps.test.crossplane.io
5+
spec:
6+
scope: Namespaced
7+
defaultCompositeDeletePolicy: Foreground
8+
group: test2.crossplane.io
9+
names:
10+
categories:
11+
- crossplane
12+
kind: UppercaseConfigMap
13+
plural: uppercaseconfigmaps
14+
singular: uppsercaseconfigmap
15+
versions:
16+
- name: v1beta1
17+
served: true
18+
referenceable: true
19+
schema:
20+
openAPIV3Schema:
21+
type: object
22+
properties:
23+
spec:
24+
type: object
25+
properties:
26+
dataToUppercase:
27+
type: object
28+
description: Key-value data to be uppercased and stored in ConfigMap
29+
additionalProperties:
30+
type: string
31+
required:
32+
- data

tests/test-xfuncjs.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ yarn --cwd tests/fixtures/domain-sdk gen-manifests
5353

5454
# Apply CRDs and Compositions
5555
echo "Applying XRD and Compositions..."
56-
kubectl apply -f tests/fixtures/domain-sdk/manifests/simpleconfigmaps
56+
kubectl apply --server-side=true -f tests/fixtures/domain-sdk/manifests/simpleconfigmaps.test.crossplane.io
5757

5858
# Wait for XRD to be established
5959
echo "Waiting for XRD to be established..."

yarn.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ __metadata:
269269
dependencies:
270270
"@crossplane-js/kubernetes-models-crd-generate": "npm:5.0.2-sha.6721c6987b6a.r5.a1"
271271
"@crossplane-js/libs": "workspace:^"
272+
"@kubernetes-models/read-input": "npm:^3.1.1"
272273
"@types/lodash": "npm:^4"
273274
"@types/uuid": "npm:^10.0.0"
274275
commander: "npm:^13.1.0"

0 commit comments

Comments
 (0)