Skip to content
Draft
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
15 changes: 15 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@
// We only want renovate to rebase PRs when they have conflicts, default
// "auto" mode is not required.
rebaseWhen: 'conflicted',
// Custom managers for non-standard file formats
customManagers: [
{
// Manage npm dependencies in TypeScript function template
customType: 'regex',
fileMatch: [
'cmd/crossplane/function/templates/typescript/package\\.json\\.tmpl$',
],
matchStrings: [
'"(?<depName>@?[^"]+)":\\s*"\\^(?<currentValue>[^"]+)"',
],
datasourceTemplate: 'npm',
versioningTemplate: 'npm',
},
],
// The maximum number of PRs to be created in parallel
prConcurrentLimit: 5,
// The branches renovate should target
Expand Down
14 changes: 8 additions & 6 deletions apis/dev/v1alpha1/project_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ const (
// ProjectSchemas.Languages. Each corresponds to a schema generator in
// internal/schemas/generator.
const (
SchemaLanguageGo = "go"
SchemaLanguageJSON = "json"
SchemaLanguageKCL = "kcl"
SchemaLanguagePython = "python"
SchemaLanguageGo = "go"
SchemaLanguageJSON = "json"
SchemaLanguageKCL = "kcl"
SchemaLanguagePython = "python"
SchemaLanguageTypescript = "typescript"
)

// SupportedSchemaLanguages returns the set of language identifiers accepted
Expand All @@ -63,6 +64,7 @@ func SupportedSchemaLanguages() []string {
SchemaLanguageJSON,
SchemaLanguageKCL,
SchemaLanguagePython,
SchemaLanguageTypescript,
}
}

