Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: is it possible to use managed resource result in go-template? #79

Open
maplewf opened this issue Mar 22, 2024 · 9 comments
Open
Labels
bug Something isn't working

Comments

@maplewf
Copy link

maplewf commented Mar 22, 2024

What happened?

I'm pretty new to use go-templating-function for composition. What I want to achieve is to pass result of provisioned managed resource to another managed resource in compostion. Let's say I want to create AWS security group and security group rule associated with group I created like:

{{- $xr := .observed.composite.resource }}
apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroup
metadata:
     name: security-group
     annotations:
         crossplane.io/composition-resource-name: security-group
spec:
    forProvider:
         region: {{ $xr.spec.region }}
         vpcId: {{ $xr.spec.vpcId }}
         name: {{ $xr.spec.name }}-security-group

apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupRule
metadata:
     name: security-group-rule
     annotations:
         crossplane.io/composition-resource-name: security-group-rule
spec:
    forProvider:
         region: {{ $xr.spec.region }}
         fromPort: {{ $xr.spec.fromPort}}
         toPort: {{ $xr.spec.toPort}}
         protocol: TCP
         type: ingress
         securityGroupId: ???

so, I just want to use security group id created by security group in security group rule. If I use patch way, normally I can use ToCompositeFieldPath patch in security group resource to write security group id to XR , then use FromCompositeFieldPath patch in security group rule resource to read security group id from XR.

But I don't know how to achieve that in go-templating function?

How can we reproduce it?

No need

What environment did it happen in?

Function version: v0.4.1

@maplewf maplewf added the bug Something isn't working label Mar 22, 2024
@jtucci
Copy link
Contributor

jtucci commented Mar 25, 2024

Yes, assuming the SecurityGroup Id is returned by the provider as a status you can retrieve the value and use it in the securityGroupRule.

Something like this should work, though you may need to change the path to the ID :)

{{ $securityGroupMR := get .observed.resources "security-group"  }}
{{ $securityGroupID := dig "resource" "status" "atProvider" "id" "" $securityGroupMR }}

apiVersion: ec2.aws.upbound.io/v1beta1
kind: SecurityGroupRule
metadata:
     name: security-group-rule
     annotations:
         crossplane.io/composition-resource-name: security-group-rule
spec:
    forProvider:
         region: {{ $xr.spec.region }}
         fromPort: {{ $xr.spec.fromPort}}
         toPort: {{ $xr.spec.toPort}}
         protocol: TCP
         type: ingress
         securityGroupId: {{ $securityGroupID }}

@maplewf Does this answer your question?

@milkpirate
Copy link

milkpirate commented Apr 28, 2024

@jtucci I have a similar question and it might fit a here (at least a little): How can I set a value in the status of the claim? F.g. copy the SG ID to <claim>.status.atProvider.sg (or similar). I have something like:

{{ $xr := .observed.composite.resource }}
{{ $status := $xr.status.atProvider }}

and then tried:

{{ $status.sg = "1234" }}
{{ $status.sg := "1234" }}
{{ .observed.composite.resource.status.atProvider.sg = "1234" }}
{{ .observed.composite.resource.status.atProvider.sg := "1234" }}

And also variantes of (really not sure about := and =)

{{ $status := dict "sg" "1234" }}
{{ $status := set $status "sg" "1234" }}

