diff --git a/content/cli/master/get-started/_index.md b/content/cli/master/get-started/_index.md new file mode 100644 index 000000000..0eb2e89d1 --- /dev/null +++ b/content/cli/master/get-started/_index.md @@ -0,0 +1,7 @@ +--- +title: "Get Started" +weight: 200 +description: "Get started with the Crossplane CLI" +--- + +{{< auto-index >}} diff --git a/content/cli/master/get-started/get-started-with-control-plane-projects.md b/content/cli/master/get-started/get-started-with-control-plane-projects.md new file mode 100644 index 000000000..53b7fa3e6 --- /dev/null +++ b/content/cli/master/get-started/get-started-with-control-plane-projects.md @@ -0,0 +1,1135 @@ +--- +title: Get Started with Control Plane Projects +weight: 100 +description: "Build a control plane project with the Crossplane CLI" +--- + +This guide shows how to use the Crossplane CLI to build a platform API from +scratch. You create a _project_, define a new `WebApp` custom resource, and +configure how Crossplane composes it. When a user creates a `WebApp`, Crossplane +creates a Kubernetes `Deployment` and a `Service`. + +The Crossplane CLI scaffolds the project, generates the API and composition, and +runs the project on a local development control plane so you can test it without +deploying to a shared cluster. + +{{}} +This guide shows how to write the composition function in Go, Python, KCL, and +templated YAML. You can pick your preferred language. +{{}} + +A `WebApp` custom resource looks like this: + +```yaml +apiVersion: platform.example.com/v1alpha1 +kind: WebApp +metadata: + name: podinfo + namespace: default +spec: + image: docker.io/stefanprodan/podinfo:6.11.0 + replicas: 3 + ports: [9898] +``` + +**The `WebApp` is the custom API your users use to deploy a containerized +workload.** + +When a user creates a `WebApp`, Crossplane creates a `Deployment` and a +`Service`: + +```yaml +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo + namespace: default + labels: + app.kubernetes.io/name: podinfo # Copied from the WebApp's name +spec: + replicas: 3 # Copied from the WebApp's spec + selector: + matchLabels: + app.kubernetes.io/name: podinfo + template: + metadata: + labels: + app.kubernetes.io/name: podinfo + spec: + containers: + - name: podinfo + image: docker.io/stefanprodan/podinfo:6.11.0 # Copied from the WebApp's spec + ports: + - containerPort: 9898 # Copied from the WebApp's spec +--- +apiVersion: v1 +kind: Service +metadata: + name: podinfo + namespace: default +spec: + selector: + app.kubernetes.io/name: podinfo + ports: + - protocol: TCP + port: 9898 + targetPort: 9898 +``` + +## Prerequisites + +This guide requires: + +* The [Crossplane CLI]({{}}) installed +* A Docker compatible container runtime + +The CLI builds the project's functions and runs a local development control +plane in a [KIND](https://kind.sigs.k8s.io) cluster. Both require a working +Docker installation. You don't need an existing Kubernetes cluster. + +## Create the project + +A Crossplane _project_ is a directory that contains everything that makes up a +platform API: the API definitions, compositions, embedded functions, examples, +and a `crossplane-project.yaml` metadata file. + +Initialize a new project named `example-project-webapp`: + +```shell +crossplane project init example-project-webapp +``` + +The `init` command creates a directory named after the project, containing a +minimal `crossplane-project.yaml` and the standard project directories: + +```shell {copy-lines="1"} +tree example-project-webapp +example-project-webapp +├── apis +├── crossplane-project.yaml +├── examples +├── functions +├── operations +└── tests + +6 directories, 1 file +``` + +Change into the new project directory: + +```shell +cd example-project-webapp +``` + +Run the remaining commands in this guide from the project directory. + +## The `crossplane-project.yaml` file + +The `crossplane-project.yaml` file contains metadata about your project. This +metadata influences how the Crossplane CLI builds your project into +[Crossplane packages (xpkgs)]({{}}). For now, the file +sets only the project's name and OCI repository: + +```yaml +apiVersion: dev.crossplane.io/v1alpha1 +kind: Project +metadata: + name: example-project-webapp +spec: + repository: example.com/my-org/example-project-webapp +``` + +For the rest of this guide, you can leave the placeholder `repository` as +is. You must update it if you want to push your project to an OCI registry +later. + +## Define the API + +Crossplane calls a custom resource that's powered by composition a _composite +resource_, or XR. You define the schema of an XR with a _composite resource +definition_, or XRD. + +{{}} +Kubernetes calls user-defined API resources _custom resources_. + +Crossplane calls user-defined API resources that use composition _composite +resources_. + +A composite resource is a kind of custom resource. +{{}} + +Rather than write the XRD by hand, you can write an example XR or describe the +API with [SimpleSchema](https://kro.run/api/specifications/simple-schema/), then +let the CLI generate the XRD. + +{{< tabs >}} + +{{< tab "Example XR" >}} + +Create an example XR at `examples/webapp/podinfo.yaml` showing all the fields +the `WebApp` API should include: + +```yaml +apiVersion: platform.example.com/v1alpha1 +kind: WebApp +metadata: + name: podinfo + namespace: default +spec: + image: docker.io/stefanprodan/podinfo:6.11.0 + replicas: 3 + ports: [9898] +``` + +The `crossplane xrd generate` command reads the example XR, infers the field +types from their contents, and generates an appropriate XRD: + +```shell +crossplane xrd generate examples/webapp/podinfo.yaml --from xr +``` + +The command writes the generated XRD to `apis/webapps/definition.yaml`: + +```yaml +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: webapps.platform.example.com +spec: + group: platform.example.com + names: + categories: + - crossplane + kind: WebApp + plural: webapps + scope: Namespaced + versions: + - name: v1alpha1 + referenceable: true + schema: + openAPIV3Schema: + description: WebApp is the Schema for the WebApp API. + properties: + spec: + description: WebAppSpec defines the desired state of WebApp. + properties: + image: + type: string + ports: + items: + type: number + type: array + replicas: + type: number + type: object + status: + description: WebAppStatus defines the observed state of WebApp. + type: object + required: + - spec + type: object + served: true +``` + +{{}} +Generating an XRD from an example XR doesn't let you specify field types, +defaults, validation rules, or field descriptions. SimpleSchema provides a more +powerful, but more complex, way to describe your API. +{{}} + +{{< /tab >}} + +{{< tab "SimpleSchema" >}} +Create a SimpleSchema document at `apis/webapps/schema.yaml` that describes the +`WebApp` API: + +```yaml +apiVersion: platform.example.com/v1alpha1 +kind: WebApp +spec: + image: string | required=true description="OCI image for the webapp" + replicas: integer | default=1 minimum=1 maximum=100 description="Number of replicas to run" + ports: "[]integer | default=[80] description=\"Ports to expose from the application container\"" +``` + +The SimpleSchema document lists each field of the API's `spec` along with its +type, validation rules, and description. + +Generate the XRD from the SimpleSchema document: + +```shell +crossplane xrd generate apis/webapps/schema.yaml --from simpleschema +``` + +The command writes the generated XRD to `apis/webapps/definition.yaml`: + +```yaml +apiVersion: apiextensions.crossplane.io/v2 +kind: CompositeResourceDefinition +metadata: + name: webapps.platform.example.com +spec: + group: platform.example.com + names: + categories: + - crossplane + kind: WebApp + plural: webapps + scope: Namespaced + versions: + - name: v1alpha1 + referenceable: true + schema: + openAPIV3Schema: + description: WebApp is the Schema for the WebApp API. + properties: + spec: + properties: + image: + description: OCI image for the webapp + type: string + ports: + default: + - 80 + description: Ports to expose from the application container + items: + type: integer + type: array + replicas: + default: 1 + description: Number of replicas to run + maximum: 100 + minimum: 1 + type: integer + required: + - image + type: object + status: + type: object + required: + - spec + type: object + served: true +``` + +{{< /tab >}} +{{< /tabs >}} + +The XRD is the contract between your users and your platform. It defines the +fields a user can set on a `WebApp`, the validation Crossplane applies, and the +default values Crossplane fills in. + +## Generate the composition + +A _composition_ tells Crossplane what to do when a user creates or updates a +`WebApp`. It contains a pipeline of functions that build the resources +Crossplane creates. + +Generate a composition from the XRD: + +```shell +crossplane composition generate apis/webapps/definition.yaml +``` + +The command writes the composition to `apis/webapps/composition.yaml`: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: webapps.platform.example.com +spec: + compositeTypeRef: + apiVersion: platform.example.com/v1alpha1 + kind: WebApp + mode: Pipeline + pipeline: + - functionRef: + name: crossplane-contrib-function-auto-ready + step: crossplane-contrib-function-auto-ready +``` + + +The generated composition contains a single pipeline step that runs +[function-auto-ready](https://github.com/crossplane-contrib/function-auto-ready), +which marks the `WebApp` ready when its composed resources are ready. The +`composition generate` command adds `function-auto-ready` to the project's +dependencies automatically. + + +The composition doesn't yet create any resources. In the next step you write a +function that turns a `WebApp` into a `Deployment` and a `Service`. + +## Add dependencies + +Your function creates Kubernetes `Deployment` and `Service` resources, so it +needs the schemas for the Kubernetes core APIs. Add the Kubernetes APIs as a +project dependency: + +```shell +crossplane dependency add k8s:v1.35.0 +``` + +The `dependency add` command generates language bindings (schemas) for the +dependency and records it in `crossplane-project.yaml`. The function uses these +schemas for typed access to the `Deployment` and `Service` resources. + +{{}} +You don't need to add `function-auto-ready` as a dependency. The +`composition generate` command added it for you in the previous step. +{{}} + +After this step, your project has two dependencies listed in +`crossplane-project.yaml`: + +```yaml +apiVersion: dev.crossplane.io/v1alpha1 +kind: Project +metadata: + name: example-project-webapp +spec: + dependencies: + - type: xpkg + xpkg: + apiVersion: pkg.crossplane.io/v1 + kind: Function + package: xpkg.crossplane.io/crossplane-contrib/function-auto-ready + version: '>=v0.0.0' + - k8s: + version: v1.35.0 + type: k8s + repository: example.com/my-org/example-project-webapp +``` + +## Write the function + +A composition function contains the logic that turns a `WebApp` into the +resources Crossplane creates. The CLI scaffolds an embedded function in the +language you choose and adds it as a step in your composition's pipeline. + +Pick a language to write your function in. + +{{< tabs >}} + +{{< tab "Templated YAML" >}} +Templated YAML is a good choice if you're used to writing +[Helm charts](https://helm.sh). It doesn't require a separate toolchain. + +Generate a templated YAML function named `compose-webapp` and add it to the +composition's pipeline: + +```shell +crossplane function generate compose-webapp apis/webapps/composition.yaml --language go-templating +``` + +The command scaffolds the function under `functions/compose-webapp/`, where you +write one or more `.gotmpl` template files. Templates render in alphabetical +order by filename. + +The function scaffold includes the file +`functions/compose-webapp/00-prelude.yaml.gotmpl`, which reads the observed XR +into a variable: + +```yaml +# Get the observed composite resource into a variable. This can be used in any +# subsequent templates. +{{ $xr := getCompositeResource . }} +``` + +You can use the `$xr` variable to access fields from the XR in later templates. + +Replace the example contents of +`functions/compose-webapp/01-compose.yaml.gotmpl` with the following: + +```yaml +# code: language=yaml +# yaml-language-server: $schema=../../schemas/json/index.schema.json + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: deployment + name: {{ $xr.metadata.name }} + namespace: {{ $xr.metadata.namespace }} + labels: + app.kubernetes.io/name: {{ $xr.metadata.name }} +spec: + replicas: {{ $xr.spec.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ $xr.metadata.name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ $xr.metadata.name }} + spec: + containers: + - name: {{ $xr.metadata.name }} + image: {{ $xr.spec.image }} + ports: +#{{- range $p := $xr.spec.ports }} + - containerPort: {{ $p }} +#{{- end }} + +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: service + name: {{ $xr.metadata.name }} + namespace: {{ $xr.metadata.namespace }} +spec: + selector: + app.kubernetes.io/name: {{ $xr.metadata.name }} + ports: +#{{- range $p := $xr.spec.ports }} + - protocol: TCP + port: {{ $p }} + targetPort: {{ $p }} +#{{- end }} +``` + +The comment lines at the top configure the +[YAML language server](https://github.com/redhat-developer/yaml-language-server) +to use the JSON Schema files the CLI generated when you added the Kubernetes +dependency. This lets you use editor features like hover documentation and +autocompletion when writing your templates. Note that the template control flow +can confuse the YAML language server. Making them YAML comments causes the +language server to ignore them, but they're still invoked by the template +engine. + +The `composition-resource-name` annotation gives each resource a stable name in +the composition. + +{{}} +Templated YAML functions use +[function-go-templating](https://github.com/crossplane-contrib/function-go-templating) +as their runtime image. You can use any of the features and functions described +in its documentation. +{{}} + +{{< /tab >}} + +{{< tab "Python" >}} +Python is a good choice for functions with dynamic logic. You can use the full +[Python standard library](https://docs.python.org/3/library/index.html) and any +other Python library you need. + +Generate a Python function named `compose-webapp` and add it to the +composition's pipeline: + +```shell +crossplane function generate compose-webapp apis/webapps/composition.yaml --language python +``` + +The command scaffolds the function under `functions/compose-webapp/` and adds a +pipeline step to `apis/webapps/composition.yaml`. + +Replace the contents of `functions/compose-webapp/function/fn.py` with the +following function logic: + +```python +"""A Crossplane composition function.""" + +import grpc +from crossplane.function import logging, response, resource +from crossplane.function.proto.v1 import run_function_pb2 as fnv1 +from crossplane.function.proto.v1 import run_function_pb2_grpc as grpcv1 + +from models.com.example.platform.webapp import v1alpha1 +from models.io.k8s.api.apps import v1 as appsv1 +from models.io.k8s.api.core import v1 as corev1 +from models.io.k8s.apimachinery.pkg.apis.core.meta import v1 as metav1 +from models.io.k8s.apimachinery.pkg.util import intstr + +class FunctionRunner(grpcv1.FunctionRunnerService): + """A FunctionRunner handles gRPC RunFunctionRequests.""" + + def __init__(self): + """Create a new FunctionRunner.""" + self.log = logging.get_logger() + + async def RunFunction( + self, req: fnv1.RunFunctionRequest, _: grpc.aio.ServicerContext + ) -> fnv1.RunFunctionResponse: + """Run the function.""" + log = self.log.bind(tag=req.meta.tag) + log.info("Running function") + + rsp = response.to(req) + + xr = v1alpha1.WebApp(**resource.struct_to_dict(req.observed.composite.resource)) + + assert xr.metadata is not None + assert xr.metadata.name is not None + assert xr.spec.ports is not None + + labels = {"app.kubernetes.io/name": xr.metadata.name} + ports = xr.spec.ports + + dply = appsv1.Deployment( + metadata=metav1.ObjectMeta( + labels=labels, + ), + spec=appsv1.DeploymentSpec( + selector=metav1.LabelSelector( + matchLabels=labels, + ), + replicas=xr.spec.replicas, + template=corev1.PodTemplateSpec( + metadata=metav1.ObjectMeta( + labels=labels, + ), + spec=corev1.PodSpec( + containers=[ + corev1.Container( + name="app", + image=xr.spec.image, + ports=[corev1.ContainerPort(containerPort=p) for p in ports], + ) + ] + ), + ), + ), + ) + + resource.update(rsp.desired.resources["deployment"], dply) + + svc = corev1.Service( + metadata=metav1.ObjectMeta( + labels=labels, + ), + spec=corev1.ServiceSpec( + ports=[corev1.ServicePort(port=p, targetPort=intstr.IntOrString(p)) for p in ports], + selector=labels, + ), + ) + + resource.update(rsp.desired.resources["service"], svc) + + return rsp +``` + +The function reads the observed `WebApp` XR, then builds a `Deployment` and a +`Service` from its `spec`. The `models` packages are the type bindings the CLI +generated when you added the Kubernetes dependency, so you build the resources +with typed Python classes instead of raw dictionaries. +{{< /tab >}} + +{{< tab "Go" >}} +Go is a good choice if you want a statically typed, compiled function and access +to the full Go ecosystem. + +Generate a Go function named `compose-webapp` and add it to the composition's +pipeline: + +```shell +crossplane function generate compose-webapp apis/webapps/composition.yaml --language go +``` + +The command scaffolds the function under `functions/compose-webapp/` and adds a +pipeline step to `apis/webapps/composition.yaml`. + +Replace the contents of `functions/compose-webapp/fn.go` with the following +function logic: + +```go +package main + +import ( + "context" + "encoding/json" + + "dev.crossplane.io/models/com/example/platform/v1alpha1" + appsv1 "dev.crossplane.io/models/io/k8s/apps/v1" + metav1 "dev.crossplane.io/models/io/k8s/core/meta/v1" + corev1 "dev.crossplane.io/models/io/k8s/core/v1" + utilv1 "dev.crossplane.io/models/io/k8s/util/v1" + "github.com/crossplane/crossplane-runtime/v2/pkg/errors" + "github.com/crossplane/function-sdk-go/logging" + fnv1 "github.com/crossplane/function-sdk-go/proto/v1" + "github.com/crossplane/function-sdk-go/request" + "github.com/crossplane/function-sdk-go/resource" + "github.com/crossplane/function-sdk-go/resource/composed" + "github.com/crossplane/function-sdk-go/response" + "k8s.io/utils/ptr" +) + +// Function is your composition function. +type Function struct { + fnv1.UnimplementedFunctionRunnerServiceServer + + log logging.Logger +} + +// RunFunction runs the Function. +func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest) (*fnv1.RunFunctionResponse, error) { + f.log.Info("Running function", "tag", req.GetMeta().GetTag()) + rsp := response.To(req, response.DefaultTTL) + + observedComposite, err := request.GetObservedCompositeResource(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get xr")) + return rsp, nil + } + + var xr v1alpha1.WebApp + if err := convertViaJSON(&xr, observedComposite.Resource); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot convert xr")) + return rsp, nil + } + + // Collect the desired composed resources into this map, then convert them to + // the SDK's types and set them in the response on return. + desiredComposed := make(map[resource.Name]any) + defer func() { + desiredComposedResources, err := request.GetDesiredComposedResources(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get desired resources")) + return + } + + for name, obj := range desiredComposed { + c := composed.New() + if err := convertViaJSON(c, obj); err != nil { + response.Fatal(rsp, errors.Wrapf(err, "cannot convert %s to unstructured", name)) + return + } + desiredComposedResources[name] = &resource.DesiredComposed{Resource: c} + } + + if err := response.SetDesiredComposedResources(rsp, desiredComposedResources); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot set desired resources")) + return + } + }() + + var ( + cports []corev1.ContainerPort + sports []corev1.ServicePort + ) + if xr.Spec.Ports != nil { + cports = make([]corev1.ContainerPort, len(*xr.Spec.Ports)) + sports = make([]corev1.ServicePort, len(*xr.Spec.Ports)) + + for i, p := range *xr.Spec.Ports { + cports[i] = corev1.ContainerPort{ + ContainerPort: ptr.To(int32(p)), + } + sports[i] = corev1.ServicePort{ + Port: ptr.To(int32(p)), + TargetPort: new(utilv1.IntOrString), + } + _ = sports[i].TargetPort.FromInt(int(p)) + } + } + + labels := map[string]string{"app.kubernetes.io/name": *xr.Metadata.Name} + + deployment := &appsv1.Deployment{ + APIVersion: ptr.To(appsv1.DeploymentAPIVersionAppsV1), + Kind: ptr.To(appsv1.DeploymentKindDeployment), + Metadata: &metav1.ObjectMeta{ + Name: xr.Metadata.Name, + Namespace: xr.Metadata.Namespace, + Labels: &labels, + }, + Spec: &appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(*xr.Spec.Replicas)), + Selector: &metav1.LabelSelector{ + MatchLabels: &labels, + }, + Template: &corev1.PodTemplateSpec{ + Metadata: &metav1.ObjectMeta{ + Labels: &labels, + }, + Spec: &corev1.PodSpec{ + Containers: &[]corev1.Container{{ + Name: xr.Metadata.Name, + Image: xr.Spec.Image, + Ports: &cports, + }}, + }, + }, + }, + } + + desiredComposed["deployment"] = deployment + + service := &corev1.Service{ + APIVersion: ptr.To(corev1.ServiceAPIVersionV1), + Kind: ptr.To(corev1.ServiceKindService), + Metadata: &metav1.ObjectMeta{ + Name: xr.Metadata.Name, + Namespace: xr.Metadata.Namespace, + }, + Spec: &corev1.ServiceSpec{ + Selector: &labels, + Ports: &sports, + }, + } + + desiredComposed["service"] = service + + return rsp, nil +} + +func convertViaJSON(to, from any) error { + bs, err := json.Marshal(from) + if err != nil { + return err + } + return json.Unmarshal(bs, to) +} +``` + +The function reads the observed `WebApp` XR, then builds a `Deployment` and a +`Service` from its `spec`. The `dev.crossplane.io/models` packages are the +bindings the CLI generated when you added the Kubernetes dependency, so the +compiler checks the resources you create. +{{< /tab >}} + +{{< tab "KCL" >}} +[KCL](https://kcl-lang.io) is a good choice for functions with dynamic logic. +It's fast and sandboxed. + +Generate a KCL function named `compose-webapp` and add it to the composition's +pipeline: + +```shell +crossplane function generate compose-webapp apis/webapps/composition.yaml --language kcl +``` + +The command scaffolds the function under `functions/compose-webapp/` and adds a +pipeline step to `apis/webapps/composition.yaml`. + +Replace the contents of `functions/compose-webapp/main.k` with the following +function logic: + +```python +import models.io.k8s.api.core.v1 as corev1 +import models.com.example.platform.v1alpha1 as platformv1alpha1 +import models.io.k8s.api.apps.v1 as appsv1 +import models.io.k8s.apimachinery.pkg.apis.meta.v1 as metav1 + +oxr = option("params").oxr # observed composite resource +dcds = option("params").dcds # desired composed resources + +_xr = platformv1alpha1.WebApp{**oxr} +_replicas = int(_xr.spec.replicas) +_ports = [int(p) for p in _xr.spec.ports] + +_labels = {"app.kubernetes.io/name": _xr.metadata.name} +_metadata = lambda name: str -> any { + { + annotations = { "krm.kcl.dev/composition-resource-name" = name } + labels = _labels + } +} + +_items = [ + appsv1.Deployment{ + metadata: _metadata("deployment") + spec: appsv1.DeploymentSpec{ + replicas: _replicas + selector: metav1.LabelSelector{ + matchLabels: _labels + } + template: corev1.PodTemplateSpec{ + metadata: metav1.ObjectMeta{ + labels: _labels + } + spec: corev1.PodSpec{ + containers: [ + corev1.Container{ + name: _xr.metadata.name + image: _xr.spec.image + ports: [corev1.ContainerPort{containerPort: p} for p in _ports] + } + ] + } + } + } + }, + corev1.Service{ + metadata: _metadata("service") + spec: corev1.ServiceSpec{ + selector: _labels + ports: [corev1.ServicePort{ + protocol: "TCP" + port: p + targetPort: p + } for p in _ports] + } + } +] +items = _items +``` + +The function reads the observed `WebApp` XR through `option("params").oxr`, then +builds a `Deployment` and a `Service` from its `spec`. The `models` packages are +the type bindings the CLI generated when you added the Kubernetes dependency. +{{< /tab >}} + +{{}} + +The composition now has two pipeline steps: your `compose-webapp` function, which +creates the `Deployment` and `Service`, followed by `function-auto-ready`: + +```yaml +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: webapps.platform.example.com +spec: + compositeTypeRef: + apiVersion: platform.example.com/v1alpha1 + kind: WebApp + mode: Pipeline + pipeline: + - functionRef: + name: example-project-webappcompose-webapp + step: compose-webapp + - functionRef: + name: crossplane-contrib-function-auto-ready + step: crossplane-contrib-function-auto-ready +``` + +## Add an example + +If you generated your XRD from SimpleSchema, create an example `WebApp` so you +can render and run the project against a realistic input. If you generated your +XRD from an example XR, you already have the example and can skip this step. + +Create `examples/webapp/podinfo.yaml`: + +```yaml +apiVersion: platform.example.com/v1alpha1 +kind: WebApp +metadata: + name: podinfo + namespace: default +spec: + image: docker.io/stefanprodan/podinfo:6.11.0 + replicas: 3 + ports: [9898] +``` + +## Render the composition + +Before running the project, use `crossplane composition render` to preview what +your composition produces. The `render` command runs your composition pipeline +locally and prints the resources the composition would create. + +In a project directory, `render` discovers and builds the composition's +functions automatically, so you only pass the example XR and the composition: + +```shell +crossplane composition render examples/webapp/podinfo.yaml apis/webapps/composition.yaml +``` + +The command prints the rendered `Deployment` and `Service` as well as the +updates Crossplane would make to the `WebApp` XR: + +```yaml {copy-lines="none"} +--- +apiVersion: platform.example.com/v1alpha1 +kind: WebApp +metadata: + name: podinfo + namespace: default +spec: + crossplane: + resourceRefs: + - apiVersion: apps/v1 + kind: Deployment + name: podinfo + - apiVersion: v1 + kind: Service + name: podinfo +status: + conditions: + - lastTransitionTime: "2024-01-01T00:00:00Z" + reason: WatchCircuitClosed + status: "True" + type: Responsive + - lastTransitionTime: "2024-01-01T00:00:00Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-01-01T00:00:00Z" + message: 'Unready resources: deployment, service' + reason: Creating + status: "False" + type: Ready +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + annotations: + crossplane.io/composition-resource-name: deployment + labels: + app.kubernetes.io/name: podinfo + crossplane.io/composite: podinfo + name: podinfo + namespace: default + ownerReferences: + - apiVersion: platform.example.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: WebApp + name: podinfo + uid: 88356664-76da-5ea5-a715-b1bde80fe0a5 +spec: + replicas: 3 + selector: + matchLabels: + app.kubernetes.io/name: podinfo + template: + metadata: + labels: + app.kubernetes.io/name: podinfo + spec: + containers: + - image: docker.io/stefanprodan/podinfo:6.11.0 + name: podinfo + ports: + - containerPort: 9898 +--- +apiVersion: v1 +kind: Service +metadata: + annotations: + crossplane.io/composition-resource-name: service + labels: + crossplane.io/composite: podinfo + name: podinfo + namespace: default + ownerReferences: + - apiVersion: platform.example.com/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: WebApp + name: podinfo + uid: 88356664-76da-5ea5-a715-b1bde80fe0a5 +spec: + ports: + - port: 9898 + protocol: TCP + targetPort: 9898 + selector: + app.kubernetes.io/name: podinfo +``` + +`render` runs the same composition logic as a real Crossplane control plane, so +you can iterate on your function without deploying anything. + +## Run the project + +When you're happy with the rendered output, run the project on a local +development control plane: + +```shell +crossplane project run +``` + +The `run` command: + +* creates a local development control plane in a KIND cluster and a local OCI + registry +* builds your embedded function into a `Function` xpkg +* builds a `Configuration` xpkg containing your project's XRD and Composition, + with a dependency on your embedded function and function-auto-ready +* pushes your project's xpkgs to the local OCI registry +* installs the `Configuration` package on the control plane +* points your `kubectl` context at the development control plane + +The first run takes some time while the CLI creates the cluster and installs +Crossplane. Later runs reuse the cluster and are faster. + +After `run` completes, your `kubectl` context points at the development control +plane. Create the example `WebApp`: + +```shell +kubectl apply -f examples/webapp/podinfo.yaml +``` + +Check that the `WebApp` is ready: + +```shell {copy-lines="1"} +kubectl get webapp podinfo +NAME SYNCED READY COMPOSITION AGE +podinfo True True webapps.platform.example.com 45s +``` + +Check that Crossplane created the `Deployment` and `Service`: + +```shell {copy-lines="1"} +kubectl get deployment,service -l app.kubernetes.io/name=podinfo +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/podinfo 3/3 3 3 45s + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/podinfo ClusterIP 10.96.142.110 9898/TCP 45s +``` + +Crossplane created a `Deployment` with three replicas and a `Service`, just as +your function defined. + +{{}} +Edit the `WebApp`'s `replicas` or `image` and apply it again. Crossplane updates +the `Deployment` to match. +{{}} + +When you're done, delete the `WebApp`: + +```shell +kubectl delete -f examples/webapp/podinfo.yaml +``` + +Crossplane deletes the `Deployment` and `Service` along with the `WebApp`. + +Tear down the development control plane: + +```shell +crossplane project stop +``` + +## Next steps + +You built a complete platform API with the Crossplane CLI: an API, a +composition, and a function, all running on a local control plane. + +To install your API on an existing Crossplane cluster, use +[`crossplane project build`]({{}}) +and +[`crossplane project push`]({{}}) +to package it and push it to an OCI registry. Remember to update the OCI +repository in `crossplane-project` before running `build` and `push`. + +After pushing your project to an OCI registry, you can install it using +[`crossplane xpkg install configuration`](({{}})). + +{{}} +When you install the `Configuration` package built by `crossplane project +build`, the Crossplane package manager will automatically install your project's +dependencies and embedded functions, which are declared as dependencies of the +`Configuration`. To ensure updates work properly when you make changes to your +functions in the future, configure your Crossplane cluster to +[automatically update dependency versions]({{}}). +This alpha feature is disabled by default. +{{}} + +To extend the `WebApp`, add more fields to the SimpleSchema document or example +and regenerate the XRD, then update your function to use them. You can also add +more dependencies with +[`crossplane dependency add`]({{}}) +to compose managed resources from cloud providers alongside the `Deployment` and +`Service`. + +See the [CLI command reference]({{}}) for the +full set of commands. diff --git a/themes/geekboot/layouts/partials/docs-sidebar.html b/themes/geekboot/layouts/partials/docs-sidebar.html index f946e7eac..18888418c 100644 --- a/themes/geekboot/layouts/partials/docs-sidebar.html +++ b/themes/geekboot/layouts/partials/docs-sidebar.html @@ -10,7 +10,18 @@ Contributing Guide), rather than nesting them under a redundant parent. */}} {{ $navRoot := .Site.GetPage "section" .Section }} {{ if eq .Section "cli" }} + {{/* For CLI pages .Section is "cli" (the unversioned parent). The version + root is the ancestor section whose parent is the /cli page (e.g. + cli/master), regardless of how deeply the page is nested. Falls back to + .CurrentSection for the version index page itself, whose only "cli" + ancestor is /cli. */}} + {{ $cliRoot := .Site.GetPage "/cli" }} {{ $navRoot = .CurrentSection }} + {{ range .Ancestors }} + {{ if eq .Parent $cliRoot }} + {{ $navRoot = . }} + {{ end }} + {{ end }} {{ end }} {{ with $navRoot }} {{ $sectionPages := (.Pages | append .) }} diff --git a/utils/vale/styles/Crossplane/crossplane-words.txt b/utils/vale/styles/Crossplane/crossplane-words.txt index 29904446b..3e351fa80 100644 --- a/utils/vale/styles/Crossplane/crossplane-words.txt +++ b/utils/vale/styles/Crossplane/crossplane-words.txt @@ -51,6 +51,7 @@ finalizers FromCompositeFieldPath FromEnvironmentFieldPath fromFieldPath +function-auto-ready function-environment-configs function-extra-resources function-go-templating @@ -91,6 +92,7 @@ RunFunctionRequests RunFunctionResponse RunFunctionResponses Sigstore +SimpleSchema SSL StoreConfig StoreConfigs