From 72f1d84ac92cc390f2a9696bd0cbcf3b81b5380d Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:13:50 +0200 Subject: [PATCH 1/5] add pkl native composition creation add code generation for some pkl/crossplane.contrib files. Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- Makefile | 26 +- hack/pklcrd/PklProject | 55 ++ hack/pklcrd/README.md | 6 + hack/pklcrd/crd2module-composition-fix.pkl | 15 + hack/pklcrd/crd2module.pkl | 6 + hack/pklcrd/generate.pkl | 153 +++++ hack/pklcrd/internal/ModuleGenerator.pkl | 392 +++++++++++ input/v1beta1/input.go | 2 +- ...ls.yaml => pkl.fn.crossplane.io_pkls.yaml} | 7 +- .../PklProject.deps.json | 71 -- pkl/crossplane.contrib.example/inline.pkl | 40 ++ .../PklProject.deps.json | 59 -- pkl/crossplane.contrib/Composition.pkl | 636 ++++++++++++++++++ .../CompositionResponse.pkl | 5 + pkl/crossplane.contrib/Pkl.pkl | 44 ++ pkl/crossplane.contrib/PklProject.deps.json | 12 - 16 files changed, 1371 insertions(+), 158 deletions(-) create mode 100644 hack/pklcrd/PklProject create mode 100644 hack/pklcrd/README.md create mode 100644 hack/pklcrd/crd2module-composition-fix.pkl create mode 100644 hack/pklcrd/crd2module.pkl create mode 100644 hack/pklcrd/generate.pkl create mode 100644 hack/pklcrd/internal/ModuleGenerator.pkl rename package/input/{template.fn.crossplane.io_pkls.yaml => pkl.fn.crossplane.io_pkls.yaml} (91%) delete mode 100644 pkl/crossplane.contrib.example/PklProject.deps.json create mode 100644 pkl/crossplane.contrib.example/inline.pkl delete mode 100644 pkl/crossplane.contrib.xrd/PklProject.deps.json create mode 100644 pkl/crossplane.contrib/Composition.pkl create mode 100644 pkl/crossplane.contrib/Pkl.pkl delete mode 100644 pkl/crossplane.contrib/PklProject.deps.json diff --git a/Makefile b/Makefile index 8ee9f7f..64cd5d7 100644 --- a/Makefile +++ b/Makefile @@ -7,26 +7,20 @@ TARGET =? $(shell git branch --show-current) LATEST_CORE := $(shell git tag -l "crossplane.contrib@*.*.*" --sort=-v:refname | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') LATEST_EXAMPLE := $(shell git tag -l "crossplane.contrib.example@*.*.*" --sort=-v:refname | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') LATEST_XRD := $(shell git tag -l "crossplane.contrib.xrd@*.*.*" --sort=-v:refname | head -n 1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') +REPO_PARAM := $(if $(REPO),-e REPOSITORY="$(REPO)") +CORE_PARAM := $(if $(LATEST_CORE),-e CROSSPLANE_CONTRIB_VERSION="$(LATEST_CORE)") +EXAMPLE_PARAM := $(if $(LATEST_EXAMPLE),-e CROSSPLANE_CONTRIB_EXAMPLE_VERSION="$(LATEST_EXAMPLE)") +XRD_PARAM := $(if $(LATEST_XRD),-e CROSSPLANE_CONTRIB_XRD_VERSION="$(LATEST_XRD)") # This Resolves the Dependencies and sets the versions of our packages to the Latest ones for the package in Git .PHONY: pkl-resolve pkl-resolve: - pkl project resolve \ - -e REPOSITORY="$(REPO)" \ - -e CROSSPLANE_CONTRIB_VERSION="$(LATEST_CORE)" \ - -e CROSSPLANE_CONTRIB_EXAMPLE_VERSION="$(LATEST_EXAMPLE)" \ - -e CROSSPLANE_CONTRIB_XRD_VERSION="$(LATEST_XRD)" \ - ./pkl/*/ + pkl project resolve $(REPO_PARAM) $(CORE_PARAM) $(EXAMPLE_PARAM) $(XRD_PARAM) ./pkl/*/ .PHONY: pkl-package pkl-package: pkl-resolve $(eval PACKAGE_FILES := $(shell \ - pkl project package \ - -e REPOSITORY="$(REPO)" \ - -e CROSSPLANE_CONTRIB_VERSION="$(LATEST_CORE)" \ - -e CROSSPLANE_CONTRIB_EXAMPLE_VERSION="$(LATEST_EXAMPLE)" \ - -e CROSSPLANE_CONTRIB_XRD_VERSION="$(LATEST_XRD)" \ - ./pkl/*/ )) + pkl project package $(REPO_PARAM) $(CORE_PARAM) $(EXAMPLE_PARAM) $(XRD_PARAM) ./pkl/*/ )) # Ensures the TAG Variable is set. .PHONY: check-tag @@ -53,6 +47,14 @@ pkl-release: check-tag pkl-package --target ${TARGET} \ $(RELEASE_FILES) +PROJECT_DIR := $(dir $(firstword $(MAKEFILE_LIST))) +.PHONY: generate +generate: pkl-resolve + go generate ./... + pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module.pkl + pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module-composition-fix.pkl + + .PHONY: build-image build-image: docker build --build-arg -t runtime . diff --git a/hack/pklcrd/PklProject b/hack/pklcrd/PklProject new file mode 100644 index 0000000..64012a2 --- /dev/null +++ b/hack/pklcrd/PklProject @@ -0,0 +1,55 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// + +/// Templates for configuring [Kubernetes](https://kubernetes.io). +amends "pkl:Project" + +local repo = read?("env:REPOSITORY") ?? "github.com/crossplane-contrib/function-pkl" +local packageVersion: String = "0.0.0" + +package { + name = "pklcrd" + authors { + "Tim Geimer <32556895+Avarei@users.noreply.github.com>" + } + version = packageVersion + baseUri = "package://pkg.pkl-lang.org/\(repo)/\(name)" + packageZipUrl = "https://\(repo)/releases/download/\(name)@\(version)/\(name)@\(version).zip" + sourceCode = "https://\(repo)" + + license = "Apache-2.0" + description = """ + Convert CompositeResourceDefitions to Modules that can be amended + """ +} +dependencies { + ["crossplane.contrib.xrd"] = import("../../pkl/crossplane.contrib.xrd/PklProject") + + ["k8s.contrib.crd"] { + uri = "package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@1.0.7" + } + ["k8s"] { + uri = "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0" + } + + //workaround + ["jsonschema"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema@1.0.4" } + ["jsonschema.contrib"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1.0.9" } + ["deepToTyped"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.0.2" } + ["uri"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1.0.3" } + ["syntax"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1.0.3" } + ["openapiv3"] { uri = "package://pkg.pkl-lang.org/pkl-pantry/org.openapis.v3@2.1.2" } +} diff --git a/hack/pklcrd/README.md b/hack/pklcrd/README.md new file mode 100644 index 0000000..b8a816b --- /dev/null +++ b/hack/pklcrd/README.md @@ -0,0 +1,6 @@ +# Generate the Pkl.pkl and Composition.pkl + +```shell +pkl eval -m ../../pkl/crossplane-contrib crd2module.pkl +pkl eval -m ../../pkl/crossplane-contrib crd2module-composition-fix.pkl +``` diff --git a/hack/pklcrd/crd2module-composition-fix.pkl b/hack/pklcrd/crd2module-composition-fix.pkl new file mode 100644 index 0000000..dd0b14e --- /dev/null +++ b/hack/pklcrd/crd2module-composition-fix.pkl @@ -0,0 +1,15 @@ +amends "generate.pkl" + +import "@k8s/K8sResource.pkl" + +source = "https://raw.githubusercontent.com/crossplane/crossplane/v1.16.0/cluster/crds/apiextensions.crossplane.io_compositions.yaml" + +// These Converters Provide Type Safety on the Object resource. Without them manifest would be of type "Dynamic" +converters { + ["compositions.apiextensions.crossplane.io"] { + [List("spec", "pipeline", "pipeline", "input")] = K8sResource + } +} + +// The Package references to be used within the new Module +k8sImportPath = "@k8s" // package://pkg.pkl-lang.org/pkl-k8s/k8s@# diff --git a/hack/pklcrd/crd2module.pkl b/hack/pklcrd/crd2module.pkl new file mode 100644 index 0000000..e6cfcd5 --- /dev/null +++ b/hack/pklcrd/crd2module.pkl @@ -0,0 +1,6 @@ +amends "@k8s.contrib.crd/generate.pkl" + +source = "package/input/pkl.fn.crossplane.io_pkls.yaml" + +// The Package references to be used within the new Module +k8sImportPath = "@k8s" // package://pkg.pkl-lang.org/pkl-k8s/k8s@# diff --git a/hack/pklcrd/generate.pkl b/hack/pklcrd/generate.pkl new file mode 100644 index 0000000..df3a2df --- /dev/null +++ b/hack/pklcrd/generate.pkl @@ -0,0 +1,153 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +/// Generate Pkl sources from CustomResourceDefinition documents. +/// +/// Limitations: +/// - Cannot generate `not`, `allOf`, or `anyOf` combinators correctly due to limitations in Pkl's model. +/// * Union types exist, but they are logically the same as `oneOf` (only one subschema can match). +/// * Intersection types do not exist (can use this to model `allOf`). +/// - Cannot generate tuple types (this is missing in Pkl). +/// - Properties called `default` cannot be generated (currently a limitation of the json parser). +/// +/// TODO: +/// - Handle usages of `allOf`. We can do this by merging subschemas into a larger schema. +/// - Copy doc comments from a class or typealias to its usage sites if there isn't a doc comment already. +/// - Handle if schema root is not an object type (Example: ansible's schema root has `"type": "array"`). +/// - Handle if schema root should be a mapping (it has `additionalProperties` or `patternProperties` set). +/// +/// Sample CLI usage: +/// +/// ``` +/// pkl eval package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl \ +/// -m . \ +/// -p source="https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" +/// ``` +/// +/// Setting up replacement of Kube native types with types from the k8s standard library can be done with amending: +/// +/// ```pkl +/// amends "package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@#/generate.pkl" +/// +/// import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/ResourceRequirements.pkl" +/// +/// source = "https://raw.githubusercontent.com/monzo/egress-operator/master/config/crd/bases/egress.monzo.com_externalservices.yaml" +/// +/// converters { +/// ["externalservices.egress.monzo.com"] { +/// [List("spec", "resources")] = ResourceRequirements +/// } +/// } +/// ``` +/// +/// To figure out which paths you need to override, try running with `-p logPaths`. +/// +@ModuleInfo { minPklVersion = "0.25.0" } +module k8s.contrib.crd.generate + +import "pkl:yaml" +import "pkl:semver" +import "pkl:platform" +import "@deepToTyped/deepToTyped.pkl" +import "@uri/URI.pkl" + +import "internal/ModuleGenerator.pkl" + +/// The version of the Pkl Kubernetes package to import. +/// +/// This property is not used if [k8sImportPath] is set directly. +k8sVersion: String(semver.isValid(this)) = "1.0.1" + +/// The base path to use for the Kubernetes imports. +/// +/// Examples: +/// ``` +/// // Change the version +/// k8sImportPath = "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.0#" +/// +/// // Use dependency notation, assuming the dependency is called `@k8s`. +/// k8sImportPath = "@k8s" +/// ``` +k8sImportPath: String = "package://pkg.pkl-lang.org/pkl-k8s/k8s@\(k8sVersion)#" + +/// Where to find the CRDs; can be a URI (`https:`, `file:` etc), an absolute file path, or a relative file path +source: String = read("prop:source") + +local sourceUri = + if (source.startsWith(Regex(#"\w+:"#))) source // absolute URI + else if (source.startsWith("/")) "file://\(source)" // absolute file path + else // relative file path + let (pwd = read("env:PWD")) + let (path = + if (platform.current.operatingSystem.name == "Windows") "/\(pwd)/\(source)".replaceAll("\\", "/") + else "\(pwd)/\(source)" + ) + "file://\(URI.encode(path))" + +/// The CRD's source contents, as computed from [source]. +sourceContents: String|Resource = read(URI.encode(sourceUri)) + +/// Whether to log out every path we find in each CRD to aid in setting converters. +/// +/// Default: `false`. +logPaths: Boolean? = read?("prop:logPaths")?.toBoolean() + +local crds: Listing = + let (parser = new yaml.Parser { useMapping = true }) + new { + for (crd in parser.parseAll(sourceContents)) { + when (crd is Mapping && crd.getOrNull("kind") == "CustomResourceDefinition") { + deepToTyped.apply(ModuleGenerator.CRD, crd) as ModuleGenerator.CRD + } + } + } + +/// Type conversions when generating property types. +/// +/// This is a two-dimensional mapping, where top-level entries designate CRD names (for example, +/// "restateclusters.restate.dev"). +/// +/// The inner mapping specifies how individual paths within a CRD should be mapped to a custom type. +/// +/// Example: +/// ``` +/// converters { +/// ["restateclusters.restate.dev"] { +/// [List("spec", "compute", "env", "env")] = EnvVar +/// } +/// } +/// ``` +converters: Mapping, Module|Class|TypeAlias>>? + +fixed modules: Listing = new { + for (_crd in crds) { + new ModuleGenerator { + k8sImportPath = module.k8sImportPath + crd = _crd + baseUri = URI.parse(sourceUri)!! + converters = module.converters?.getOrNull(crd.metadata.name) ?? new Mapping {} + logPaths = module.logPaths ?? false + } + } +} + +output { + text = throw("The JSON Schema generator only works with multiple-file output. Try running again with the -m option.") + files { + for (mod in modules) { + ["\(mod.moduleName.split(".").last).pkl"] = mod.moduleNode.output + } + } +} diff --git a/hack/pklcrd/internal/ModuleGenerator.pkl b/hack/pklcrd/internal/ModuleGenerator.pkl new file mode 100644 index 0000000..ce15ca6 --- /dev/null +++ b/hack/pklcrd/internal/ModuleGenerator.pkl @@ -0,0 +1,392 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//===----------------------------------------------------------------------===// +/// Support for generating classes. +@ModuleInfo { minPklVersion = "0.25.0" } +@Unlisted +module k8s.contrib.crd.internal.ModuleGenerator + +import "pkl:reflect" +import "@jsonschema.contrib/internal/Type.pkl" +import "@jsonschema.contrib/internal/TypesGenerator.pkl" +import "@jsonschema.contrib/internal/utils.pkl" +import "@jsonschema/JsonSchema.pkl" +import "@jsonschema/Parser.pkl" +import "@k8s/apiextensions-apiserver/pkg/apis/apiextensions/v1/CustomResourceDefinition.pkl" +import "@k8s/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/CustomResourceDefinition.pkl" as BetaCRD +import "@syntax/ClassNode.pkl" +import "@syntax/ClassOrModuleNode.pkl" +import "@syntax/DocCommentNode.pkl" +import "@syntax/ModuleNode.pkl" +import "@syntax/TypeNode.pkl" +import "@uri/URI.pkl" + +local pcfRenderer = new PcfRenderer { useCustomStringDelimiters = true } +local jsonRenderer = new JsonRenderer {} + +// noinspection Deprecated +typealias CRD = CustomResourceDefinition|BetaCRD + +/// The CRD +crd: CRD + +logPaths: Boolean = false + +k8sImportPath: String + +// noinspection Deprecated +local version: CustomResourceDefinition.CustomResourceDefinitionVersion|BetaCRD.CustomResourceDefinitionVersion = + crd.spec.versions.toList().find((version) -> version.storage) +// noinspection Deprecated +local schema: CustomResourceDefinition.CustomResourceValidation|BetaCRD.CustomResourceValidation = + (if (crd is BetaCRD) version.schema ?? crd.spec.validation else version.schema)!! + +/// The Schema +rootSchema: JsonSchema((s) -> validCRDSchema(s)) = Parser.parse(new JsonRenderer {}.renderDocument(schema.openAPIV3Schema)) as JsonSchema + +local ignoreProperties = Set("apiVersion", "kind", "metadata") +local filteredRootSchema = (rootSchema) { + when (rootSchema.properties != null) { + properties = new { + for (k, v in rootSchema.properties!!) { + when (!ignoreProperties.contains(k)) { + [k] = v + } + } + } + } +} + +// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#validation +local function validCRDSchema(schema: (JsonSchema.Schema|Listing)?) = + if (schema == null) + true + else if (schema is JsonSchema) + validCRDJsonSchema(schema) + else if (schema is Listing) + schema.toList().every((s) -> validCRDSchema(s)) + else + true + +local function validCRDJsonSchema(schema: JsonSchema) = + schema.definitions == null + && schema.$defs == null + && schema.deprecated == null + && schema.$id == null + && schema.patternProperties == null + && schema.readOnly == null + && schema.writeOnly == null + && schema.$ref == null + && schema.uniqueItems != true + && (schema.properties?.toMap()?.values?.every((s) -> validCRDSchema(s)) ?? true) + && schema.additionalProperties != false + && !(schema.additionalProperties != null && schema.properties != null) + && validCRDSchema(schema.additionalProperties) + && validCRDSchema(schema.propertyNames) + && validCRDSchema(schema.items) + && validCRDSchema(schema.additionalItems) + && validCRDSchema(schema.contains) + && validCRDSchema(schema.oneOf) + && validCRDSchema(schema.anyOf) + && validCRDSchema(schema.allOf) + && validCRDSchema(schema.not) + + + +converters: Mapping, Module|Class|TypeAlias> + +/// The URI representing the root schema, used for the doc comment. +baseUri: URI + +/// The name of this module +moduleName: String = crd.spec.group.split(".").reverse() + .add(version.name) + .add(crd.spec.names.kind) + .join(".") + +local typesGenerator: TypesGenerator = new { baseUri = new {}; enclosingModuleName = moduleName } + +/// The types described in this JSON Schema document, included those replaced via converters +moduleTypes: Type.TypeNames = convertedSchemas.map((schema, importAndType) -> Pair(schema, importAndType.type)) + classSchemas + +/// Generate a Pkl [ModuleNode] from a given schema. +moduleNode: ModuleNode = + let (allTypeNames = moduleTypes) + new { + imports { + new { + value = "\(k8sImportPath)/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl" + } + for (_import in convertedSchemas.values.map((it) -> it._import).toSet()) { + new { + value = _import + } + } + } + classes { + for (schema, type in classSchemas) { + generatePklClass(schema, type, allTypeNames) + } + } + declaration { + docComment = getDocComment(filteredRootSchema, "module") + moduleHeader { + name { + parts { + for (part in moduleName.split(".")) { + new { value = part } + } + } + } + moduleExtendsOrAmendsClause { + type = "extends" + extendedModule = "\(k8sImportPath)/K8sResource.pkl" + } + } + } + properties { + new { + modifiers { "fixed" } + name { value = "apiVersion" } + typeAnnotation { + type = new TypeNode.StringLiteralTypeNode { + value = "\(crd.spec.group)/\(version.name)" + } + } + } + new { + modifiers { "fixed" } + name { value = "kind" } + typeAnnotation { + type = new TypeNode.StringLiteralTypeNode { + value = crd.spec.names.kind + } + } + } + new { + name { value = "metadata" } + docComment { + value = """ + Standard object's metadata. + + More info: . + """ + } + typeAnnotation { + type = new TypeNode.NullableTypeNode { + typeNode = new TypeNode.DeclaredTypeNode { + name { + parts { + new { + value = "ObjectMeta" + } + } + } + } + } + } + } + when (isClassLike(filteredRootSchema)) { + ...generateClassBody(filteredRootSchema, allTypeNames) + } + } + } + +/// Determine the doc comments of a schema. +/// +/// This combines a schema's title, description and "default" descriptions into one doc comment. +/// Wraps the comments at 100 columns. +local function getDocComment(schema: JsonSchema|Boolean, type: "class"|"module"|Null): DocCommentNode? = + if (schema is Boolean) + null + else + let (docCommentText: String = + new Listing { + schema.description + when (type is "module") { "This module was generated from the CustomResourceDefinition at <\(baseUri)>." } + when (type is "class"|"module") { getWarnings(schema, type) } + when (schema.default != null) { "Default if undefined: `\(pcfRenderer.renderValue(schema.default))`" } + }.toList().filterNonNull().join("\n\n") + ) + if (docCommentText.isEmpty) null + else + new DocCommentNode { + value = docCommentText + autoWrap = true + } + +local function getWarnings(schema: JsonSchema, type: "class"|"module"): String? = + if (type == "class") null + else if (schema.type != null && schema.type != "object") + "WARN: The root schema's type is `\(jsonRenderer.renderValue(schema.type))`, and cannot be correctly mapped to a Pkl module." + else + null + +local function generateClassBody( + schema: JsonSchema(this.properties != null), + typeNames: Type.TypeNames +): Listing = + new { + for (propName, propSchema in schema.properties!!) { + new { + name { + value = propName + } + when (propSchema is JsonSchema && propSchema.deprecated == true) { + annotations { utils.DEPRECATED } + } + docComment = getDocComment(propSchema, null) + typeAnnotation { + // If this property doesn't appear in the `required` array, mark it as nullable. + // We can't do this within [TypesGenerator] because it doesn't have enough information available. + type = let (underlyingType = typesGenerator.generateTypeNode(propSchema, typeNames)) + if (schema.required?.toSet()?.contains(propName) ?? false) underlyingType + // If the type is already nullable, no need to make it *more* nullable. + else if (underlyingType is TypeNode.NullableTypeNode) underlyingType + else new TypeNode.NullableTypeNode { typeNode = underlyingType } + } + } + } + } + +/// Generates a [ClassNode] from a [CRDJsonSchema]. +local function generatePklClass(schema: JsonSchema, className: Type, typeNames: Type.TypeNames): ClassNode = + new { + docComment = getDocComment(schema, "class") + classHeader { + name { + value = className.name + } + } + properties = generateClassBody(schema, typeNames) + } + +function isClassLike(schema: JsonSchema.Schema): Boolean = + !(schema is Boolean) && schema.properties != null + +/// Determines the name of a type. +/// +/// Try to use the parent property's name as part of the class name in case of conflict. +/// If already at the root, add a number at the end. +local function determineTypeName(path: List, candidateName: String, existingTypeNames: Set, index: Int): Type = + if (existingTypeNames.contains(utils.pascalCase(candidateName))) + if (path.isEmpty) + determineTypeName(path, candidateName + index.toString(), existingTypeNames, index + 1) + else + determineTypeName( + path.dropLast(1), + utils.pascalCase(path.last.capitalize()) + utils.pascalCase(candidateName), + existingTypeNames, + index + ) + else + new { name = utils.pascalCase(candidateName); moduleName = module.moduleName } + +/// The schemas that should be rendered as classes. +/// +/// Classes get rendered for any subschema that has [JsonSchema.properties] defined, and does not show up in converters +local classSchemas: Type.TypeNames = + utils._findMatchingSubSchemas(filteredRootSchema, List(), (elem) -> elem != filteredRootSchema && isClassLike(elem)) + .filter((path, _) -> !pathPrefixes(path).any((prefix) -> converters.containsKey(prefix))) // path or prefix are not explicitly in converters + .entries + .fold(Map(), (accumulator: Type.TypeNames, pair) -> + let (path = pair.first) + let (schema = pair.second) + let (typeName = determineTypeName(path, path.fold(List(), (res: List, el: String) -> res.add(el.capitalize())).join(""), accumulator.values.toSet(), 0)) + accumulator.put(schema, typeName) + ) + +// Return all possible path prefixes of this path, starting with its first entry and ending with the full path +local function pathPrefixes(path: List): List> = new Listing { + for (i, _ in path) { + path.take(i + 1) + } +}.toList() + +/// mapSchema deeply transforms a CRD schema. CRD schemas are significantly simplified, as many fields must not be set. +/// Nested types are *always* represented in properties, items or additionalProperties, even if they are also set in oneOf etc +function mapSchema( + schema: JsonSchema.Schema, + path: Listing, + f: (JsonSchema.Schema) -> JsonSchema.Schema +): JsonSchema.Schema = + let (applied = f.apply(schema)) + (applied) { + when (applied.properties != null) { + properties = new { + for (name, property in applied.properties!!) { + [name] = mapSchema(property, (path) { name }, f) + } + } + } + when (applied.additionalProperties != null) { + additionalProperties = mapSchema(applied.additionalProperties!!, (path) { "additionalProperties" }, f) + } + when (applied.items != null) { + when (applied.items is Listing) { + items = new { + for (i, item in applied.items as Listing) { + [i] = mapSchema(item, (path) { i.toString() }, f) + } + } + } else { + items = mapSchema(applied.items as JsonSchema.Schema, (path) { "items" }, f) + } + } + } + +local convertedSchemas: Map = + utils.findMatchingSubschemas(filteredRootSchema, (elem) -> elem != filteredRootSchema) + .filter((path, _) -> converters.containsKey(if (logPaths) trace(path) else path)) + .entries + .fold(Map(), (accumulator: Map, pair) -> + let (path = pair.first) + let (schema = pair.second) + let (converted = converters[path]) + let (reflected = + if (converted is Module) + reflect.Module(converted) + else if (converted is TypeAlias) + reflect.TypeAlias(converted) + else + reflect.Class(converted) + ) + accumulator.put(schema, new ImportAndType { + type { + moduleName = if (reflected is reflect.Module) module.moduleName else reflected.enclosingDeclaration.name + name = reflected.name.split(".").last + } + local uri = + if (reflected is reflect.Module) + reflected.uri + else + reflected.enclosingDeclaration.uri + _import = determineImportPath(uri) + }) + ) + +/// If any conversions are from the core k8s library, replace the import path with [k8sImportPath] +local function determineImportPath(uri: String): String = + if ( + uri.startsWith("package://pkg.pkl-lang.org/pkl-k8s/k8s") + || uri.startsWith("projectpackage://pkg.pkl-lang.org/pkl-k8s/k8s") + ) + k8sImportPath + uri.dropWhile((it) -> it != "#").drop(1) + else uri + +class ImportAndType { + _import: String + type: Type +} diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index e4911eb..41b0202 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -14,7 +14,7 @@ limitations under the License. // Package v1beta1 contains the input type for this Function // +kubebuilder:object:generate=true -// +groupName=template.fn.crossplane.io +// +groupName=pkl.fn.crossplane.io // +versionName=v1beta1 package v1beta1 diff --git a/package/input/template.fn.crossplane.io_pkls.yaml b/package/input/pkl.fn.crossplane.io_pkls.yaml similarity index 91% rename from package/input/template.fn.crossplane.io_pkls.yaml rename to package/input/pkl.fn.crossplane.io_pkls.yaml index 8d91b1e..2677560 100644 --- a/package/input/template.fn.crossplane.io_pkls.yaml +++ b/package/input/pkl.fn.crossplane.io_pkls.yaml @@ -4,9 +4,9 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.15.0 - name: pkls.template.fn.crossplane.io + name: pkls.pkl.fn.crossplane.io spec: - group: template.fn.crossplane.io + group: pkl.fn.crossplane.io names: categories: - crossplane @@ -19,7 +19,7 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: Input can be used to provide input to this Function. + description: Pkl struct can be used to provide input to this Function. properties: apiVersion: description: |- @@ -39,6 +39,7 @@ spec: metadata: type: object spec: + description: PklSpec specifies references for the function properties: inline: description: Contains a stringified Pkl file diff --git a/pkl/crossplane.contrib.example/PklProject.deps.json b/pkl/crossplane.contrib.example/PklProject.deps.json deleted file mode 100644 index 84d9b93..0000000 --- a/pkl/crossplane.contrib.example/PklProject.deps.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "schemaVersion": 1, - "resolvedDependencies": { - "package://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/k8s.contrib.crd@1.0.7", - "checksums": { - "sha256": "186583646a357cea24bd7d27b94dfd6f5067a6cceb17dfda144d7ba18a3dd152" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1.0.2", - "checksums": { - "sha256": "83ad123a9de2e1e559dbf72370ec8d0353b8d017f37823cdd4ab71f0ce04eb18" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.0.2", - "checksums": { - "sha256": "0746fe618d59187b15ca3e589caf359a285c74efadd8c796da86aaa08202d542" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1.0.8", - "checksums": { - "sha256": "b76f2ea3fd7252b7d6ec4ac7331dea197b4ce9846adc86c3853c13c48b358d13" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1.0.3", - "checksums": { - "sha256": "0b1db5755fa0c7651d5c62e0d5ef8a9ed4ed6411fe31769d06714600162e1589" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema@1.0.4", - "checksums": { - "sha256": "7a11e61df5248c81b9256cae11ae333c13aa5a05101aff1d7b26d7a2e825df96" - } - }, - "package://pkg.pkl-lang.org/pkl-k8s/k8s@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0", - "checksums": { - "sha256": "9ca6002419e405a19b517d506490b46be9279556b5ef2664be0e836df80535e5" - } - }, - "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.xrd@1": { - "type": "local", - "uri": "projectpackage://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.xrd@1.0.0", - "path": "../crossplane.contrib.xrd" - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.openapis.v3@2": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.openapis.v3@2.1.1", - "checksums": { - "sha256": "a3ce491d0e504c14d456d599f988bbb4a2b77915696450dfd65b8e4cef7c9522" - } - }, - "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1": { - "type": "local", - "uri": "projectpackage://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1.0.0", - "path": "../crossplane.contrib" - } - } -} \ No newline at end of file diff --git a/pkl/crossplane.contrib.example/inline.pkl b/pkl/crossplane.contrib.example/inline.pkl new file mode 100644 index 0000000..8f78801 --- /dev/null +++ b/pkl/crossplane.contrib.example/inline.pkl @@ -0,0 +1,40 @@ +amends "@crossplane.contrib/Composition.pkl" + +import "crds/XR.pkl" +import "@crossplane.contrib/Pkl.pkl" +import "@crossplane.contrib/CompositionResponse.pkl" + +local renderer = new PcfRenderer{ + omitNullProperties = true +} + +metadata { + name = "inline-example" +} +spec { + compositeTypeRef { + apiVersion = XR.apiVersion + kind = XR.kind + } + mode = "Pipeline" + pipeline { + new { + step = "pkl-template" + functionRef { + name = "function-pkl" + } + input = new Pkl { + spec { + type = "inline" + inline = new CompositionResponse { + results { + new { + message = "hi" + } + } + }.toPrettyString() + } + } + } + } +} diff --git a/pkl/crossplane.contrib.xrd/PklProject.deps.json b/pkl/crossplane.contrib.xrd/PklProject.deps.json deleted file mode 100644 index 6119d55..0000000 --- a/pkl/crossplane.contrib.xrd/PklProject.deps.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "schemaVersion": 1, - "resolvedDependencies": { - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.deepToTyped@1.0.2", - "checksums": { - "sha256": "0746fe618d59187b15ca3e589caf359a285c74efadd8c796da86aaa08202d542" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema.contrib@1.0.8", - "checksums": { - "sha256": "b76f2ea3fd7252b7d6ec4ac7331dea197b4ce9846adc86c3853c13c48b358d13" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.syntax@1.0.2", - "checksums": { - "sha256": "83ad123a9de2e1e559dbf72370ec8d0353b8d017f37823cdd4ab71f0ce04eb18" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1.0.3", - "checksums": { - "sha256": "0b1db5755fa0c7651d5c62e0d5ef8a9ed4ed6411fe31769d06714600162e1589" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.json_schema@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.json_schema@1.0.4", - "checksums": { - "sha256": "7a11e61df5248c81b9256cae11ae333c13aa5a05101aff1d7b26d7a2e825df96" - } - }, - "package://pkg.pkl-lang.org/pkl-k8s/k8s@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0", - "checksums": { - "sha256": "9ca6002419e405a19b517d506490b46be9279556b5ef2664be0e836df80535e5" - } - }, - "package://pkg.pkl-lang.org/pkl-pantry/org.openapis.v3@2": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-pantry/org.openapis.v3@2.1.1", - "checksums": { - "sha256": "a3ce491d0e504c14d456d599f988bbb4a2b77915696450dfd65b8e4cef7c9522" - } - }, - "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1": { - "type": "local", - "uri": "projectpackage://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1.0.0", - "path": "../crossplane.contrib" - } - } -} \ No newline at end of file diff --git a/pkl/crossplane.contrib/Composition.pkl b/pkl/crossplane.contrib/Composition.pkl new file mode 100644 index 0000000..e002bf7 --- /dev/null +++ b/pkl/crossplane.contrib/Composition.pkl @@ -0,0 +1,636 @@ +/// A Composition defines a collection of managed resources or functions that Crossplane uses to create +/// and manage new composite resources. +/// +/// Read the Crossplane documentation for [more information about +/// Compositions](https://docs.crossplane.io/latest/concepts/compositions). +/// +/// This module was generated from the CustomResourceDefinition at +/// . +module io.crossplane.apiextensions.v1.Composition + +extends "@k8s/K8sResource.pkl" + +import "@k8s/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl" +import "@k8s/K8sResource.pkl" + +fixed apiVersion: "apiextensions.crossplane.io/v1" + +fixed kind: "Composition" + +/// Standard object's metadata. +/// +/// More info: . +metadata: ObjectMeta? + +/// CompositionSpec specifies desired state of a composition. +spec: Spec? + +/// CompositionSpec specifies desired state of a composition. +class Spec { + /// CompositeTypeRef specifies the type of composite resource that this composition is compatible with. + compositeTypeRef: SpecCompositeTypeRef + + /// Environment configures the environment in which resources are rendered. + /// + /// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored unless the relevant + /// Crossplane feature flag is enabled, and may be changed or removed without notice. + environment: SpecEnvironment? + + /// Mode controls what type or "mode" of Composition will be used. + /// + /// "Resources" (the default) indicates that a Composition uses what is commonly referred to as "Patch + /// & Transform" or P&T composition. This mode of Composition uses an array of resources, each a + /// template for a composed resource. + /// + /// "Pipeline" indicates that a Composition specifies a pipeline of Composition Functions, each of + /// which is responsible for producing composed resources that Crossplane should create or update. THE + /// PIPELINE MODE IS A BETA FEATURE. It is not honored if the relevant Crossplane feature flag is + /// disabled. + /// + /// Default if undefined: `"Resources"` + mode: ("Resources"|"Pipeline")? + + /// PatchSets define a named set of patches that may be included by any resource in this Composition. + /// PatchSets cannot themselves refer to other PatchSets. + /// + /// PatchSets are only used by the "Resources" mode of Composition. They are ignored by other modes. + patchSets: Listing? + + /// Pipeline is a list of composition function steps that will be used when a composite resource + /// referring to this composition is created. One of resources and pipeline must be specified - you + /// cannot specify both. + /// + /// The Pipeline is only used by the "Pipeline" mode of Composition. It is ignored by other modes. + /// + /// THIS IS A BETA FIELD. It is not honored if the relevant Crossplane feature flag is disabled. + pipeline: Listing? + + /// PublishConnectionDetailsWithStoreConfig specifies the secret store config with which the connection + /// details of composite resources dynamically provisioned using this composition will be published. + /// + /// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored unless the relevant + /// Crossplane feature flag is enabled, and may be changed or removed without notice. + /// + /// Default if undefined: `{ ["name"] = "default" }` + publishConnectionDetailsWithStoreConfigRef: SpecPublishConnectionDetailsWithStoreConfigRef? + + /// Resources is a list of resource templates that will be used when a composite resource referring to + /// this composition is created. + /// + /// Resources are only used by the "Resources" mode of Composition. They are ignored by other modes. + resources: Listing? + + /// WriteConnectionSecretsToNamespace specifies the namespace in which the connection secrets of + /// composite resource dynamically provisioned using this composition will be created. This field is + /// planned to be replaced in a future release in favor of PublishConnectionDetailsWithStoreConfigRef. + /// Currently, both could be set independently and connection details would be published to both + /// without affecting each other as long as related fields at MR level specified. + writeConnectionSecretsToNamespace: String? +} + +/// CompositeTypeRef specifies the type of composite resource that this composition is compatible with. +class SpecCompositeTypeRef { + /// APIVersion of the type. + apiVersion: String + + /// Kind of the type. + kind: String +} + +/// Environment configures the environment in which resources are rendered. +/// +/// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored unless the relevant Crossplane +/// feature flag is enabled, and may be changed or removed without notice. +class SpecEnvironment { + /// DefaultData statically defines the initial state of the environment. It has the same schema-less + /// structure as the data field in environment configs. It is overwritten by the selected environment + /// configs. + defaultData: Mapping? + + /// EnvironmentConfigs selects a list of `EnvironmentConfig`s. The resolved resources are stored in the + /// composite resource at `spec.environmentConfigRefs` and is only updated if it is null. + /// + /// The list of references is used to compute an in-memory environment at compose time. The data of all + /// object is merged in the order they are listed, meaning the values of EnvironmentConfigs with a + /// larger index take priority over ones with smaller indices. + /// + /// The computed environment can be accessed in a composition using `FromEnvironmentFieldPath` and + /// `CombineFromEnvironment` patches. + environmentConfigs: Listing? + + /// Patches is a list of environment patches that are executed before a composition's resources are + /// composed. + patches: Listing? + + /// Policy represents the Resolve and Resolution policies which apply to all + /// EnvironmentSourceReferences in EnvironmentConfigs list. + policy: SpecEnvironmentPolicy? +} + +/// EnvironmentSource selects a EnvironmentConfig resource. +class SpecEnvironmentEnvironmentConfigsEnvironmentConfig { + /// Ref is a named reference to a single EnvironmentConfig. Either Ref or Selector is required. + ref: SpecEnvironmentEnvironmentConfigsEnvironmentConfigRef? + + /// Selector selects EnvironmentConfig(s) via labels. + selector: SpecEnvironmentEnvironmentConfigsEnvironmentConfigSelector? + + /// Type specifies the way the EnvironmentConfig is selected. Default is `Reference` + /// + /// Default if undefined: `"Reference"` + type: ("Reference"|"Selector")? +} + +/// Ref is a named reference to a single EnvironmentConfig. Either Ref or Selector is required. +class SpecEnvironmentEnvironmentConfigsEnvironmentConfigRef { + /// The name of the object. + name: String +} + +/// Selector selects EnvironmentConfig(s) via labels. +class SpecEnvironmentEnvironmentConfigsEnvironmentConfigSelector { + /// MatchLabels ensures an object with matching labels is selected. + matchLabels: Listing? + + /// MaxMatch specifies the number of extracted EnvironmentConfigs in Multiple mode, extracts all if + /// nil. + maxMatch: Int? + + /// MinMatch specifies the required minimum of extracted EnvironmentConfigs in Multiple mode. + minMatch: Int? + + /// Mode specifies retrieval strategy: "Single" or "Multiple". + /// + /// Default if undefined: `"Single"` + mode: ("Single"|"Multiple")? + + /// SortByFieldPath is the path to the field based on which list of EnvironmentConfigs is + /// alphabetically sorted. + /// + /// Default if undefined: `"metadata.name"` + sortByFieldPath: String? +} + +/// An EnvironmentSourceSelectorLabelMatcher acts like a k8s label selector but can draw the label value +/// from a different path. +class SpecEnvironmentEnvironmentConfigsEnvironmentConfigSelectorMatchLabelsMatchLabel { + /// FromFieldPathPolicy specifies the policy for the valueFromFieldPath. The default is Required, + /// meaning that an error will be returned if the field is not found in the composite resource. + /// Optional means that if the field is not found in the composite resource, that label pair will just + /// be skipped. N.B. other specified label matchers will still be used to retrieve the desired + /// environment config, if any. + /// + /// Default if undefined: `"Required"` + fromFieldPathPolicy: ("Optional"|"Required")? + + /// Key of the label to match. + key: String + + /// Type specifies where the value for a label comes from. + /// + /// Default if undefined: `"FromCompositeFieldPath"` + type: ("FromCompositeFieldPath"|"Value")? + + /// Value specifies a literal label value. + value: String? + + /// ValueFromFieldPath specifies the field path to look for the label value. + valueFromFieldPath: String? +} + +/// EnvironmentPatch is a patch for a Composition environment. +class SpecEnvironmentPatchesPatch { + /// Combine is the patch configuration for a CombineFromComposite or CombineToComposite patch. + combine: SpecEnvironmentPatchesPatchCombine? + + /// FromFieldPath is the path of the field on the resource whose value is to be used as input. Required + /// when type is FromCompositeFieldPath or ToCompositeFieldPath. + fromFieldPath: String? + + /// Policy configures the specifics of patching behaviour. + policy: SpecResourcesResourcePatchesPatchPolicy? + + /// ToFieldPath is the path of the field on the resource whose value will be changed with the result of + /// transforms. Leave empty if you'd like to propagate to the same path as fromFieldPath. + toFieldPath: String? + + /// Transforms are the list of functions that are used as a FIFO pipe for the input to be transformed. + transforms: Listing? + + /// Type sets the patching behaviour to be used. Each patch type may require its own fields to be set + /// on the Patch object. + /// + /// Default if undefined: `"FromCompositeFieldPath"` + type: ("FromCompositeFieldPath"|"ToCompositeFieldPath"|"CombineFromComposite"|"CombineToComposite")? +} + +/// Combine is the patch configuration for a CombineFromComposite or CombineToComposite patch. +class SpecEnvironmentPatchesPatchCombine { + /// Strategy defines the strategy to use to combine the input variable values. Currently only string is + /// supported. + strategy: "string" + + /// String declares that input variables should be combined into a single string, using the relevant + /// settings for formatting purposes. + string: SpecResourcesResourcePatchesPatchCombineString? + + /// Variables are the list of variables whose values will be retrieved and combined. + variables: Listing(!isEmpty) +} + +/// String declares that input variables should be combined into a single string, using the relevant +/// settings for formatting purposes. +class SpecResourcesResourcePatchesPatchCombineString { + /// Format the input using a Go format string. See https://golang.org/pkg/fmt/ for details. + fmt: String +} + +/// A CombineVariable defines the source of a value that is combined with others to form and patch an +/// output value. Currently, this only supports retrieving values from a field path. +class SpecResourcesResourcePatchesPatchCombineVariablesVariable { + /// FromFieldPath is the path of the field on the source whose value is to be used as input. + fromFieldPath: String +} + +/// Policy configures the specifics of patching behaviour. +class SpecResourcesResourcePatchesPatchPolicy { + /// FromFieldPath specifies how to patch from a field path. The default is 'Optional', which means the + /// patch will be a no-op if the specified fromFieldPath does not exist. Use 'Required' if the patch + /// should fail if the specified path does not exist. + fromFieldPath: ("Optional"|"Required")? + + /// MergeOptions Specifies merge options on a field path. + mergeOptions: SpecResourcesResourcePatchesPatchPolicyMergeOptions? +} + +/// MergeOptions Specifies merge options on a field path. +class SpecResourcesResourcePatchesPatchPolicyMergeOptions { + /// Specifies that already existing elements in a merged slice should be preserved + appendSlice: Boolean? + + /// Specifies that already existing values in a merged map should be preserved + keepMapValues: Boolean? +} + +/// Transform is a unit of process whose input is transformed into an output with the supplied +/// configuration. +class SpecResourcesResourcePatchesPatchTransformsTransform { + /// Convert is used to cast the input into the given output type. + convert: SpecResourcesResourcePatchesPatchTransformsTransformConvert? + + /// Map uses the input as a key in the given map and returns the value. + map: Mapping? + + /// Match is a more complex version of Map that matches a list of patterns. + match: SpecResourcesResourcePatchesPatchTransformsTransformMatch? + + /// Math is used to transform the input via mathematical operations such as multiplication. + math: SpecResourcesResourcePatchesPatchTransformsTransformMath? + + /// String is used to transform the input into a string or a different kind of string. Note that the + /// input does not necessarily need to be a string. + string: SpecResourcesResourcePatchesPatchTransformsTransformString? + + /// Type of the transform to be run. + type: "map"|"match"|"math"|"string"|"convert" +} + +/// Convert is used to cast the input into the given output type. +class SpecResourcesResourcePatchesPatchTransformsTransformConvert { + /// The expected input format. + /// + /// * `quantity` - parses the input as a K8s + /// [`resource.Quantity`](https://pkg.go.dev/k8s.io/apimachinery/pkg/api/resource#Quantity). Only used + /// during `string -> float64` conversions. * `json` - parses the input as a JSON string. Only used + /// during `string -> object` or `string -> list` conversions. + /// + /// If this property is null, the default conversion is applied. + format: ("none"|"quantity"|"json")? + + /// ToType is the type of the output of this transform. + toType: "string"|"int"|"int64"|"bool"|"float64"|"object"|"array" +} + +/// Match is a more complex version of Map that matches a list of patterns. +class SpecResourcesResourcePatchesPatchTransformsTransformMatch { + /// Determines to what value the transform should fallback if no pattern matches. + /// + /// Default if undefined: `"Value"` + fallbackTo: ("Value"|"Input")? + + /// The fallback value that should be returned by the transform if now pattern matches. + fallbackValue: Any? + + /// The patterns that should be tested against the input string. Patterns are tested in order. The + /// value of the first match is used as result of this transform. + patterns: Listing? +} + +/// MatchTransformPattern is a transform that returns the value that matches a pattern. +class SpecResourcesResourcePatchesPatchTransformsTransformMatchPatternsPattern { + /// Literal exactly matches the input string (case sensitive). Is required if `type` is `literal`. + literal: String? + + /// Regexp to match against the input string. Is required if `type` is `regexp`. + regexp: String? + + /// The value that is used as result of the transform if the pattern matches. + result: Any + + /// Type specifies how the pattern matches the input. + /// + /// * `literal` - the pattern value has to exactly match (case sensitive) the input string. This is the + /// default. + /// + /// * `regexp` - the pattern treated as a regular expression against which the input string is tested. + /// Crossplane will throw an error if the key is not a valid regexp. + /// + /// Default if undefined: `"literal"` + type: "literal"|"regexp" +} + +/// Math is used to transform the input via mathematical operations such as multiplication. +class SpecResourcesResourcePatchesPatchTransformsTransformMath { + /// ClampMax makes sure that the value is not bigger than the given value. + clampMax: Int? + + /// ClampMin makes sure that the value is not smaller than the given value. + clampMin: Int? + + /// Multiply the value. + multiply: Int? + + /// Type of the math transform to be run. + /// + /// Default if undefined: `"Multiply"` + type: ("Multiply"|"ClampMin"|"ClampMax")? +} + +/// String is used to transform the input into a string or a different kind of string. Note that the +/// input does not necessarily need to be a string. +class SpecResourcesResourcePatchesPatchTransformsTransformString { + /// Optional conversion method to be specified. `ToUpper` and `ToLower` change the letter case of the + /// input string. `ToBase64` and `FromBase64` perform a base64 conversion based on the input string. + /// `ToJson` converts any input value into its raw JSON representation. `ToSha1`, `ToSha256` and + /// `ToSha512` generate a hash value based on the input converted to JSON. `ToAdler32` generate a + /// addler32 hash based on the input string. + convert: ("ToUpper"|"ToLower"|"ToBase64"|"FromBase64"|"ToJson"|"ToSha1"|"ToSha256"|"ToSha512"|"ToAdler32")? + + /// Format the input using a Go format string. See https://golang.org/pkg/fmt/ for details. + fmt: String? + + /// Join defines parameters to join a slice of values to a string. + join: SpecResourcesResourcePatchesPatchTransformsTransformStringJoin? + + /// Extract a match from the input using a regular expression. + regexp: SpecResourcesResourcePatchesPatchTransformsTransformStringRegexp? + + /// Trim the prefix or suffix from the input + trim: String? + + /// Type of the string transform to be run. + /// + /// Default if undefined: `"Format"` + type: ("Format"|"Convert"|"TrimPrefix"|"TrimSuffix"|"Regexp"|"Join")? +} + +/// Join defines parameters to join a slice of values to a string. +class SpecResourcesResourcePatchesPatchTransformsTransformStringJoin { + /// Separator defines the character that should separate the values from each other in the joined + /// string. + separator: String +} + +/// Extract a match from the input using a regular expression. +class SpecResourcesResourcePatchesPatchTransformsTransformStringRegexp { + /// Group number to match. 0 (the default) matches the entire expression. + group: Int? + + /// Match string. May optionally include submatches, aka capture groups. See https://pkg.go.dev/regexp/ + /// for details. + match: String +} + +/// Policy represents the Resolve and Resolution policies which apply to all EnvironmentSourceReferences +/// in EnvironmentConfigs list. +class SpecEnvironmentPolicy { + /// Resolution specifies whether resolution of this reference is required. The default is 'Required', + /// which means the reconcile will fail if the reference cannot be resolved. 'Optional' means this + /// reference will be a no-op if it cannot be resolved. + /// + /// Default if undefined: `"Required"` + resolution: ("Required"|"Optional")? + + /// Resolve specifies when this reference should be resolved. The default is 'IfNotPresent', which will + /// attempt to resolve the reference only when the corresponding field is not present. Use 'Always' to + /// resolve the reference on every reconcile. + resolve: ("Always"|"IfNotPresent")? +} + +/// A PatchSet is a set of patches that can be reused from all resources within a Composition. +class SpecPatchSetsPatchSet { + /// Name of this PatchSet. + name: String + + /// Patches will be applied as an overlay to the base resource. + patches: Listing +} + +/// Patch objects are applied between composite and composed resources. Their behaviour depends on the +/// Type selected. The default Type, FromCompositeFieldPath, copies a value from the composite resource +/// to the composed resource, applying any defined transformers. +class SpecResourcesResourcePatchesPatch { + /// Combine is the patch configuration for a CombineFromComposite, CombineFromEnvironment, + /// CombineToComposite or CombineToEnvironment patch. + combine: SpecResourcesResourcePatchesPatchCombine? + + /// FromFieldPath is the path of the field on the resource whose value is to be used as input. Required + /// when type is FromCompositeFieldPath, FromEnvironmentFieldPath, ToCompositeFieldPath, + /// ToEnvironmentFieldPath. + fromFieldPath: String? + + /// PatchSetName to include patches from. Required when type is PatchSet. + patchSetName: String? + + /// Policy configures the specifics of patching behaviour. + policy: SpecResourcesResourcePatchesPatchPolicy? + + /// ToFieldPath is the path of the field on the resource whose value will be changed with the result of + /// transforms. Leave empty if you'd like to propagate to the same path as fromFieldPath. + toFieldPath: String? + + /// Transforms are the list of functions that are used as a FIFO pipe for the input to be transformed. + transforms: Listing? + + /// Type sets the patching behaviour to be used. Each patch type may require its own fields to be set + /// on the Patch object. + /// + /// Default if undefined: `"FromCompositeFieldPath"` + type: ( + "FromCompositeFieldPath" + |"FromEnvironmentFieldPath" + |"PatchSet" + |"ToCompositeFieldPath" + |"ToEnvironmentFieldPath" + |"CombineFromEnvironment" + |"CombineFromComposite" + |"CombineToComposite" + |"CombineToEnvironment")? +} + +/// Combine is the patch configuration for a CombineFromComposite, CombineFromEnvironment, +/// CombineToComposite or CombineToEnvironment patch. +class SpecResourcesResourcePatchesPatchCombine { + /// Strategy defines the strategy to use to combine the input variable values. Currently only string is + /// supported. + strategy: "string" + + /// String declares that input variables should be combined into a single string, using the relevant + /// settings for formatting purposes. + string: SpecResourcesResourcePatchesPatchCombineString? + + /// Variables are the list of variables whose values will be retrieved and combined. + variables: Listing(!isEmpty) +} + +/// A PipelineStep in a Composition Function pipeline. +class SpecPipelinePipeline { + /// Credentials are optional credentials that the Composition Function needs. + credentials: Listing? + + /// FunctionRef is a reference to the Composition Function this step should execute. + functionRef: SpecPipelinePipelineFunctionRef + + /// Input is an optional, arbitrary Kubernetes resource (i.e. a resource with an apiVersion and kind) + /// that will be passed to the Composition Function as the 'input' of its RunFunctionRequest. + input: K8sResource? + + /// Step name. Must be unique within its Pipeline. + step: String +} + +/// FunctionCredentials are optional credentials that a Composition Function needs to run. +class SpecPipelinePipelineCredentialsCredential { + /// Name of this set of credentials. + name: String + + /// A SecretRef is a reference to a secret containing credentials that should be supplied to the + /// function. + secretRef: SpecPipelinePipelineCredentialsCredentialSecretRef? + + /// Source of the function credentials. + source: "None"|"Secret" +} + +/// A SecretRef is a reference to a secret containing credentials that should be supplied to the +/// function. +class SpecPipelinePipelineCredentialsCredentialSecretRef { + /// Name of the secret. + name: String + + /// Namespace of the secret. + namespace: String +} + +/// FunctionRef is a reference to the Composition Function this step should execute. +class SpecPipelinePipelineFunctionRef { + /// Name of the referenced Function. + name: String +} + +/// PublishConnectionDetailsWithStoreConfig specifies the secret store config with which the connection +/// details of composite resources dynamically provisioned using this composition will be published. +/// +/// THIS IS AN ALPHA FIELD. Do not use it in production. It is not honored unless the relevant Crossplane +/// feature flag is enabled, and may be changed or removed without notice. +/// +/// Default if undefined: `{ ["name"] = "default" }` +class SpecPublishConnectionDetailsWithStoreConfigRef { + /// Name of the referenced StoreConfig. + name: String +} + +/// ComposedTemplate is used to provide information about how the composed resource should be processed. +class SpecResourcesResource { + /// Base is the target resource that the patches will be applied on. + base: Dynamic + + /// ConnectionDetails lists the propagation secret keys from this target resource to the composition + /// instance connection secret. + connectionDetails: Listing? + + /// A Name uniquely identifies this entry within its Composition's resources array. Names are optional + /// but *strongly* recommended. When all entries in the resources array are named entries may added, + /// deleted, and reordered as long as their names do not change. When entries are not named the length + /// and order of the resources array should be treated as immutable. Either all or no entries must be + /// named. + name: String? + + /// Patches will be applied as overlay to the base resource. + patches: Listing? + + /// ReadinessChecks allows users to define custom readiness checks. All checks have to return true in + /// order for resource to be considered ready. The default readiness check is to have the "Ready" + /// condition to be "True". + /// + /// Default if undefined: `{ new { ["matchCondition"] { ["status"] = "True" ["type"] = "Ready" } + /// ["type"] = "MatchCondition" } }` + readinessChecks: Listing? +} + +/// ConnectionDetail includes the information about the propagation of the connection information from +/// one secret to another. +class SpecResourcesResourceConnectionDetailsConnectionDetail { + /// FromConnectionSecretKey is the key that will be used to fetch the value from the composed + /// resource's connection secret. + fromConnectionSecretKey: String? + + /// FromFieldPath is the path of the field on the composed resource whose value to be used as input. + /// Name must be specified if the type is FromFieldPath. + fromFieldPath: String? + + /// Name of the connection secret key that will be propagated to the connection secret of the + /// composition instance. Leave empty if you'd like to use the same key name. + name: String? + + /// Type sets the connection detail fetching behaviour to be used. Each connection detail type may + /// require its own fields to be set on the ConnectionDetail object. If the type is omitted Crossplane + /// will attempt to infer it based on which other fields were specified. If multiple fields are + /// specified the order of precedence is: 1. FromValue 2. FromConnectionSecretKey 3. FromFieldPath + type: ("FromConnectionSecretKey"|"FromFieldPath"|"FromValue")? + + /// Value that will be propagated to the connection secret of the composite resource. May be set to + /// inject a fixed, non-sensitive connection secret value, for example a well-known port. + value: String? +} + +/// ReadinessCheck is used to indicate how to tell whether a resource is ready for consumption. +class SpecResourcesResourceReadinessChecksReadinessCheck { + /// FieldPath shows the path of the field whose value will be used. + fieldPath: String? + + /// MatchCondition specifies the condition you'd like to match if you're using "MatchCondition" type. + matchCondition: SpecResourcesResourceReadinessChecksReadinessCheckMatchCondition? + + /// MatchInt is the value you'd like to match if you're using "MatchInt" type. + matchInteger: Int? + + /// MatchString is the value you'd like to match if you're using "MatchString" type. + matchString: String? + + /// Type indicates the type of probe you'd like to use. + type: "MatchString"|"MatchInteger"|"NonEmpty"|"MatchCondition"|"MatchTrue"|"MatchFalse"|"None" +} + +/// MatchCondition specifies the condition you'd like to match if you're using "MatchCondition" type. +class SpecResourcesResourceReadinessChecksReadinessCheckMatchCondition { + /// Status is the status of the condition you'd like to match. + /// + /// Default if undefined: `"True"` + status: String + + /// Type indicates the type of condition you'd like to use. + /// + /// Default if undefined: `"Ready"` + type: String +} diff --git a/pkl/crossplane.contrib/CompositionResponse.pkl b/pkl/crossplane.contrib/CompositionResponse.pkl index b47f21c..3ddaa8b 100644 --- a/pkl/crossplane.contrib/CompositionResponse.pkl +++ b/pkl/crossplane.contrib/CompositionResponse.pkl @@ -110,3 +110,8 @@ local class protobufDuration { seconds: Int // int64 not available in pkl nanos: Int } + +// Convert to a pretty printed Pcf file +function toPrettyString(): String = new PcfRenderer { + omitNullProperties = true +}.renderDocument(this) diff --git a/pkl/crossplane.contrib/Pkl.pkl b/pkl/crossplane.contrib/Pkl.pkl new file mode 100644 index 0000000..a378ffc --- /dev/null +++ b/pkl/crossplane.contrib/Pkl.pkl @@ -0,0 +1,44 @@ +/// Pkl struct can be used to provide input to this Function. +/// +/// This module was generated from the CustomResourceDefinition at +/// . +module io.crossplane.fn.template.v1beta1.Pkl + +extends "@k8s/K8sResource.pkl" + +import "@k8s/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl" + +fixed apiVersion: "template.fn.crossplane.io/v1beta1" + +fixed kind: "Pkl" + +/// Standard object's metadata. +/// +/// More info: . +metadata: ObjectMeta? + +/// PklSpec specifies references for the function +spec: Spec? + +/// PklSpec specifies references for the function +class Spec { + /// Contains a stringified Pkl file + inline: String? + + /// Reference to a Pklfile and Project + `local`: Local? + + type: ("uri"|"inline"|"local")? + + /// Use URI Scheme to load Project/Package + uri: String? +} + +/// Reference to a Pklfile and Project +class Local { + /// Path to file relative from the Project Dir + file: String? + + /// Path to the Project containing a Pklfile + projectDir: String? +} diff --git a/pkl/crossplane.contrib/PklProject.deps.json b/pkl/crossplane.contrib/PklProject.deps.json deleted file mode 100644 index 9a7a11a..0000000 --- a/pkl/crossplane.contrib/PklProject.deps.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "schemaVersion": 1, - "resolvedDependencies": { - "package://pkg.pkl-lang.org/pkl-k8s/k8s@1": { - "type": "remote", - "uri": "projectpackage://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0", - "checksums": { - "sha256": "9ca6002419e405a19b517d506490b46be9279556b5ef2664be0e836df80535e5" - } - } - } -} \ No newline at end of file From 18acad46e17e5f095d0b09d3d84c470bf0cf1b81 Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:14:49 +0200 Subject: [PATCH 2/5] fix input function group refs Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- README.md | 2 +- example/full/composition.yaml | 2 +- example/inline/composition.yaml | 2 +- example/minimal/composition.yaml | 2 +- pkl/crossplane.contrib/Pkl.pkl | 6 +++--- pkl/crossplane.contrib/crossplane.pkl | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bb3cef9..a1a39ca 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ spec: functionRef: name: function-pkl input: - apiVersion: template.fn.crossplane.io/v1beta1 + apiVersion: pkl.fn.crossplane.io/v1beta1 kind: Pkl spec: type: uri diff --git a/example/full/composition.yaml b/example/full/composition.yaml index 4e4b625..a8dc89e 100644 --- a/example/full/composition.yaml +++ b/example/full/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pkl input: - apiVersion: template.fn.crossplane.io/v1beta1 + apiVersion: pkl.fn.crossplane.io/v1beta1 kind: Pkl spec: type: uri diff --git a/example/inline/composition.yaml b/example/inline/composition.yaml index 2eb3d8b..c9132b6 100644 --- a/example/inline/composition.yaml +++ b/example/inline/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pkl input: - apiVersion: template.fn.crossplane.io/v1beta1 + apiVersion: pkl.fn.crossplane.io/v1beta1 kind: Pkl spec: type: inline diff --git a/example/minimal/composition.yaml b/example/minimal/composition.yaml index 930cb09..2ed7194 100644 --- a/example/minimal/composition.yaml +++ b/example/minimal/composition.yaml @@ -12,7 +12,7 @@ spec: functionRef: name: function-pkl input: - apiVersion: template.fn.crossplane.io/v1beta1 + apiVersion: pkl.fn.crossplane.io/v1beta1 kind: Pkl spec: type: uri diff --git a/pkl/crossplane.contrib/Pkl.pkl b/pkl/crossplane.contrib/Pkl.pkl index a378ffc..81556d2 100644 --- a/pkl/crossplane.contrib/Pkl.pkl +++ b/pkl/crossplane.contrib/Pkl.pkl @@ -1,14 +1,14 @@ /// Pkl struct can be used to provide input to this Function. /// /// This module was generated from the CustomResourceDefinition at -/// . -module io.crossplane.fn.template.v1beta1.Pkl +/// . +module io.crossplane.fn.pkl.v1beta1.Pkl extends "@k8s/K8sResource.pkl" import "@k8s/apimachinery/pkg/apis/meta/v1/ObjectMeta.pkl" -fixed apiVersion: "template.fn.crossplane.io/v1beta1" +fixed apiVersion: "pkl.fn.crossplane.io/v1beta1" fixed kind: "Pkl" diff --git a/pkl/crossplane.contrib/crossplane.pkl b/pkl/crossplane.contrib/crossplane.pkl index dec6751..acc4165 100644 --- a/pkl/crossplane.contrib/crossplane.pkl +++ b/pkl/crossplane.contrib/crossplane.pkl @@ -135,8 +135,8 @@ local function convert(value: Any, type: reflect.Type?): Any = let (template = getResourceTemplate(value, customResourceTemplates)) // TODO: replace customResourceTemplate with actual val value .toMap() - .map((k, v) -> Pair(k, convert(v, reflect.Class(template.getClass()).properties.getOrNull(k)?.type))) - .toTyped(template.getClass()) + .map((k, v) -> Pair(k, convert(v, reflect.Class(pkl.getClass()).properties.getOrNull(k)?.type))) + .toTyped(pkl.getClass()) else if (value is Mapping && referent is reflect.Class && referent.isSubclassOf(typedClass)) value .toMap() From 5314695eb11147ac87ae88afeb4f25982daeb76e Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:19:37 +0200 Subject: [PATCH 3/5] add pkl resolve for hack dir Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- .gitignore | 5 ++++- Makefile | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ddb842d..d66d3ed 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ go.work .idea/ # Temporary Debug Binaries -__debug_bin* \ No newline at end of file +__debug_bin* + +# Pkl +*PklProject.deps.json \ No newline at end of file diff --git a/Makefile b/Makefile index 64cd5d7..2affee1 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,10 @@ XRD_PARAM := $(if $(LATEST_XRD),-e CROSSPLANE_CONTRIB_XRD_VERSION="$(LATEST pkl-resolve: pkl project resolve $(REPO_PARAM) $(CORE_PARAM) $(EXAMPLE_PARAM) $(XRD_PARAM) ./pkl/*/ +.PHONY: pkl-resolve-hack +pkl-resolve-hack: + pkl project resolve ./hack/pklcrd/ + .PHONY: pkl-package pkl-package: pkl-resolve $(eval PACKAGE_FILES := $(shell \ @@ -49,7 +53,7 @@ pkl-release: check-tag pkl-package PROJECT_DIR := $(dir $(firstword $(MAKEFILE_LIST))) .PHONY: generate -generate: pkl-resolve +generate: pkl-resolve pkl-resolve-hack go generate ./... pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module.pkl pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module-composition-fix.pkl From 99c897e017586f1d3cd7b5ed0d018945ff749a58 Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 3 Jul 2024 19:38:53 +0200 Subject: [PATCH 4/5] set actual upcoming first version in references Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- README.md | 2 +- example/full/composition.yaml | 2 +- example/minimal/composition.yaml | 2 +- pkl/crossplane.contrib.example/DEVELOP.md | 2 +- pkl/crossplane.contrib.xrd/generate.pkl | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a1a39ca..8a52364 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ spec: spec: type: uri # This pkl file is at `pkl/crossplane.contrib.example/full.pkl` in this repo - uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@1.0.0#/full.pkl" + uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@0.0.1#/full.pkl" ``` ### Example diff --git a/example/full/composition.yaml b/example/full/composition.yaml index a8dc89e..7be0286 100644 --- a/example/full/composition.yaml +++ b/example/full/composition.yaml @@ -17,4 +17,4 @@ spec: spec: type: uri # This pkl file is at `pkl/crossplane.contrib.example/full.pkl` in this repo - uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@1.0.0#/full.pkl" + uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@0.0.1#/full.pkl" diff --git a/example/minimal/composition.yaml b/example/minimal/composition.yaml index 2ed7194..ed69431 100644 --- a/example/minimal/composition.yaml +++ b/example/minimal/composition.yaml @@ -17,4 +17,4 @@ spec: spec: type: uri # This pkl file is at `pkl/crossplane-example/minimal.pkl` in this repo - uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane-example@1.0.0#/minimal.pkl" + uri: "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane-example@0.0.1#/minimal.pkl" diff --git a/pkl/crossplane.contrib.example/DEVELOP.md b/pkl/crossplane.contrib.example/DEVELOP.md index d38fed8..7ec8d83 100644 --- a/pkl/crossplane.contrib.example/DEVELOP.md +++ b/pkl/crossplane.contrib.example/DEVELOP.md @@ -7,7 +7,7 @@ Either can be converted to a Pkl Module. ## Create XRD When Creating a Pkl file ensure it amends CompositeResourceDefinition ```pkl -amends "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crospslane.contrib.xrd@1.0.0#/CompositeResourceDefinition.pkl" +amends "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crospslane.contrib.xrd@#/CompositeResourceDefinition.pkl" ``` view [xrds/ExampleXR.pkl](xrds/ExampleXR.pkl) for more details on how to Build it. diff --git a/pkl/crossplane.contrib.xrd/generate.pkl b/pkl/crossplane.contrib.xrd/generate.pkl index ef2ef45..50de9ba 100644 --- a/pkl/crossplane.contrib.xrd/generate.pkl +++ b/pkl/crossplane.contrib.xrd/generate.pkl @@ -69,8 +69,8 @@ import "@deepToTyped/deepToTyped.pkl" /// The version of the Pkl Kubernetes package to import. /// /// This property is not used if [k8sImportPath] is set directly. -k8sVersion: String(semver.isValid(this)) = "1.0.1" -crossplaneVersion: String(semver.isValid(this)) = "1.0.0" +k8sVersion: String(semver.isValid(this)) = "1.1.0" +crossplaneVersion: String(semver.isValid(this)) = "0.0.1" /// The base path to use for the Kubernetes imports. /// From 8f94e30a729dedb38c09527ab6c58e36bbc26399 Mon Sep 17 00:00:00 2001 From: Tim <32556895+Avarei@users.noreply.github.com> Date: Wed, 3 Jul 2024 20:13:57 +0200 Subject: [PATCH 5/5] update and autogenerate example/inline/composition Signed-off-by: Tim <32556895+Avarei@users.noreply.github.com> --- Makefile | 2 +- example/inline/composition.yaml | 78 +++---------------- pkl/crossplane.contrib.example/DEVELOP.md | 4 + .../{inline.pkl => composition-inline.pkl} | 13 +--- .../minimal-no-project.pkl | 28 +++++++ 5 files changed, 44 insertions(+), 81 deletions(-) rename pkl/crossplane.contrib.example/{inline.pkl => composition-inline.pkl} (60%) create mode 100644 pkl/crossplane.contrib.example/minimal-no-project.pkl diff --git a/Makefile b/Makefile index 2affee1..3b5706b 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ generate: pkl-resolve pkl-resolve-hack go generate ./... pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module.pkl pkl eval --working-dir $(PROJECT_DIR)hack/pklcrd -m ../../pkl/crossplane.contrib crd2module-composition-fix.pkl - + pkl eval --working-dir $(PROJECT_DIR)pkl/crossplane.contrib.example composition-inline.pkl > $(PROJECT_DIR)example/inline/composition.yaml .PHONY: build-image build-image: diff --git a/example/inline/composition.yaml b/example/inline/composition.yaml index c9132b6..7945c24 100644 --- a/example/inline/composition.yaml +++ b/example/inline/composition.yaml @@ -1,68 +1,27 @@ apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: function-pkl + name: inline-example spec: compositeTypeRef: apiVersion: example.crossplane.io/v1 kind: XR mode: Pipeline pipeline: - - step: run-the-template - functionRef: + - functionRef: name: function-pkl input: apiVersion: pkl.fn.crossplane.io/v1beta1 kind: Pkl spec: - type: inline - # This pkl file is at `pkl/crossplane.contrib.example/full.pkl` in this repo inline: | - amends "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1.0.0#/CompositionResponse.pkl" - import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1.0.0#/Resource.pkl" - import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@1.0.0#/Crossplane.pkl" - - import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@1.0.0#/crds/XR.pkl" - import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@1.0.0#/crds/Object.pkl" - - import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.0.1#/api/core/v1/ConfigMap.pkl" - - local request = new Crossplane { - customResourceTemplates = new { - ["XR"] { - ["example.crossplane.io/v1"] = XR - } - ["Object"] { - ["kubernetes.crossplane.io/v1alpha2"] = Object - } - } - }.Request - - local observedCompositeResource: XR = request.observed.composite.resource as XR - local extraResource: Object? = request.getExtraResource("ineed", 0)?.resource as Object? - - requirements { - extraResources { - ["ineed"] { - apiVersion = Object.apiVersion - kind = Object.kind - match { - matchName = "required" - } - } - } - } + amends "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@0.0.1#/CompositionResponse.pkl" + import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@0.0.1#/crds/Object.pkl" + import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0#/api/core/v1/ConfigMap.pkl" desired { - composite { - resource = new XR { - status { - someStatus = "pretty status" - } - } - } resources { - ["cm-one"] = new Resource { + ["cm-minimal"] = new { resource = new Object { metadata { name = "cm-one" @@ -71,35 +30,18 @@ spec: forProvider { manifest = new ConfigMap { metadata { - name = "cm-one" + name = "cm-minimal" namespace = "crossplane-system" } data { - ["foo"] = observedCompositeResource?.metadata?.name ?? throw("Composite could not find observed composite name") - ["required"] = extraResource?.metadata?.name ?? "i could not find what I needed..." + ["foo"] = "bar" } } } } } - ready = Ready_READY_TRUE } - } } - results { - new { - severity = Severity_SEVERITY_NORMAL - message = "welcome" - } - } - context { - ["greetings"] = "with <3 from function-pkl" - when (request.context.containsKey("apiextensions.crossplane.io/environment")) { - ["apiextensions.crossplane.io/environment"] = request.context.getOrNull("apiextensions.crossplane.io/environment") - } - } - - meta = if (request.meta != null) new ResponseMeta { - ttl = 60.s - } else null + type: inline + step: pkl-template diff --git a/pkl/crossplane.contrib.example/DEVELOP.md b/pkl/crossplane.contrib.example/DEVELOP.md index 7ec8d83..7674585 100644 --- a/pkl/crossplane.contrib.example/DEVELOP.md +++ b/pkl/crossplane.contrib.example/DEVELOP.md @@ -52,6 +52,10 @@ local request = new crossplane { ``` To Parse the Request automatically into Pkl Language all K8sResources used must be declared in customResourceTemplates in the specified form. +## Create a Composition +The Composition itself can be created in [yaml](../../example/full/composition.yaml) or in [pkl](composition-inline.pkl). The latter is especially useful, if the the function is used with an inline Pkl file. + + ## Create PklProject [PklProject](PklProject) contains the dependencies of the Project as well as Metadata on where it will be Published, version and name. diff --git a/pkl/crossplane.contrib.example/inline.pkl b/pkl/crossplane.contrib.example/composition-inline.pkl similarity index 60% rename from pkl/crossplane.contrib.example/inline.pkl rename to pkl/crossplane.contrib.example/composition-inline.pkl index 8f78801..d772952 100644 --- a/pkl/crossplane.contrib.example/inline.pkl +++ b/pkl/crossplane.contrib.example/composition-inline.pkl @@ -2,11 +2,6 @@ amends "@crossplane.contrib/Composition.pkl" import "crds/XR.pkl" import "@crossplane.contrib/Pkl.pkl" -import "@crossplane.contrib/CompositionResponse.pkl" - -local renderer = new PcfRenderer{ - omitNullProperties = true -} metadata { name = "inline-example" @@ -26,13 +21,7 @@ spec { input = new Pkl { spec { type = "inline" - inline = new CompositionResponse { - results { - new { - message = "hi" - } - } - }.toPrettyString() + inline = read("minimal-no-project.pkl").text } } } diff --git a/pkl/crossplane.contrib.example/minimal-no-project.pkl b/pkl/crossplane.contrib.example/minimal-no-project.pkl new file mode 100644 index 0000000..4c0cd7d --- /dev/null +++ b/pkl/crossplane.contrib.example/minimal-no-project.pkl @@ -0,0 +1,28 @@ +amends "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib@0.0.1#/CompositionResponse.pkl" +import "package://pkg.pkl-lang.org/github.com/crossplane-contrib/function-pkl/crossplane.contrib.example@0.0.1#/crds/Object.pkl" +import "package://pkg.pkl-lang.org/pkl-k8s/k8s@1.1.0#/api/core/v1/ConfigMap.pkl" + +desired { + resources { + ["cm-minimal"] = new { + resource = new Object { + metadata { + name = "cm-one" + } + spec { + forProvider { + manifest = new ConfigMap { + metadata { + name = "cm-minimal" + namespace = "crossplane-system" + } + data { + ["foo"] = "bar" + } + } + } + } + } + } + } +}