Expand Down Expand Up @@ -133,8 +135,8 @@ type ProjectPackageMetadata struct {
// produced both for the project's own XRDs and for its declared dependencies.
type ProjectSchemas struct {
// Languages restricts schema generation to the listed languages.
// Supported values are "go", "json", "kcl", and "python". If not
// specified, schemas are generated for all supported languages.
// If not specified, schemas are generated for all supported languages.
// +kubebuilder:validation:items:Enum=go;json;kcl;python;typescript
Languages []string `json:"languages,omitempty"`
}

Expand Down
14 changes: 9 additions & 5 deletions cmd/crossplane/common/resource/xrm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@ import (
"github.com/crossplane/cli/v2/cmd/crossplane/common/resource"
)

// defaultConcurrency is the concurrency using which the resource tree if loaded when not explicitly specified.
const defaultConcurrency = 5
const (
// defaultConcurrency is the concurrency using which the resource tree if loaded when not explicitly specified.
defaultConcurrency = 5
// kindSecret is the Kubernetes Secret kind.
kindSecret = "Secret"
)

// Client to get a Resource with all its children.
type Client struct {
Expand Down Expand Up @@ -105,7 +109,7 @@ func getResourceChildrenRefs(r *resource.Resource, getConnectionSecrets bool) []
obj := r.Unstructured

switch obj.GroupVersionKind().GroupKind() {
case schema.GroupKind{Group: "", Kind: "Secret"},
case schema.GroupKind{Group: "", Kind: kindSecret},
v1alpha1.UsageGroupVersionKind.GroupKind(),
v1beta1.EnvironmentConfigGroupVersionKind.GroupKind():
// nothing to do here, it's a resource we know not to have any reference
Expand All @@ -131,7 +135,7 @@ func getResourceChildrenRefs(r *resource.Resource, getConnectionSecrets bool) []
if cmSecretRef := cm.GetWriteConnectionSecretToReference(); cmSecretRef != nil {
ref := v1.ObjectReference{
APIVersion: "v1",
Kind: "Secret",
Kind: kindSecret,
Name: cmSecretRef.Name,
Namespace: cm.GetNamespace(),
}
Expand Down Expand Up @@ -159,7 +163,7 @@ func getResourceChildrenRefs(r *resource.Resource, getConnectionSecrets bool) []
if xrSecretRef := xr.GetWriteConnectionSecretToReference(); xrSecretRef != nil {
ref := v1.ObjectReference{
APIVersion: "v1",
Kind: "Secret",
Kind: kindSecret,
Name: xrSecretRef.Name,
Namespace: xrSecretRef.Namespace,
}
Expand Down
72 changes: 66 additions & 6 deletions cmd/crossplane/function/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ import (
"github.com/crossplane/cli/v2/internal/terminal"
)

// Function language constants.
const (
langGoTemplating = "go-templating"
langPython = "python"
)

//go:embed help/generate.md
var generateHelp string

Expand All @@ -58,6 +64,8 @@ var (
pythonTemplates embed.FS
//go:embed templates/go-templating/*
goTemplatingTemplates embed.FS
//go:embed all:templates/typescript
typescriptTemplates embed.FS

// The go template contains a go.mod, so we can't embed it as an
// embed.FS. Instead we have to embed it as a tar archive and extract it
Expand All @@ -69,7 +77,7 @@ var (
type generateCmd struct {
Name string `arg:"" help:"Name of the function to generate. Must be a valid DNS-1035 label."`
PipelinePath string `arg:"" help:"Path to a Composition YAML file to add a pipeline step to." optional:""`
Language string `default:"go-templating" enum:"go,go-templating,kcl,python" help:"Language to use for the function." short:"l"`
Language string `default:"go-templating" enum:"go,go-templating,kcl,python,typescript" help:"Language to use for the function." short:"l"`
ProjectFile string `default:"crossplane-project.yaml" help:"Path to project definition file." short:"f"`

projFS afero.Fs
Expand Down Expand Up @@ -137,7 +145,7 @@ func validateLanguageAgainstSchemas(functionLang string, schemaLangs []string) e
// the given function language consumes. Most function languages map to a
// like-named schema language; go-templating consumes the JSON schema.
func functionSchemaLanguage(functionLang string) string {
if functionLang == "go-templating" {
if functionLang == langGoTemplating {
return v1alpha1.SchemaLanguageJSON
}
return functionLang
Expand Down Expand Up @@ -169,10 +177,11 @@ func (c *generateCmd) Run(sp terminal.SpinnerPrinter) error {

type generatorFunc func(afero.Fs) error
generators := map[string]generatorFunc{
"go": c.generateGoFiles,
"go-templating": c.generateGoTemplatingFiles,
"kcl": c.generateKCLFiles,
"python": c.generatePythonFiles,
"go": c.generateGoFiles,
langGoTemplating: c.generateGoTemplatingFiles,
"kcl": c.generateKCLFiles,
langPython: c.generatePythonFiles,
"typescript": c.generateTypescriptFiles,
}

generator, ok := generators[c.Language]
Expand Down Expand Up @@ -405,6 +414,57 @@ func (c *generateCmd) generateGoTemplatingFiles(fs afero.Fs) error {
return renderTemplates(fs, tmpls, tmplData)
}

type typescriptTemplateData struct {
HasSchemas bool
SchemasPath string
}

func (c *generateCmd) generateTypescriptFiles(targetFS afero.Fs) error {
hasSchemas, err := afero.DirExists(c.schemasFS, "typescript")
if err != nil {
return errors.Wrap(err, "cannot inspect typescript schemas directory")
}
if hasSchemas {
entries, err := afero.ReadDir(c.schemasFS, "typescript")
if err != nil {
return errors.Wrap(err, "cannot read typescript schemas directory")
}
hasSchemas = len(entries) > 0
}

// Compute the relative path from the function dir to schemas/typescript/.
fnDir := filepath.Join("/", c.proj.Spec.Paths.Functions, c.Name)
relRoot, err := filepath.Rel(fnDir, "/")
if err != nil {
return errors.Wrap(err, "cannot determine path to schemas directory")
}
schemasPath := filepath.ToSlash(filepath.Join(relRoot, c.proj.Spec.Paths.Schemas, "typescript"))

data := typescriptTemplateData{
HasSchemas: hasSchemas,
SchemasPath: schemasPath,
}

// Parse top-level templates
tmpls, err := template.ParseFS(typescriptTemplates, "templates/typescript/*.*")
if err != nil {
return errors.Wrap(err, "cannot parse top-level TypeScript templates")
}
if err := renderTemplates(targetFS, tmpls, data); err != nil {
return err
}

// Create src directory and parse src templates
if err := targetFS.Mkdir("src", 0o755); err != nil {
return errors.Wrap(err, "cannot create src directory")
}
tmpls, err = template.ParseFS(typescriptTemplates, "templates/typescript/src/*.*")
if err != nil {
return errors.Wrap(err, "cannot parse TypeScript source templates")
}
return renderTemplates(afero.NewBasePathFs(targetFS, "src"), tmpls, data)
}

func renderTemplates(targetFS afero.Fs, tmpls *template.Template, data any) error {
for _, tmpl := range tmpls.Templates() {
fname := tmpl.Name()
Expand Down
1 change: 1 addition & 0 deletions cmd/crossplane/function/help/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The following are valid arguments to the `--language` / `-l` flag:
- `go`
- `kcl`
- `python`
- `typescript`

## Examples

Expand Down
36 changes: 36 additions & 0 deletions cmd/crossplane/function/templates/typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Crossplane Composition Function

This is a [Crossplane](https://crossplane.io) composition function written in TypeScript.

## Development

Install dependencies:

```shell
npm install
```

Build the function:

```shell
npm run build
```

Run locally (for testing):

```shell
npm run local
```

## Testing

Test your function using `crossplane composition render`:

```shell
crossplane composition render xr.yaml composition.yaml functions.yaml
```

## Learn More

- [Composition Functions documentation](https://docs.crossplane.io/latest/concepts/composition-functions/)
- [TypeScript Function SDK](https://github.com/crossplane/function-sdk-typescript)
26 changes: 26 additions & 0 deletions cmd/crossplane/function/templates/typescript/package.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "function",
"version": "0.1.0",
"description": "A Crossplane composition function.",
"license": "Apache-2.0",
"type": "module",
"main": "dist/main.js",
"scripts": {
"build": "tsgo",
"local": "node dist/main.js --insecure --debug"
},
"dependencies": {
"@crossplane-org/function-sdk-typescript": "^0.5.0",
"@types/node": "^26.0.0",
"commander": "^15.0.0",
{{- if .HasSchemas }}
"crossplane-models": "file:{{ .SchemasPath }}",
{{- end }}
"kubernetes-models": "^4.5.1",
"pino": "^10.3.0"
},
"devDependencies": {
"@typescript/native-preview": "^7.0.0-dev.20260627.1",
"typescript": "^6.0.0"
}
}
39 changes: 39 additions & 0 deletions cmd/crossplane/function/templates/typescript/src/function.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
type RunFunctionRequest,
type RunFunctionResponse,
type FunctionHandler,
type Logger,
to,
normal,
getObservedCompositeResource,
getDesiredComposedResources,
setDesiredComposedResources,
} from '@crossplane-org/function-sdk-typescript';

/**
* Function is a Crossplane composition function.
*/
export class Function implements FunctionHandler {
async RunFunction(req: RunFunctionRequest, logger?: Logger): Promise<RunFunctionResponse> {
let rsp = to(req);

// Get the observed composite resource (XR).
const observedComposite = getObservedCompositeResource(req);
logger?.debug({ observedComposite }, 'Observed composite resource');

// Get the desired composed resources from previous functions in the pipeline.
const desiredComposed = getDesiredComposedResources(req);
logger?.debug({ desiredComposed }, 'Desired composed resources');

// TODO: Add your function logic here.
// Use desiredComposed to add, modify, or remove composed resources.
// Example:
// desiredComposed['my-resource'] = { resource: { ... } };

// Update the response with the desired composed resources.
rsp = setDesiredComposedResources(rsp, desiredComposed);

normal(rsp, 'Function completed successfully');
return rsp;
}
}
Loading
Loading