Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ You can use type-safe Kubernetes models from the `kubernetes-models` package:
```typescript
import { Deployment } from "kubernetes-models/apps/v1";
import { Pod } from "kubernetes-models/v1";
import { fromModel } from "@crossplane-org/function-sdk-typescript";

// Create a type-safe Pod
const pod = new Pod({
Expand All @@ -200,7 +201,10 @@ const pod = new Pod({
// Validate the pod
pod.validate();

// Convert to Resource
// Convert to Resource using the simplified helper
dcds["my-pod"] = fromModel(pod);

// Alternative: Using the lower-level API
dcds["my-pod"] = Resource.fromJSON({
resource: pod.toJSON()
});
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export {
asObject,
asStruct,
fromObject,
fromModel,
toObject,
newDesiredComposed,
mustStructObject,
Expand Down
130 changes: 130 additions & 0 deletions src/resource/resource.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { describe, it, expect } from 'vitest';
import { fromModel, fromObject, toObject } from './resource.js';
import { Resource, Ready } from '../proto/run_function.js';

describe('fromModel', () => {
it('should convert a kubernetes-models-like object with toJSON() method', () => {
// Simulate a kubernetes-models object
const mockModel = {
metadata: {
name: 'my-pod',
namespace: 'default',
},
spec: {
containers: [{ name: 'app', image: 'nginx:latest' }],
},
toJSON() {
return {
apiVersion: 'v1',
kind: 'Pod',
metadata: this.metadata,
spec: this.spec,
};
},
};

const resource = fromModel(mockModel);

expect(resource.resource).toEqual({
apiVersion: 'v1',
kind: 'Pod',
metadata: { name: 'my-pod', namespace: 'default' },
spec: { containers: [{ name: 'app', image: 'nginx:latest' }] },
});
expect(resource.ready).toBe(Ready.READY_UNSPECIFIED);
expect(resource.connectionDetails).toEqual({});
});

it('should accept connection details and ready status', () => {
const mockModel = {
metadata: {
name: 'my-secret',
},
toJSON() {
return {
apiVersion: 'v1',
kind: 'Secret',
metadata: this.metadata,
};
},
};

const connectionDetails = { password: Buffer.from('secret') };
const ready = Ready.READY_TRUE;

const resource = fromModel(mockModel, connectionDetails, ready);

expect(resource.resource).toEqual({
apiVersion: 'v1',
kind: 'Secret',
metadata: { name: 'my-secret' },
});
expect(resource.ready).toBe(Ready.READY_TRUE);
expect(resource.connectionDetails).toEqual(connectionDetails);
});
});

describe('fromObject', () => {
it('should create a Resource from a plain object', () => {
const obj = {
apiVersion: 'v1',
kind: 'Service',
metadata: { name: 'my-service' },
};

const resource = fromObject(obj);

expect(resource.resource).toEqual(obj);
expect(resource.ready).toBe(Ready.READY_UNSPECIFIED);
expect(resource.connectionDetails).toEqual({});
});

it('should accept connection details and ready status', () => {
const obj = {
apiVersion: 'v1',
kind: 'ConfigMap',
metadata: { name: 'my-config' },
data: { key: 'value' },
};

const connectionDetails = { apiKey: Buffer.from('secret') };
const ready = Ready.READY_TRUE;

const resource = fromObject(obj, connectionDetails, ready);

expect(resource.resource).toEqual(obj);
expect(resource.ready).toBe(Ready.READY_TRUE);
expect(resource.connectionDetails).toEqual(connectionDetails);
});
});

describe('toObject', () => {
it('should extract the resource object from a Resource', () => {
const obj = {
apiVersion: 'v1',
kind: 'Deployment',
metadata: { name: 'my-deployment' },
};

const resource = Resource.fromJSON({
resource: obj,
connectionDetails: {},
ready: Ready.READY_TRUE,
});

const extracted = toObject(resource);

expect(extracted).toEqual(obj);
});

it('should return undefined if resource is not present', () => {
const resource = Resource.fromJSON({
connectionDetails: {},
ready: Ready.READY_TRUE,
});

const extracted = toObject(resource);

expect(extracted).toBeUndefined();
});
});
65 changes: 57 additions & 8 deletions src/resource/resource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Resource utilities for working with Kubernetes resources and protobuf conversion
import { Resource, Ready } from '../proto/run_function.js';
import { Ready, Resource } from '../proto/run_function.js';

// Type aliases for better readability
export type ConnectionDetails = { [key: string]: Buffer };
Expand Down Expand Up @@ -51,7 +51,9 @@ export function newDesiredComposed(): DesiredComposed {
* @param struct - The protobuf Struct to convert
* @returns A plain JavaScript object representing the Kubernetes resource
*/
export function asObject(struct: Record<string, unknown> | undefined): Record<string, unknown> {
export function asObject(
struct: Record<string, unknown> | undefined,
): Record<string, unknown> {
if (!struct) {
return {};
}
Expand All @@ -73,7 +75,9 @@ export function asObject(struct: Record<string, unknown> | undefined): Record<st
* @param obj - The plain JavaScript object to convert
* @returns A protobuf Struct representation
*/
export function asStruct(obj: Record<string, unknown>): Record<string, unknown> {
export function asStruct(
obj: Record<string, unknown>,
): Record<string, unknown> {
// In our TypeScript implementation, this is essentially a pass-through
// The actual conversion happens in the protobuf serialization layer
return obj;
Expand All @@ -92,12 +96,16 @@ export function asStruct(obj: Record<string, unknown>): Record<string, unknown>
* @returns A Struct representation
* @throws Error if conversion fails
*/
export function mustStructObject(obj: Record<string, unknown>): Record<string, unknown> {
export function mustStructObject(
obj: Record<string, unknown>,
): Record<string, unknown> {
try {
return asStruct(obj);
} catch (error) {
throw new Error(
`Failed to convert object to struct: ${error instanceof Error ? error.message : String(error)}`
`Failed to convert object to struct: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
Expand All @@ -121,7 +129,9 @@ export function mustStructJSON(json: string): Record<string, unknown> {
return asStruct(obj);
} catch (error) {
throw new Error(
`Failed to parse JSON to struct: ${error instanceof Error ? error.message : String(error)}`
`Failed to parse JSON to struct: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
Expand All @@ -138,7 +148,7 @@ export function mustStructJSON(json: string): Record<string, unknown> {
export function fromObject(
obj: Record<string, unknown>,
connectionDetails?: ConnectionDetails,
ready?: Ready
ready?: Ready,
): Resource {
return Resource.fromJSON({
resource: obj,
Expand All @@ -154,6 +164,45 @@ export function fromObject(
* @param resource - The Resource to extract from
* @returns The plain JavaScript object, or undefined if not present
*/
export function toObject(resource: Resource): Record<string, unknown> | undefined {
export function toObject(
resource: Resource,
): Record<string, unknown> | undefined {
return resource.resource;
}

/**
* Create a Resource from a kubernetes-models object
* This is a convenience function for objects with a toJSON() method
* (like kubernetes-models objects)
*
* For plain JavaScript objects, use fromObject() instead.
*
* @param obj - A kubernetes-models object with a toJSON() method
* @param connectionDetails - Optional connection details
* @param ready - Optional ready status
* @returns A Resource
*
* @example
* ```typescript
* import { Pod } from "kubernetes-models/v1";
*
* const pod = new Pod({
* metadata: { name: "my-pod" },
* spec: { containers: [{ name: "app", image: "nginx" }] }
* });
*
* // Automatically calls toJSON() on the model object
* const resource = fromModel(pod);
* ```
*/
export function fromModel<T extends Record<string, unknown>>(
obj: { toJSON: () => T },
connectionDetails?: ConnectionDetails,
ready?: Ready,
): Resource {
return fromObject(
obj.toJSON() as Record<string, unknown>,
connectionDetails,
ready,
);
}