Skip to content
Open
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
41 changes: 39 additions & 2 deletions cmd/crossplane/dependency/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package dependency

import (
"context"
"fmt"
"path/filepath"
"slices"
"strings"

"github.com/google/go-containerregistry/pkg/name"
Expand Down Expand Up @@ -108,9 +110,44 @@ func (c *addCmd) Run(logger logging.Logger, sp terminal.SpinnerPrinter, cfg *con

desc := dependency.GetSourceDescription(dep)
logger.Debug("Adding dependency", "dependency", desc)
return sp.WrapWithSuccessSpinner("Adding "+desc, func() error {
if err := sp.WrapWithSuccessSpinner("Adding "+desc, func() error {
return m.AddDependency(ctx, &dep)
})
}); err != nil {
return err
}

if note := schemaLanguageNote(dep, proj.Spec.Schemas.GetLanguages()); note != "" {
fmt.Println(note) //nolint:forbidigo // CLI output.
}

return nil
}

// schemaLanguageNote returns a note for a dependency that will not produce
// models in one of the languages the project asked for, or "" when there is
// nothing to say.
//
// A Kubernetes API dependency is described by an OpenAPI spec rather than by
// CRDs, and the TypeScript generator reads CRDs. It produces nothing for such a
// source and says nothing, so a user who has seen Python and Go generate
// bindings for the Kubernetes API reasonably expects the same and finds out
// otherwise when an import fails. This is the moment that expectation forms,
// which is why the note lives here rather than at build time.
//
// Nothing is wrong: TypeScript functions get typed built-ins from the
// kubernetes-models package, which the function scaffold already depends on, so
// generating them would duplicate it.
func schemaLanguageNote(dep v1alpha1.Dependency, langs []string) string {
if dep.Type != v1alpha1.DependencyTypeK8s {
return ""
}
if !slices.Contains(langs, v1alpha1.SchemaLanguageTypescript) {
return ""
}

return "Note: TypeScript models are not generated from Kubernetes API dependencies. " +
"Import built-in types from the kubernetes-models package instead, for example " +
"`import { Deployment } from 'kubernetes-models/apps/v1'`."
}

func (c *addCmd) buildDependency() (v1alpha1.Dependency, error) {
Expand Down
92 changes: 81 additions & 11 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:"${project_file}" help:"Path to project definition file." short:"f"`

projFS afero.Fs
Expand Down Expand Up @@ -120,14 +128,22 @@ func (c *generateCmd) AfterApply() error {
// validateLanguageAgainstSchemas refuses to generate a function in a language
// whose schemas the project doesn't generate. Such a function would have no
// models to import, which is surprising, so we fail up front rather than
// scaffolding a function that can't compile. An empty schemaLangs means the
// project generates all languages (matching generator.Filter), so any function
// language is fine.
// scaffolding a function that can't compile.
func validateLanguageAgainstSchemas(functionLang string, schemaLangs []string) error {
required := functionSchemaLanguage(functionLang)

// An unset list is not permission for anything: it selects a default set,
// which does not include every supported language. Validating against the
// defaults is what stops `function generate --language typescript` on a
// freshly initialised project from scaffolding a function whose models are
// never generated.
if len(schemaLangs) == 0 {
return nil
if slices.Contains(generator.DefaultLanguages(), required) {
return nil
}
return errors.Errorf("cannot generate a %q function: this project does not set spec.schemas.languages, so it generates %v schemas and not %q; add %q to spec.schemas.languages", functionLang, generator.DefaultLanguages(), required, required)
}
required := functionSchemaLanguage(functionLang)

if !slices.Contains(schemaLangs, required) {
return errors.Errorf("cannot generate a %q function: the project only generates %v schemas; add %q to spec.schemas.languages or choose a different language", functionLang, schemaLangs, required)
}
Expand All @@ -138,7 +154,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 +192,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 @@ -420,6 +437,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/*.*")
Comment thread
stevendborrelli marked this conversation as resolved.
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
Loading
Loading