Skip to content

Commit

Permalink
Merge pull request #83 from phisco/extra-resources
Browse files Browse the repository at this point in the history
  • Loading branch information
phisco authored Aug 14, 2024
2 parents bbda9de + a17cb79 commit 86a62a8
Show file tree
Hide file tree
Showing 8 changed files with 340 additions and 3 deletions.
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pipeline context using notation like:
- `{{ .desired.composite.resource.status.widgets }}`
- `{{ (index .desired.composed "resource-name").resource.spec.widgets }}`
- `{{ index .context "apiextensions.crossplane.io/environment" }}`
- `{{ index .extraResources "some-bucket-by-name" }}`

This function supports all of Go's [built-in template functions][builtin]. The
above examples use the `index` function to access keys like `resource-name` that
Expand Down Expand Up @@ -106,6 +107,65 @@ $ crossplane beta render xr.yaml composition.yaml functions.yaml
See the [composition functions documentation][docs-functions] to learn more
about `crossplane beta render`.

### ExtraResources

By defining one or more special `ExtraResources`, you can ask Crossplane to
retrieve additional resources from the local cluster and make them available to
your templates. See the [docs][extra-resources] for more information.

```yaml
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ExtraResources
requirements:
some-foo-by-name:
# Resources can be requested either by name
apiVersion: example.com/v1beta1
kind: Foo
matchName: "some-extra-foo"
some-foo-by-labels:
# Or by label.
apiVersion: example.com/v1beta1
kind: Foo
matchLabels:
app: my-app
some-bar-by-a-computed-label:
# But you can also generate them dynamically using the template, for example:
apiVersion: example.com/v1beta1
kind: Bar
matchLabels:
foo: {{ .observed.composite.resource.name }}
```

This will result in Crossplane retrieving the requested resources and making
them available to your templates under the `extraResources` key, with the
following format:

```json5
{
"extraResources": {
"some-foo-by-name": [
// ... the requested bucket if found, empty otherwise ...
],
"some-foo-by-labels": [
// ... the requested buckets if found, empty otherwise ...
],
// ... any other requested extra resources ...
}
}
```

So, you can access the retrieved resources in your templates like this, for
example:

```yaml
{{ someExtraResources := index .extraResources "some-extra-resources-key" }}
{{- range $i, $extraResource := $someExtraResources }}
#
# Do something for each retrieved extraResource
#
{{- end }}
```

## Additional functions

| Name | Description |
Expand Down Expand Up @@ -147,3 +207,4 @@ $ crossplane xpkg build -f package --embed-runtime-image=runtime
[go]: https://go.dev
[docker]: https://www.docker.com
[cli]: https://docs.crossplane.io/latest/cli
[extra-resources]: https://docs.crossplane.io/latest/concepts/composition-functions/#how-composition-functions-work
54 changes: 54 additions & 0 deletions example/extra-resources/composition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example-extra-resources
spec:
compositeTypeRef:
apiVersion: example.crossplane.io/v1beta1
kind: XR
mode: Pipeline
pipeline:
- step: render-templates
functionRef:
name: function-go-templating
input:
apiVersion: gotemplating.fn.crossplane.io/v1beta1
kind: GoTemplate
source: Inline
inline:
template: |
---
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
kind: ExtraResources
requirements:
bucket:
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
matchName: my-awesome-{{ .observed.composite.resource.spec.environment }}-bucket
{{- with .extraResources }}
{{ $someExtraResources := index . "bucket" }}
{{- range $i, $extraResource := $someExtraResources.items }}
---
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: Object
metadata:
annotations:
gotemplating.fn.crossplane.io/composition-resource-name: bucket-configmap-{{ $i }}
spec:
forProvider:
manifest:
apiVersion: v1
kind: Configmap
metadata:
name: {{ $extraResource.resource.metadata.name }}-bucket
data:
bucket: {{ $extraResource.resource.status.atProvider.id }}
providerConfigRef:
name: "kubernetes"
{{- end }}
{{- end }}
---
apiVersion: example.crossplane.io/v1beta1
kind: XR
status:
dummy: cool-status
13 changes: 13 additions & 0 deletions example/extra-resources/extraResources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
apiVersion: s3.aws.upbound.io/v1beta1
kind: Bucket
metadata:
labels:
testing.upbound.io/example-name: bucket-notification
name: my-awesome-dev-bucket
spec:
forProvider:
region: us-west-1
status:
atProvider:
id: random-bucket-id
6 changes: 6 additions & 0 deletions example/extra-resources/functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: pkg.crossplane.io/v1beta1
kind: Function
metadata:
name: function-go-templating
spec:
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.4.1
6 changes: 6 additions & 0 deletions example/extra-resources/xr.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: example.crossplane.io/v1beta1
kind: XR
metadata:
name: example
spec:
environment: dev
43 changes: 43 additions & 0 deletions extraresources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
)

// ExtraResourcesRequirements defines the requirements for extra resources.
type ExtraResourcesRequirements map[string]ExtraResourcesRequirement

// ExtraResourcesRequirement defines a single requirement for extra resources.
// Needed to have camelCase keys instead of the snake_case keys as defined
// through json tags by fnv1beta1.ResourceSelector.
type ExtraResourcesRequirement struct {
// APIVersion of the resource.
APIVersion string `json:"apiVersion"`
// Kind of the resource.
Kind string `json:"kind"`
// MatchLabels defines the labels to match the resource, if defined,
// matchName is ignored.
MatchLabels map[string]string `json:"matchLabels,omitempty"`
// MatchName defines the name to match the resource, if MatchLabels is
// empty.
MatchName string `json:"matchName,omitempty"`
}

// ToResourceSelector converts the ExtraResourcesRequirement to a fnv1beta1.ResourceSelector.
func (e *ExtraResourcesRequirement) ToResourceSelector() *fnv1beta1.ResourceSelector {
out := &fnv1beta1.ResourceSelector{
ApiVersion: e.APIVersion,
Kind: e.Kind,
}
if e.MatchName == "" {
out.Match = &fnv1beta1.ResourceSelector_MatchLabels{
MatchLabels: &fnv1beta1.MatchLabels{Labels: e.MatchLabels},
}
return out
}

out.Match = &fnv1beta1.ResourceSelector_MatchName{
MatchName: e.MatchName,
}
return out
}
25 changes: 23 additions & 2 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
return rsp, nil
}

// Initialize the requirements.
requirements := &fnv1beta1.Requirements{ExtraResources: make(map[string]*fnv1beta1.ResourceSelector)}

// Convert the rendered manifests to a list of desired composed resources.
for _, obj := range objs {
cd := resource.NewDesiredComposed()
Expand Down Expand Up @@ -177,17 +180,31 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
}

// TODO(ezgidemirel): Refactor to reduce cyclomatic complexity.
// Set composite resource's connection details.
if cd.Resource.GetAPIVersion() == metaApiVersion {
switch obj.GetKind() {
case "CompositeConnectionDetails":
// Set composite resource's connection details.
con, _ := cd.Resource.GetStringObject("data")
for k, v := range con {
d, _ := base64.StdEncoding.DecodeString(v) //nolint:errcheck // k8s returns secret values encoded
desiredComposite.ConnectionDetails[k] = d
}
case "ExtraResources":
// Set extra resources requirements.
ers := make(ExtraResourcesRequirements)
if err = cd.Resource.GetValueInto("requirements", &ers); err != nil {
response.Fatal(rsp, errors.Wrap(err, "cannot get extra resources requirements"))
return rsp, nil
}
for k, v := range ers {
if _, found := requirements.ExtraResources[k]; found {
response.Fatal(rsp, errors.Errorf("duplicate extra resource key %q", k))
return rsp, nil
}
requirements.ExtraResources[k] = v.ToResourceSelector()
}
default:
response.Fatal(rsp, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails", obj.GetKind(), metaApiVersion))
response.Fatal(rsp, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails or ExtraResources", obj.GetKind(), metaApiVersion))
return rsp, nil
}

Expand Down Expand Up @@ -234,6 +251,10 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
return rsp, nil
}

if len(requirements.ExtraResources) > 0 {
rsp.Requirements = requirements
}

f.log.Info("Successfully composed desired resources", "source", in.Source, "count", len(objs))

return rsp, nil
Expand Down
Loading

0 comments on commit 86a62a8

Please sign in to comment.