Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ on:

env:
# Common versions
GO_VERSION: '1.23.8'
GOLANGCI_VERSION: 'v1.62.0'
GO_VERSION: '1.24.1'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go1.24.3 was a security release so if updating to 1.24, it should be 1.24.3 (or .4 in a couple of days)

GOLANGCI_VERSION: 'v2.1.6'
DOCKER_BUILDX_VERSION: 'v0.23.0'

# These environment variables are important to the Crossplane CLI install.sh
Expand All @@ -41,10 +41,10 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
with:
go-version: ${{ env.GO_VERSION }}
cache: false # The golangci-lint action does its own caching.
Expand All @@ -53,18 +53,18 @@ jobs:
run: go mod tidy && git diff --exit-code go.mod go.sum

- name: Lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8
with:
version: ${{ env.GOLANGCI_VERSION }}

unit-test:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Setup Go
uses: actions/setup-go@v5
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5
with:
go-version: ${{ env.GO_VERSION }}

Expand All @@ -85,24 +85,24 @@ jobs:
- arm64
steps:
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3
with:
platforms: all

- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
with:
version: ${{ env.DOCKER_BUILDX_VERSION }}
install: true

- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

# We ask Docker to use GitHub Action's native caching support to speed up
# the build, per https://docs.docker.com/build/cache/backends/gha/.
- name: Build Runtime
id: image
uses: docker/build-push-action@v6
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6
with:
context: .
platforms: linux/${{ matrix.arch }}
Expand All @@ -120,7 +120,7 @@ jobs:
run: ./crossplane xpkg build --package-file=${{ matrix.arch }}.xpkg --package-root=package/ --embed-runtime-image-tarball=runtime-${{ matrix.arch }}.tar

- name: Upload Single-Platform Package
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: package-${{ matrix.arch }}
path: "*.xpkg"
Expand All @@ -136,10 +136,10 @@ jobs:
- build
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4

- name: Download Single-Platform Packages
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
path: .
merge-multiple: true
Expand All @@ -149,7 +149,7 @@ jobs:
run: "curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | sh"

- name: Login to Upbound
uses: docker/login-action@v3
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
if: env.XPKG_ACCESS_ID != ''
with:
registry: xpkg.upbound.io
Expand All @@ -169,7 +169,7 @@ jobs:
run: "./crossplane --verbose xpkg push --package-files $(echo *.xpkg|tr ' ' ,) ${{ env.XPKG }}:${{ env.XPKG_VERSION }}"

- name: Login to GHCR
uses: docker/[email protected]
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down
6 changes: 3 additions & 3 deletions claimconditions.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func UpdateClaimConditions(rsp *fnv1.RunFunctionResponse, conditions ...Targeted
// transformCondition converts a TargetedCondition to be compatible with the Protobuf SDK
func transformCondition(tc TargetedCondition) *fnv1.Condition {
c := &fnv1.Condition{
Type: string(tc.Condition.Type),
Reason: string(tc.Condition.Reason),
Type: string(tc.Type),
Reason: string(tc.Reason),
Target: transformTarget(tc.Target),
}

switch tc.Condition.Status {
switch tc.Status {
case corev1.ConditionTrue:
c.Status = fnv1.Status_STATUS_CONDITION_TRUE
case corev1.ConditionFalse:
Expand Down
27 changes: 25 additions & 2 deletions fn.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"io"
"io/fs"
"os"
"time"

"dario.cat/mergo"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/structpb"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/json"
Expand Down Expand Up @@ -42,27 +44,36 @@ type Function struct {

log logging.Logger
fsys fs.FS
ttl time.Duration
}

const (
annotationKeyCompositionResourceName = "gotemplating.fn.crossplane.io/composition-resource-name"
annotationKeyReady = "gotemplating.fn.crossplane.io/ready"
annotationKeyTtl = "gotemplating.fn.crossplane.io/ttl"

metaApiVersion = "meta.gotemplating.fn.crossplane.io/v1alpha1"
)

// 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())
in := &v1beta1.GoTemplate{}

rsp := response.To(req, response.DefaultTTL)
rsp := response.To(req, f.ttl)

in := &v1beta1.GoTemplate{}
if err := request.GetInput(req, in); err != nil {
response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req))
return rsp, nil
}

if in.TTL != "" {
ttlDuration, err := time.ParseDuration(in.TTL)
if err == nil {
rsp.Meta.Ttl = durationpb.New(ttlDuration)
}
}

tg, err := NewTemplateSourceGetter(f.fsys, in)
if err != nil {
response.Fatal(rsp, errors.Wrap(err, "invalid function input"))
Expand Down Expand Up @@ -147,6 +158,18 @@ func (f *Function) RunFunction(_ context.Context, req *fnv1.RunFunctionRequest)
// Initialize the requirements.
requirements := &fnv1.Requirements{ExtraResources: make(map[string]*fnv1.ResourceSelector)}

// Override the TTL if specified in the observed composite.
if v, found := observedComposite.Resource.GetAnnotations()[annotationKeyTtl]; found {

t, err := time.ParseDuration(v)
if err != nil {
f.log.Debug("Ignoring Ttl annotation, wrong format", v)
}
rsp.Meta.Ttl = durationpb.New(t)
// Remove meta annotation.
meta.RemoveAnnotations(observedComposite.Resource, annotationKeyTtl)
}

// Convert the rendered manifests to a list of desired composed resources.
for _, obj := range objs {
cd := resource.NewDesiredComposed()
Expand Down
81 changes: 81 additions & 0 deletions fn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"embed"
"fmt"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
Expand Down Expand Up @@ -40,6 +41,7 @@ var (
xrWithNestedStatusFoo = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"state":{"foo":"bar"}}}`
xrWithNestedStatusBaz = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"state":{"baz":"qux"}}}`
xrRecursiveTmpl = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"recursive-xr"},"name":"recursive-xr","labels":{"belongsTo":{{.observed.composite.resource.metadata.name|quote}}}},"spec":{"count":2}}`
xrWithTtl = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"annotations":{"gotemplating.fn.crossplane.io/ttl": "5m"},"name":"cool-xr"},"spec":{"count":2}}`

claimConditions = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"TestCondition","status":"False","reason":"InstallFail","message":"failed to install","target":"ClaimAndComposite"},{"type":"ConditionTrue","status":"True","reason":"this condition is true","message":"we are true","target":"Composite"},{"type":"DatabaseReady","status":"True","reason":"Ready","message":"Database is ready"}]}`
claimConditionsReservedKey = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"ClaimConditions","conditions":[{"type":"Ready","status":"False","reason":"InstallFail","message":"I am using a reserved Condition","target":"ClaimAndComposite"}]}`
Expand Down Expand Up @@ -948,13 +950,92 @@ func TestRunFunction(t *testing.T) {
},
},
},
"CustomInputTtl": {
reason: "The Function should use a custom TTL when instructed.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "templates"},
Input: resource.MustStructObject(
&v1beta1.GoTemplate{
Source: v1beta1.InlineSource,
Inline: &v1beta1.TemplateSourceInline{Template: cdTmpl},
TTL: "20m",
}),
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
},
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(20 * time.Minute)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xr),
},
Resources: map[string]*fnv1.Resource{
"cool-cd": {
Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`),
},
},
},
},
},
},
"CustomAnnotationTtl": {
reason: "The Function should use a custom TTL when given in the XR annotation.",
args: args{
req: &fnv1.RunFunctionRequest{
Meta: &fnv1.RequestMeta{Tag: "templates"},
Input: resource.MustStructObject(
&v1beta1.GoTemplate{
Source: v1beta1.InlineSource,
Inline: &v1beta1.TemplateSourceInline{Template: cdTmpl},
TTL: "20m", // this should be overridden by the annotation
}),
Observed: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTtl),
},
},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTtl),
},
},
},
},
want: want{
rsp: &fnv1.RunFunctionResponse{
Meta: &fnv1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(5 * time.Minute)},
Desired: &fnv1.State{
Composite: &fnv1.Resource{
Resource: resource.MustStructJSON(xrWithTtl),
},
Resources: map[string]*fnv1.Resource{
"cool-cd": {
Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`),
},
},
},
},
},
},
}

for name, tc := range cases {
t.Run(name, func(t *testing.T) {
f := &Function{
log: logging.NewNopLogger(),
fsys: testdataFS,
ttl: response.DefaultTTL,
}
rsp, err := f.RunFunction(tc.args.ctx, tc.args.req)

Expand Down
Loading