also with full path .observed.composite.resource.status.atProvider.sg instead of $status. Or does now everything has to be written to a CompositeConnectionDetails (#85) ?

@jtucci
Copy link
Contributor

jtucci commented Apr 28, 2024

@jtucci I have a similar question and it might fit a here (at least a little): How can I set a value in the status of the claim? F.g. copy the SG ID to <claim>.status.atProvider.sg (or similar). I have something like:

{{ $xr := .observed.composite.resource }}
{{ $status := $xr.status.atProvider }}

and then tried:

{{ $status.sg = "1234" }}
{{ $status.sg := "1234" }}
{{ .observed.composite.resource.status.atProvider.sg = "1234" }}
{{ .observed.composite.resource.status.atProvider.sg := "1234" }}

And also variantes of (really not sure about := and =)

{{ $status := dict "sg" "1234" }}
{{ $status := set $status "sg" "1234" }}

also with full path .observed.composite.resource.status.atProvider.sg instead of $status. Or does now everything has to be written to a CompositeConnectionDetails (#85) ?

You have to set the status on the XR itself which will propagate to the claim.

Here is an example - lets say I have a composition with an XR kind XStorageAccount and a single MR (account.storage.azure.upbound.io/v1beta1) with a status I want propagated to the claim. To set the status in its entirety I would do the following

# composition.yaml
 {{ $accountStatus := get .observed.resources.account.resource "status" | default dict }}

apiVersion: storage.api.example/v1alpha1
kind: XStorageAccount
status:
  account: {{ $accountStatus | toYaml | nindent 4 }}
#definition.yaml
           spec: 
            ... 
            status:
              type: object
              description: A Status represents the observed state
              properties:
                account:
                  type: object
                  description: FreeForm field containing status information from a azure account instance
                  x-kubernetes-preserve-unknown-fields: true

The above example will copy over the account status in its entirety, so if you only care about a single field you could define that single field in the definition status and then just extract and set the desired field in composition.yaml, same general process though :)

@milkpirate
Copy link

@jtucci 😞 Sorry I still dont get it.

My composition.yaml looks like so:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xcfsetup
spec:
  compositeTypeRef:
    apiVersion: cloudflare.private/v1alpha1
    kind: CFSetup

  mode: Pipeline
  pipeline:
  - step: go-template
    functionRef:
      name: function-go-templating
    input:
      apiVersion: gotemplating.fn.crossplane.io/v1beta1
      kind: GoTemplate
      source: Inline
      inline:
        template: |
          {{ $xr := .observed.composite.resource }}
          {{ $name := $xr.metadata.name }}
          {{ $spec := $xr.spec }}
          ---
          ... mrs ...
  - step: automatically-detect-ready-composed-resources
    functionRef:
      name: function-auto-ready

  writeConnectionSecretsToNamespace: crossplane-system

xrd.yaml is similar to:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: cfsetups.cloudflare.private
spec:
  group: cloudflare.private
  names:
    kind: CFSetup
    plural: cfsetups
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
...
          status:
            description: Status of the setup
            type: object
            properties:
              atProvider:
                type: object
                x-kubernetes-preserve-unknown-fields: true

I guess you use source: Filesystem instead of source: Inline like me. How do I solve this while inlining?

@jtucci
Copy link
Contributor

jtucci commented May 10, 2024

@jtucci 😞 Sorry I still dont get it.

My composition.yaml looks like so:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xcfsetup
spec:
  compositeTypeRef:
    apiVersion: cloudflare.private/v1alpha1
    kind: CFSetup

  mode: Pipeline
  pipeline:
  - step: go-template
    functionRef:
      name: function-go-templating
    input:
      apiVersion: gotemplating.fn.crossplane.io/v1beta1
      kind: GoTemplate
      source: Inline
      inline:
        template: |
          {{ $xr := .observed.composite.resource }}
          {{ $name := $xr.metadata.name }}
          {{ $spec := $xr.spec }}
          ---
          ... mrs ...
  - step: automatically-detect-ready-composed-resources
    functionRef:
      name: function-auto-ready

  writeConnectionSecretsToNamespace: crossplane-system

xrd.yaml is similar to:

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: cfsetups.cloudflare.private
spec:
  group: cloudflare.private
  names:
    kind: CFSetup
    plural: cfsetups
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
...
          status:
            description: Status of the setup
            type: object
            properties:
              atProvider:
                type: object
                x-kubernetes-preserve-unknown-fields: true

I guess you use source: Filesystem instead of source: Inline like me. How do I solve this while inlining?

@milkpirate Nope, we are using inline as well.

In the below example lets assume that we want the entire status of an MR named 'example1MR' to be patched back into the XR, as well as a single field 'id' from the status of another MR named 'example2MR'.

You're composition should look something like this

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
  name: xcfsetup
spec:
  compositeTypeRef:
    apiVersion: cloudflare.private/v1alpha1
    kind: XCFSetup
  mode: Pipeline
  pipeline:
  - step: go-template
    functionRef:
      name: function-go-templating
    input:
      apiVersion: gotemplating.fn.crossplane.io/v1beta1
      kind: GoTemplate
      source: Inline
      inline:
        template: |
          # This will contain the entire status object from the example1MR manaaged resource 
          {{ $example1MRStatus := get .observed.resources.example1MR.resource "status" | default dict }}
          
          # This will contain just a single string value 'id', from the example2MR status object
          {{ $example2MRStatus := get .observed.resources.example2MR.resource.status.atProvider "id" | default dict }}

          apiVersion: storage.api.example/v1alpha1
          kind: XCFSetup
          status:
            example1MR: {{ $example1MRStatus | toYaml | nindent 4 }}
            example2MR:
               id: {{ $example2MRStatus }}
          ---
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: cfsetups.cloudflare.private
spec:
  group: cloudflare.private
  names:
    kind: XCFSetup
    plural: xcfsetups
  claimNames:
    kind: CFSetup
    plural: cfsetups    
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
...
          status:
            description: Status of the underlying MR's 
            type: object
            properties:
              example1MR:
                type: object
                x-kubernetes-preserve-unknown-fields: true
              example2MR:
                type: object
                properties:
                  id: 
                    type: string 

@irizzant
Copy link

irizzant commented May 16, 2024

I think this should be reported in the doc and the examples

@milkpirate
Copy link

milkpirate commented May 21, 2024

@jtucci Hey, ran into the next problem: what you described here: #79 (comment) does not work as expected:

status:
  conditions:
  - lastTransitionTime: "2024-05-21T18:03:07Z"
    message: 'cannot compose resources: pipeline step "tunnel" returned a fatal result:
      cannot execute template: template: manifests:70:16: executing "manifests" at
      <dig "resource" "status" "atProvider" "id" "" $tunnelMR>: error calling dig:
      interface conversion: interface {} is string, not map[string]interface {}'
    reason: ReconcileError
    status: "False"
    type: Synced

source looks like so:

          ---
          apiVersion: argo.cloudflare.upbound.io/v1alpha1
          kind: Tunnel
          metadata:
            name: {{ $tunnel.name }}.tunnel.of.{{ $name }}
            annotations:
              gotemplating.fn.crossplane.io/composition-resource-name: tunnel
          spec:
            ...
          ---
          {{ $tunnelMR := get .observed.resources "tunnel" }})
          {{ $tunnelID := dig "resource" "status" "atProvider" "id" "" $tunnelMR }}
          
          apiVersion: some.api
          kind: OtherResource
          ...

