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
30 changes: 30 additions & 0 deletions .github/renovate.json5
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
schedule: [],
},
customManagers: [
{
customType: 'regex',
description: 'Bump the container images the schema generators run in',
// Scoped to the generator package on purpose. The function builders in
// internal/project/functions also declare image constants, but those are
// deliberately floating bases for user function images (for example
// distroless nodejs24-debian13, whose tag is not a version), so bumping
// them automatically would be wrong.
managerFilePatterns: [
'/^internal/schemas/generator/.*\\.go$/',
],
matchStrings: [
'Image\\s*=\\s*"(?<depName>[^":]+):(?<currentValue>[^"]+)"',
],
datasourceTemplate: 'docker',
},
{
customType: 'regex',
description: 'Bump the Renovate version used by the config validator and the bot',
Expand Down Expand Up @@ -148,6 +164,20 @@
],
enabled: false,
},
{
// The TypeScript schema generator installs this tree with npm ci inside a
// container, so package.json and package-lock.json have to move together
// or the install fails. Grouping keeps them in one reviewable PR, and
// each bump changes generated model output, so these are worth reading.
description: 'Group updates to the pinned TypeScript schema generator toolchain',
matchManagers: [
'npm',
],
matchFileNames: [
'internal/schemas/generator/typescript-toolchain/package.json',
],
groupName: 'typescript schema generator toolchain',
},
{
description: 'Group all go version updates',
matchDatasources: [
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
74 changes: 68 additions & 6 deletions cmd/crossplane/function/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,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 @@ -59,6 +65,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 @@ -70,7 +78,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 @@ -138,7 +146,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 @@ -176,10 +184,11 @@ func (c *generateCmd) Run(sp terminal.SpinnerPrinter, cfg *config.Config) 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 @@ -412,6 +421,59 @@ func (c *generateCmd) generateGoTemplatingFiles(fs afero.Fs) error {
return renderTemplates(fs, tmpls, tmplData)
}

type typescriptTemplateData struct {
Name string
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{
Name: c.Name,
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
6 changes: 4 additions & 2 deletions cmd/crossplane/function/help/generate.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
The `function generate` command creates an embedded function in the specified
language under the project's `functions/` directory. It optionally idempotently
adds the new function to the end of a Composition's pipeline when given a
Composition path.
adds the new function to the start of a Composition's pipeline when given a
Composition path, so that it runs before steps such as `function-auto-ready`,
which observe the resources it composes.

## Supported languages

Expand All @@ -11,6 +12,7 @@ The following are valid arguments to the `--language` / `-l` flag:
- `go`
- `kcl`
- `python`
- `typescript`

## Examples

Expand Down
11 changes: 10 additions & 1 deletion cmd/crossplane/function/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,16 @@ func addStepToComposition(fs afero.Fs, path, stepName, functionRef string) error

func addCompositionStep(comp *apiextv1.Composition, stepName, functionRef string) error {
for _, step := range comp.Spec.Pipeline {
if step.Step == stepName && step.FunctionRef.Name == functionRef {
if step.Step != stepName {
continue
}
if step.FunctionRef.Name == functionRef {
return nil // already exists
}
// Step names must be unique within a pipeline, so we can't add this
// one alongside the existing step. Rewriting the existing step to
// point somewhere else isn't ours to decide either.
return errors.Errorf("composition already has a step named %q referencing function %q; rename the function or edit the pipeline by hand", stepName, step.FunctionRef.Name)
}

step := apiextv1.PipelineStep{
Expand All @@ -59,6 +66,8 @@ func addCompositionStep(comp *apiextv1.Composition, stepName, functionRef string
},
}

// Prepend, so the generated function runs before steps that observe what
// it composes, such as function-auto-ready.
comp.Spec.Pipeline = append([]apiextv1.PipelineStep{step}, comp.Spec.Pipeline...)
return nil
}
Expand Down
9 changes: 9 additions & 0 deletions cmd/crossplane/function/templates/typescript/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# The generated crossplane-models package is a file: dependency. By default npm
# symlinks such dependencies, and Node resolves the symlink to its real path —
# which lives outside this function's node_modules, so the models package
# cannot find its own dependencies and importing it fails at runtime with
# "Cannot find package '@kubernetes-models/base'".
#
# install-links copies file: dependencies into node_modules instead, which also
# matches how the function is laid out inside its runtime image.
install-links=true
Loading
Loading