Skip to content

Commit 86a62a8

Browse files
authored
Merge pull request #83 from phisco/extra-resources
2 parents bbda9de + a17cb79 commit 86a62a8

File tree

8 files changed

+340
-3
lines changed

8 files changed

+340
-3
lines changed

README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pipeline context using notation like:
5959
- `{{ .desired.composite.resource.status.widgets }}`
6060
- `{{ (index .desired.composed "resource-name").resource.spec.widgets }}`
6161
- `{{ index .context "apiextensions.crossplane.io/environment" }}`
62+
- `{{ index .extraResources "some-bucket-by-name" }}`
6263

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

110+
### ExtraResources
111+
112+
By defining one or more special `ExtraResources`, you can ask Crossplane to
113+
retrieve additional resources from the local cluster and make them available to
114+
your templates. See the [docs][extra-resources] for more information.
115+
116+
```yaml
117+
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
118+
kind: ExtraResources
119+
requirements:
120+
some-foo-by-name:
121+
# Resources can be requested either by name
122+
apiVersion: example.com/v1beta1
123+
kind: Foo
124+
matchName: "some-extra-foo"
125+
some-foo-by-labels:
126+
# Or by label.
127+
apiVersion: example.com/v1beta1
128+
kind: Foo
129+
matchLabels:
130+
app: my-app
131+
some-bar-by-a-computed-label:
132+
# But you can also generate them dynamically using the template, for example:
133+
apiVersion: example.com/v1beta1
134+
kind: Bar
135+
matchLabels:
136+
foo: {{ .observed.composite.resource.name }}
137+
```
138+
139+
This will result in Crossplane retrieving the requested resources and making
140+
them available to your templates under the `extraResources` key, with the
141+
following format:
142+
143+
```json5
144+
{
145+
"extraResources": {
146+
"some-foo-by-name": [
147+
// ... the requested bucket if found, empty otherwise ...
148+
],
149+
"some-foo-by-labels": [
150+
// ... the requested buckets if found, empty otherwise ...
151+
],
152+
// ... any other requested extra resources ...
153+
}
154+
}
155+
```
156+
157+
So, you can access the retrieved resources in your templates like this, for
158+
example:
159+
160+
```yaml
161+
{{ someExtraResources := index .extraResources "some-extra-resources-key" }}
162+
{{- range $i, $extraResource := $someExtraResources }}
163+
#
164+
# Do something for each retrieved extraResource
165+
#
166+
{{- end }}
167+
```
168+
109169
## Additional functions
110170

111171
| Name | Description |
@@ -147,3 +207,4 @@ $ crossplane xpkg build -f package --embed-runtime-image=runtime
147207
[go]: https://go.dev
148208
[docker]: https://www.docker.com
149209
[cli]: https://docs.crossplane.io/latest/cli
210+
[extra-resources]: https://docs.crossplane.io/latest/concepts/composition-functions/#how-composition-functions-work
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
apiVersion: apiextensions.crossplane.io/v1
2+
kind: Composition
3+
metadata:
4+
name: example-extra-resources
5+
spec:
6+
compositeTypeRef:
7+
apiVersion: example.crossplane.io/v1beta1
8+
kind: XR
9+
mode: Pipeline
10+
pipeline:
11+
- step: render-templates
12+
functionRef:
13+
name: function-go-templating
14+
input:
15+
apiVersion: gotemplating.fn.crossplane.io/v1beta1
16+
kind: GoTemplate
17+
source: Inline
18+
inline:
19+
template: |
20+
---
21+
apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1
22+
kind: ExtraResources
23+
requirements:
24+
bucket:
25+
apiVersion: s3.aws.upbound.io/v1beta1
26+
kind: Bucket
27+
matchName: my-awesome-{{ .observed.composite.resource.spec.environment }}-bucket
28+
{{- with .extraResources }}
29+
{{ $someExtraResources := index . "bucket" }}
30+
{{- range $i, $extraResource := $someExtraResources.items }}
31+
---
32+
apiVersion: kubernetes.crossplane.io/v1alpha1
33+
kind: Object
34+
metadata:
35+
annotations:
36+
gotemplating.fn.crossplane.io/composition-resource-name: bucket-configmap-{{ $i }}
37+
spec:
38+
forProvider:
39+
manifest:
40+
apiVersion: v1
41+
kind: Configmap
42+
metadata:
43+
name: {{ $extraResource.resource.metadata.name }}-bucket
44+
data:
45+
bucket: {{ $extraResource.resource.status.atProvider.id }}
46+
providerConfigRef:
47+
name: "kubernetes"
48+
{{- end }}
49+
{{- end }}
50+
---
51+
apiVersion: example.crossplane.io/v1beta1
52+
kind: XR
53+
status:
54+
dummy: cool-status
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
apiVersion: s3.aws.upbound.io/v1beta1
3+
kind: Bucket
4+
metadata:
5+
labels:
6+
testing.upbound.io/example-name: bucket-notification
7+
name: my-awesome-dev-bucket
8+
spec:
9+
forProvider:
10+
region: us-west-1
11+
status:
12+
atProvider:
13+
id: random-bucket-id
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: pkg.crossplane.io/v1beta1
2+
kind: Function
3+
metadata:
4+
name: function-go-templating
5+
spec:
6+
package: xpkg.upbound.io/crossplane-contrib/function-go-templating:v0.4.1

example/extra-resources/xr.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: example.crossplane.io/v1beta1
2+
kind: XR
3+
metadata:
4+
name: example
5+
spec:
6+
environment: dev

extraresources.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package main
2+
3+
import (
4+
fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1"
5+
)
6+
7+
// ExtraResourcesRequirements defines the requirements for extra resources.
8+
type ExtraResourcesRequirements map[string]ExtraResourcesRequirement
9+
10+
// ExtraResourcesRequirement defines a single requirement for extra resources.
11+
// Needed to have camelCase keys instead of the snake_case keys as defined
12+
// through json tags by fnv1beta1.ResourceSelector.
13+
type ExtraResourcesRequirement struct {
14+
// APIVersion of the resource.
15+
APIVersion string `json:"apiVersion"`
16+
// Kind of the resource.
17+
Kind string `json:"kind"`
18+
// MatchLabels defines the labels to match the resource, if defined,
19+
// matchName is ignored.
20+
MatchLabels map[string]string `json:"matchLabels,omitempty"`
21+
// MatchName defines the name to match the resource, if MatchLabels is
22+
// empty.
23+
MatchName string `json:"matchName,omitempty"`
24+
}
25+
26+
// ToResourceSelector converts the ExtraResourcesRequirement to a fnv1beta1.ResourceSelector.
27+
func (e *ExtraResourcesRequirement) ToResourceSelector() *fnv1beta1.ResourceSelector {
28+
out := &fnv1beta1.ResourceSelector{
29+
ApiVersion: e.APIVersion,
30+
Kind: e.Kind,
31+
}
32+
if e.MatchName == "" {
33+
out.Match = &fnv1beta1.ResourceSelector_MatchLabels{
34+
MatchLabels: &fnv1beta1.MatchLabels{Labels: e.MatchLabels},
35+
}
36+
return out
37+
}
38+
39+
out.Match = &fnv1beta1.ResourceSelector_MatchName{
40+
MatchName: e.MatchName,
41+
}
42+
return out
43+
}

fn.go

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
143143
return rsp, nil
144144
}
145145

146+
// Initialize the requirements.
147+
requirements := &fnv1beta1.Requirements{ExtraResources: make(map[string]*fnv1beta1.ResourceSelector)}
148+
146149
// Convert the rendered manifests to a list of desired composed resources.
147150
for _, obj := range objs {
148151
cd := resource.NewDesiredComposed()
@@ -177,17 +180,31 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequ
177180
}
178181

179182
// TODO(ezgidemirel): Refactor to reduce cyclomatic complexity.
180-
// Set composite resource's connection details.
181183
if cd.Resource.GetAPIVersion() == metaApiVersion {
182184
switch obj.GetKind() {
183185
case "CompositeConnectionDetails":
186+
// Set composite resource's connection details.
184187
con, _ := cd.Resource.GetStringObject("data")
185188
for k, v := range con {
186189
d, _ := base64.StdEncoding.DecodeString(v) //nolint:errcheck // k8s returns secret values encoded
187190
desiredComposite.ConnectionDetails[k] = d
188191
}
192+
case "ExtraResources":
193+
// Set extra resources requirements.
194+
ers := make(ExtraResourcesRequirements)
195+
if err = cd.Resource.GetValueInto("requirements", &ers); err != nil {
196+
response.Fatal(rsp, errors.Wrap(err, "cannot get extra resources requirements"))
197+
return rsp, nil
198+
}
199+
for k, v := range ers {
200+
if _, found := requirements.ExtraResources[k]; found {
201+
response.Fatal(rsp, errors.Errorf("duplicate extra resource key %q", k))
202+
return rsp, nil
203+
}
204+
requirements.ExtraResources[k] = v.ToResourceSelector()
205+
}
189206
default:
190-
response.Fatal(rsp, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails", obj.GetKind(), metaApiVersion))
207+
response.Fatal(rsp, errors.Errorf("invalid kind %q for apiVersion %q - must be CompositeConnectionDetails or ExtraResources", obj.GetKind(), metaApiVersion))
191208
return rsp, nil
192209
}
193210

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

254+
if len(requirements.ExtraResources) > 0 {
255+
rsp.Requirements = requirements
256+
}
257+
237258
f.log.Info("Successfully composed desired resources", "source", in.Source, "count", len(objs))
238259

239260
return rsp, nil

0 commit comments

Comments
 (0)