The unsuccessful dig prevents the function from endering anything at all.

The same error appears when I want to render the status, like we discussed earlier. Though if I

  1. apply the composition without status
  2. apply the claim
  3. wait until its ready
  4. change the composition to include the status
  5. then the status appears as desired

@jtucci
Copy link
Contributor

jtucci commented May 24, 2024

The observed resource isn't always going to be available, so if you are going to reference later in dig you should set a default value to a dict

{{ $tunnelMR := get .observed.resources "tunnel" | default dict }}

@milkpirate
Copy link

milkpirate commented May 25, 2024

@jtucci ok, I am really sorry for bothering again but I read the thread several times and still dont know why the MR -> MR copy does not work. Here is what I have:

apiVersion: apiextensions.crossplane.io/v1
kind: Composition
...
spec:
  ...
  pipeline:
  - step: tunnel
    ...
    input:
      ...
      inline:
        template: |
          {{ $xr := .observed.composite.resource }}
          {{ $name := $xr.metadata.name }}
          {{ $spec := $xr.spec }}

          {{ with $tunnel := $spec.tunnel }}
          apiVersion: argo.cloudflare.upbound.io/v1alpha1
          kind: Tunnel
          metadata:
            annotations:
              gotemplating.fn.crossplane.io/composition-resource-name: tunnel
          spec:
            ...
          ---
          {{ $tunnelMR := get .observed.resources "tunnel" | default dict }}
          {{ $tunnelToken := dig "resource" "status" "atProvider" "tunnelToken" "NOT_A_TOKEN" $tunnelMR }}
          
          apiVersion: helm.crossplane.io/v1beta1
          kind: Release
          ...
          spec:
            forProvider:
              ...
              values:
                cloudflare:
                  tunnel_token: {{ $tunnelToken }}
          {{ end }}

  - step: automatically-detect-ready-composed-resources
    functionRef:
      name: function-auto-ready

  - step: status
    ...
    input:
      ...
      inline:
        template: |
          {{ $tunnelMR := get .observed.resources "tunnel" | default dict }}
          {{ $tunnelToken := dig "resource" "status" "atProvider" "tunnelToken" "NOT_A_TOKEN" $tunnelMR }}
          ---
          apiVersion: cloudflare.private/v1alpha1
          kind: CFSetup
          status:
            atProvider:
              tunnelToken: {{ $tunnelToken }}

The tunnel token successfully ends up in the status:

...
status:
  atProvider:
    tunnelToken: eyJhIjoi...UdCd2c9In0=
  conditions:
  - lastTransitionTime: "2024-05-25T19:25:50Z"
    reason: ReconcileSuccess
    status: "True"
    type: Synced
...

Thats good 👍🏻
But its not getting copied over to the other MR 😢 just keeping the default values from the dig:

apiVersion: helm.crossplane.io/v1beta1
kind: Release
...
spec:
  forProvider:
    ...
    values:
      cloudflare:
        tunnel_token: NOT_A_TOKEN
status:
  atProvider:
    releaseDescription: Install complete
    revision: 1
    state: deployed
  conditions:
  - lastTransitionTime: "2024-05-25T19:32:04Z"
    reason: Available
    status: "True"
    type: Ready
  - lastTransitionTime: "2024-05-25T19:32:04Z"
    reason: ReconcileSuccess
    status: "True"
    type: Synced
  synced: true

As you said, the observed resource might not always be available, but if it is later, shouldnt the function notice that and re-render, so the release gets updated with the correct value?

I really can you hint again, since there is no difference between status and MR in the token retrieval. Whats missing? Whats wrong?

Thanks again, you're helping a lot! ❤️

EDIT:
Ok, kinda interesting... I changed the following:

          {{ $xr := .observed.composite.resource }}
+         {{ $mrs := .observed.resources | default dict }}
          {{ $name := $xr.metadata.name }}
          {{ $spec := $xr.spec }}
...
          ---
-         {{ $tunnelMR := get .observed.resources "tunnel" | default dict }}
+         {{ $tunnelMR := get $mrs "tunnel" | default dict }}
          {{ $tunnelToken := dig "resource" "status" "atProvider" "tunnelToken" "NOT_A_TOKEN" $tunnelMR }}

in the tunnel step and now it works. Could you explain why?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants