diff --git a/README.md b/README.md index 7a99114..8ae4653 100644 --- a/README.md +++ b/README.md @@ -1,279 +1,55 @@ -# function-template-go +# function-go-templating -A [Crossplane] Composition Function template, for Go. +A [Crossplane] Composition Function for Golang Templating. ## What is this? -This is a template for a [Composition Function][function-design]. +This is a composition function which allows users to render Crossplane resources +using Go templating capabilities. With this function, users can use features like +conditionals, loops, and they can use values in the environment configs or other +resource fields to render Crossplane resources. -Composition Functions let you extend Crossplane with new ways to 'do -Composition' - i.e. new ways to produce composed resources given a claim or XR. -You use Composition Functions instead of the `resources` array of templates. +Currently, users can provider inputs in two different ways: inline and file system. -This template creates a beta-style Function. Functions created from this -template won't work with Crossplane v1.13 or earlier - it targets the -[implementation of Functions][function-pr] coming with Crossplane v1.14 in late -October. - -Keep in mind what is shown here is __far from the final developer experience__ -we want for Functions! This is the very first iteration - we have to start -somewhere. We want your feedback - what do you want to see from the developer -experience? Please [raise a Crossplane issue][new-crossplane-issue] with ideas. - -Here's an example of a Composition that uses a Composition Function. +Here's an example of a Composition that uses a Composition Function with inline input. ```yaml apiVersion: apiextensions.crossplane.io/v1 kind: Composition metadata: - name: test-crossplane + name: xusers.aws.platformref.upbound.io spec: + writeConnectionSecretsToNamespace: crossplane-system compositeTypeRef: - apiVersion: database.example.com/v1alpha1 - kind: NoSQL + apiVersion: aws.platformref.upbound.io/v1alpha1 + kind: XUser mode: Pipeline pipeline: - - step: run-example-function - functionRef: - name: function-example - input: - apiVersion: template.fn.crossplane.io/v1beta1 - kind: Input - # Add any input fields here! + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplate.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: | + {{- range $i := until ( .observed.composite.resource.spec.count | int ) }} + --- + apiVersion: iam.aws.upbound.io/v1beta1 + kind: User + metadata: + name: test-user-{{ $i }} + labels: + testing.upbound.io/example-name: test-user-{{ $i }} + {{ if eq $.observed.resources nil }} + dummy: {{ randomChoice "foo" "bar" "baz" }} + {{ else }} + dummy: {{ ( index $.observed.resources ( print "test-user-" $i ) ).resource.metadata.labels.dummy }} + {{ end }} + {{-end}} ``` Notice that it has a `pipeline` (of Composition Functions) instead of an array of `resources`. -## Developing a Function - -This template doesn't use the typical Crossplane build submodule and Makefile, -since we'd like Functions to have a less heavyweight developer experience. -It mostly relies on regular old Go tools: - -```shell -# Run code generation - see input/generate.go -$ go generate ./... - -# Run tests -$ go test -cover ./... -? github.com/crossplane/function-template-go/input/v1beta1 [no test files] -ok github.com/crossplane/function-template-go 0.006s coverage: 25.8% of statements - -# Lint the code -$ docker run --rm -v $(pwd):/app -v ~/.cache/golangci-lint/v1.54.2:/root/.cache -w /app golangci/golangci-lint:v1.54.2 golangci-lint run - -# Build a Docker image - see Dockerfile -$ docker build . -``` - -This Function can be pushed to any Docker registry. To push to xpkg.upbound.io -use `docker push` and `docker-credential-up` from -https://github.com/upbound/up/. - -To turn this template into a working Function, the process is: - -1. Replace `function-template-go` with your Function's name in - `package/crossplane.yaml`, `go.mod`, and any Go imports -1. Update `input/v1beta1/input.go` to reflect your desired input -1. Run `go generate ./...` -1. Add your Function logic to `RunFunction` in `fn.go` -1. Add tests for your Function logic in `fn_test.go` -1. Update this file, `README.md`, to be about your Function! - -## Testing a Function - -You can try your function out locally using Crossplane CLI's [`render`] command. With `render` -you can run a Function pipeline on your laptop. - -First you'll need to create a `functions.yaml` file. This tells `render` what -Functions to run, and how. In this case we want to run the Function you're -developing in 'Development mode'. That pretty much means you'll run the Function -manually and tell `render` where to find it. - -```yaml ---- -apiVersion: pkg.crossplane.io/v1beta1 -kind: Function -metadata: - name: function-test # Use your Function's name! - annotations: - # render will try to talk to your Function at localhost:9443 - render.crossplane.io/runtime: Development - render.crossplane.io/runtime-development-target: localhost:9443 -``` - -Next, run your Function locally: - -```shell -# Run your Function in insecure mode -go run . --insecure --debug -``` - -Once your Function is running, in another window you can use the `render` command. - -```shell -# Install Crossplane CLI -$ curl -sL https://raw.githubusercontent.com/crossplane/crossplane/master/install.sh | XP_CHANNEL=stable sh - -# Run it! -$ crossplane beta render examples/xr.yaml examples/composition.yaml examples/functions.yaml ---- -apiVersion: nopexample.org/v1 -kind: XBucket -metadata: - name: test-xrender -status: - bucketRegion: us-east-2 ---- -apiVersion: s3.aws.upbound.io/v1beta1 -kind: Bucket -metadata: - annotations: - crossplane.io/composition-resource-name: my-bucket - generateName: test-xrender- - labels: - crossplane.io/composite: test-xrender - ownerReferences: - - apiVersion: nopexample.org/v1 - blockOwnerDeletion: true - controller: true - kind: XBucket - name: test-xrender - uid: "" -spec: - forProvider: - region: us-east-2 -``` - -You can see an example Composition above. - -## Pushing a Function - -Once you feel your Function is ready, use `docker build`, `docker tag`, and -`docker push`to push it. Remember to use `docker-credential-up` (see above) if -you want to push to `xpkg.upbound.io`! - -## Tips and Tricks for Writing Functions - -In no particular order, here's some things to keep in mind when writing a -Function. - -### Take a look at function-sdk-go - -* https://github.com/crossplane/function-sdk-go -* https://pkg.go.dev/github.com/crossplane/function-sdk-go - -`function-sdk-go` is an MVP SDK for building Functions. Its API is early, and -will almost certainly change! This template uses it. We hope it will help make -writing Functions in Go easier. Eventually we intend to have SDKs for other -languages to (such as Python or TypeScript). - -### Understand RunFunctionRequest and RunFunctionResponse - -Behind the scenes, Crossplane will make a gRPC call to run your Function. It -sends a `RunFunctionRequest`, and expects a `RunFunctionResponse`. You can see -[the schema of these types][proto-schema] in `function-sdk-go.` Unlike the -`function-sdk-go` _Go_ API, we think these types are pretty stable and don't -expect to make big changes in future. - -Crossplane sends three important things in a `RunFunctionRequest`: - -1. The observed state of the XR, and any existing composed resources. -1. The desired state of the XR, and any existing composed resources. -1. The input to your Function (if any), as specified in the Composition. - -The `RunFunctionResponse` your Function returns can include two important -things: - - * The desired state, as created or mutated by your Function. - * An array of results. Crossplane emits these as events, except for Fatal - results which will immediately stop the pipeline and cause Crossplane to - return an error. - - ### Always pass through desired state - -Keep in mind that Functions are run in a pipeline - they're run in the order -specified in the `pipeline` array of the Composition. Each Function is passed -any desired state accumulated by previous Functions. This means: - -* If your Function is the first or only Function in the pipeline, the - `RunFunctionRequest` will contain no desired state. -* If your Function is not the first Function in the pipeline, the - `RunFunctionRequest` will contain whatever desired state previous Functions - produced. __It's important that your Function pass this state through - unmodified, _unless_ it has opinions about it (i.e. it wants to intentionally - undo or change desired state produced by a previous Function).__ - -### Always return your desired state - -Let's say your Function wants to create a composed resource like this: - -```yaml -apiVersion: example.crossplane.io/v1 -kind: CoolResource -spec: - coolness: 9001 - resourcefulness: 42 -``` - -It's important that your Function return a composed resource just like this -every time it's run. If your Function doesn't return the composed resource at -all, Crossplane will assume it no longer desires it and it will be deleted. The -same if your Function doesn't return a spec field - say `coolness` - Crossplane -will assume it should try to delete this field. - -### Only update XR status, and don't update composed resource status - -Composition Functions can only update the status of the XR. If you include spec -or metadata for the XR in your desired state, it will be ignored. - -Composed resources are the opposite. Composition Functions can only update the -spec and metadata of a composed resource. If you include composed resource -status in your desired state, it will be ignored. - -### Remember to tell Crossplane when your composed resources are ready - -Crossplane considers an XR to be ready when all composed resources are ready. -Remember to set the `ready` field for each composed resource in your desired -state to let Crossplane know whether they're ready. - -### Input is optional - -Your Function can take input (from the Composition), but doing so is optional. -If you don't need it, you can just delete the `input` directory. Make sure to -delete the corresponding generated CRD under `package/` too. - -### Don't worry about 'Composition machinery' - -Your Function doesn't need to worry about Composition machinery. Crossplane will -take care of the following: - -* Generating a unique name for all composed resources. You can omit - `metadata.name` when you return a desired composed resource. -* Tracking which desired composed resources correspond to which existing, - observed composed resource (using the `crossplane.io/composed-resource-name` - annotation). -* Managing the `spec.resourceRefs` of the XR. - -### Debugging your Function - -This template plumbs a logger up to your Function. Any logs you emit will show -up in the Function's pod logs. Look for the Function pod in `crossplane-system`. - -You can also use `response.Normal` and `response.Warning` to return results. -Crossplane will emit these results as Kubernetes events, associated with your -XR. Be careful with this! You don't want to emit too many events - try to only -emit events when something changes, not every time your Function is called. - -[`grpcurl`][grpcurl] is another handy tool for debugging your Function. With it, -you can `docker run` your Function locally, and send it a `RunFunctionRequest` -in JSON form. - -[Crossplane]: https://crossplane.io -[function-design]: https://github.com/crossplane/crossplane/blob/3996f20/design/design-doc-composition-functions.md -[function-pr]: https://github.com/crossplane/crossplane/pull/4500 -[new-crossplane-issue]: https://github.com/crossplane/crossplane/issues/new?assignees=&labels=enhancement&projects=&template=feature_request.md -[install-master-docs]: https://docs.crossplane.io/v1.13/software/install/#install-pre-release-crossplane-versions -[proto-schema]: https://github.com/crossplane/function-sdk-go/blob/main/proto/v1beta1/run_function.proto -[grpcurl]: https://github.com/fullstorydev/grpcurl \ No newline at end of file +[Crossplane]: https://crossplane.io \ No newline at end of file diff --git a/examples/claim.yaml b/examples/claim.yaml new file mode 100644 index 0000000..e965903 --- /dev/null +++ b/examples/claim.yaml @@ -0,0 +1,8 @@ +apiVersion: aws.platformref.upbound.io/v1alpha1 +kind: MyUser +metadata: + name: test-user + namespace: crossplane-system +spec: + id: test-user + count: 2 \ No newline at end of file diff --git a/examples/composition-fs.yaml b/examples/composition-fs.yaml new file mode 100644 index 0000000..badf8d2 --- /dev/null +++ b/examples/composition-fs.yaml @@ -0,0 +1,26 @@ +# This composition expects to find the templates in filesystem. +# You can create a configmap and mount it using DeploymentRuntimeConfig. +# Please check the examples/functions.yaml file for an example. +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xusers.aws.platformref.upbound.io +spec: + writeConnectionSecretsToNamespace: crossplane-system + compositeTypeRef: + apiVersion: aws.platformref.upbound.io/v1alpha1 + kind: XUser + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplate.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: FileSystem + fileSystem: + dirPath: /templates + - step: ready + functionRef: + name: function-auto-ready diff --git a/examples/composition.yaml b/examples/composition.yaml new file mode 100644 index 0000000..b403036 --- /dev/null +++ b/examples/composition.yaml @@ -0,0 +1,79 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: Composition +metadata: + name: xusers.aws.platformref.upbound.io +spec: + writeConnectionSecretsToNamespace: crossplane-system + compositeTypeRef: + apiVersion: aws.platformref.upbound.io/v1alpha1 + kind: XUser + mode: Pipeline + pipeline: + - step: render-templates + functionRef: + name: function-go-templating + input: + apiVersion: gotemplate.fn.crossplane.io/v1beta1 + kind: GoTemplate + source: Inline + inline: + template: | + {{- range $i := until ( .observed.composite.resource.spec.count | int ) }} + --- + apiVersion: iam.aws.upbound.io/v1beta1 + kind: User + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: test-user-{{ $i }} + name: test-user-{{ $i }} + labels: + testing.upbound.io/example-name: test-user-{{ $i }} + {{ if eq $.observed.resources nil }} + dummy: {{ randomChoice "foo" "bar" "baz" }} + {{ else }} + dummy: {{ ( index $.observed.resources ( print "test-user-" $i ) ).resource.metadata.labels.dummy }} + {{ end }} + spec: + forProvider: {} + --- + apiVersion: iam.aws.upbound.io/v1beta1 + kind: AccessKey + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: sample-access-key-{{ $i }} + name: sample-access-key-{{ $i }} + spec: + forProvider: + userSelector: + matchLabels: + testing.upbound.io/example-name: test-user-{{ $i }} + writeConnectionSecretToRef: + name: sample-access-key-secret-{{ $i }} + namespace: crossplane-system + {{- end }} + --- + apiVersion: meta.gotemplating.fn.crossplane.io/v1alpha1 + kind: CompositeConnectionDetails + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: connection-details + name: connection-details + {{ if eq $.observed.resources nil }} + data: {} + {{ else }} + data: + username: {{ ( index $.observed.resources "sample-access-key-0" ).connectionDetails.username }} + password: {{ ( index $.observed.resources "sample-access-key-0" ).connectionDetails.password }} + url: {{ "http://www.example.com" | b64enc }} + {{ end }} + --- + apiVersion: aws.platformref.upbound.io/v1alpha1 + kind: XUser + metadata: + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: {{ .observed.composite.resource.metadata.name }} + status: + dummy: cool-status + - step: ready + functionRef: + name: function-auto-ready diff --git a/examples/functions.yaml b/examples/functions.yaml new file mode 100644 index 0000000..af828ba --- /dev/null +++ b/examples/functions.yaml @@ -0,0 +1,42 @@ +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-go-templating +spec: + package: xpkg.upbound.io/ezgi/function-go-templating:v0.0.1 # TODO(ezgidemirel): Update package + packagePullPolicy: Always + # Please uncomment the following lines if you want to mount the templates from a config map with the following DeploymentRuntimeConfig resource. +# runtimeConfigRef: +# name: mount-templates + +--- + +apiVersion: pkg.crossplane.io/v1beta1 +kind: DeploymentRuntimeConfig +metadata: + name: mount-templates +spec: + deploymentTemplate: + spec: + selector: {} + template: + spec: + containers: + - name: package-runtime + volumeMounts: + - mountPath: /templates + name: templates + readOnly: true + volumes: + - name: templates + configMap: + name: templates + +--- + +apiVersion: pkg.crossplane.io/v1beta1 +kind: Function +metadata: + name: function-auto-ready +spec: + package: xpkg.upbound.io/crossplane-contrib/function-auto-ready:v0.1.0 \ No newline at end of file diff --git a/examples/xrd.yaml b/examples/xrd.yaml new file mode 100644 index 0000000..b816611 --- /dev/null +++ b/examples/xrd.yaml @@ -0,0 +1,37 @@ +apiVersion: apiextensions.crossplane.io/v1 +kind: CompositeResourceDefinition +metadata: + name: xusers.aws.platformref.upbound.io +spec: + group: aws.platformref.upbound.io + names: + kind: XUser + plural: xusers + claimNames: + kind: MyUser + plural: myusers + versions: + - name: v1alpha1 + served: true + referenceable: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + id: + type: string + description: ID of this user that other objects will use to refer to it. + count: + type: integer + required: + - id + - count + status: + type: object + properties: + dummy: + type: string + description: Dummy status field. diff --git a/fn.go b/fn.go index bc71937..b3b5c58 100644 --- a/fn.go +++ b/fn.go @@ -1,16 +1,30 @@ package main import ( + "bytes" "context" + "dario.cat/mergo" + "encoding/base64" + "fmt" + "io" + + "google.golang.org/protobuf/encoding/protojson" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/yaml" "github.com/crossplane/crossplane-runtime/pkg/errors" + "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/logging" + "github.com/crossplane/crossplane-runtime/pkg/meta" fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/request" + "github.com/crossplane/function-sdk-go/resource" + fn "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/response" - "github.com/crossplane/function-template-go/input/v1beta1" + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" ) // Function returns whatever response you ask it to. @@ -20,34 +34,207 @@ type Function struct { log logging.Logger } +const ( + errFmtInvalidFunction = "invalid function input: %s" + errFmtInvalidReadyValue = "%s is invalid, ready annotation must be True, Unspecified, or False" + errFmtInvalidMetaType = "invalid meta kind %s" + + errCannotGet = "cannot get the function input" + errCannotParse = "cannot parse the provided templates" +) + +const ( + annotationKeyCompositionResourceName = "gotemplating.fn.crossplane.io/composition-resource-name" + annotationKeyReady = "gotemplating.fn.crossplane.io/ready" + + metaApiVersion = "meta.gotemplating.fn.crossplane.io/v1alpha1" +) + // RunFunction runs the Function. func (f *Function) RunFunction(_ context.Context, req *fnv1beta1.RunFunctionRequest) (*fnv1beta1.RunFunctionResponse, error) { f.log.Info("Running Function", "tag", req.GetMeta().GetTag()) - // This creates a new response to the supplied request. Note that Functions - // are run in a pipeline! Other Functions may have run before this one. If - // they did, response.To will copy their desired state from req to rsp. Be - // sure to pass through any desired state your Function is not concerned - // with unmodified. rsp := response.To(req, response.DefaultTTL) - // Input is supplied by the author of a Composition when they choose to run - // your Function. Input is arbitrary, except that it must be a KRM-like - // object. Supporting input is also optional - if you don't need to you can - // delete this, and delete the input directory. in := &v1beta1.Input{} if err := request.GetInput(req, in); err != nil { response.Fatal(rsp, errors.Wrapf(err, "cannot get Function input from %T", req)) return rsp, nil } - // TODO: Add your Function logic here! - // - // Take a look at function-sdk-go for some utilities for working with req - // and rsp - https://pkg.go.dev/github.com/crossplane/function-sdk-go - // - // Also, be sure to look at the tips in README.md - response.Normalf(rsp, "I was run with input %q", in.Example) + tg, err := NewTemplateSourceGetter(in) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(errFmtInvalidFunction, errCannotGet))) + return rsp, nil + } + + tmpl, err := GetNewTemplateWithFunctionMaps().Parse(tg.GetTemplates()) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, fmt.Sprintf(errFmtInvalidFunction, errCannotParse))) + return rsp, nil + } + + reqMap, err := convertToMap(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot convert request to map")) + return rsp, nil + } + + f.log.Debug("constructed request map", "request", reqMap) + + buf := &bytes.Buffer{} + + if err := tmpl.Execute(buf, reqMap); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot execute template")) + return rsp, nil + } + + f.log.Debug("rendered manifests", "manifests", buf.String()) + + // Parse the rendered manifests. + var objs []*unstructured.Unstructured + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(buf.String()), 1024) + for { + u := &unstructured.Unstructured{} + if err := decoder.Decode(&u); err != nil { + if err == io.EOF { + break + } + response.Fatal(rsp, errors.Wrap(err, "cannot decode manifest")) + return rsp, nil + } + if u != nil { + objs = append(objs, u) + } + } + + // Get the desired composite resource from the request. + desiredComposite, err := request.GetDesiredCompositeResource(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get desired composite resource")) + return rsp, nil + } + + // Get the observed composite resource from the request. + observedComposite, err := request.GetObservedCompositeResource(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get observed composite resource")) + return rsp, nil + } + + // Get the desired composed resources from the request. + desiredComposed, err := request.GetDesiredComposedResources(req) + if err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot get desired composed resources")) + return rsp, nil + } + + // Convert the rendered manifests to a list of desired composed resources. + for _, obj := range objs { + cd := resource.NewDesiredComposed() + cd.Resource.Unstructured = *obj.DeepCopy() + + // TODO(ezgidemirel): Refactor to reduce cyclomatic complexity. + // Update only the status of the desired composite resource. + if cd.Resource.GetAPIVersion() == observedComposite.Resource.GetAPIVersion() && cd.Resource.GetKind() == observedComposite.Resource.GetKind() { + dst := make(map[string]any) + if err := desiredComposite.Resource.GetValueInto("status", &dst); err != nil && !fieldpath.IsNotFound(err) { + response.Fatal(rsp, errors.Wrap(err, "cannot get desired composite status")) + return rsp, nil + } + + src := make(map[string]any) + if err := cd.Resource.GetValueInto("status", &src); err != nil && !fieldpath.IsNotFound(err) { + response.Fatal(rsp, errors.Wrap(err, "cannot get templated composite status")) + return rsp, nil + } + + if err := mergo.Merge(&dst, src, mergo.WithOverride); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot merge desired composite status")) + return rsp, nil + } + + if err := fieldpath.Pave(desiredComposite.Resource.Object).SetValue("status", dst); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot set desired composite status")) + return rsp, nil + } + + continue + } + + // TODO(ezgidemirel): Refactor to reduce cyclomatic complexity. + // Set composite resource's connection details. + if cd.Resource.GetAPIVersion() == metaApiVersion { + switch obj.GetKind() { + case "CompositeConnectionDetails": + 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 + } + default: + response.Fatal(rsp, fmt.Errorf(errFmtInvalidMetaType, obj.GetKind())) + return rsp, nil + } + + continue + } + + // TODO(ezgidemirel): Refactor to reduce cyclomatic complexity. + // Set ready state. + if v, found := cd.Resource.GetAnnotations()[annotationKeyReady]; found { + if v != string(resource.ReadyTrue) && v != string(resource.ReadyUnspecified) && v != string(resource.ReadyFalse) { + response.Fatal(rsp, fmt.Errorf(fmt.Sprintf(errFmtInvalidFunction, errFmtInvalidReadyValue), v)) + return rsp, nil + } + + cd.Ready = fn.Ready(v) + + // Remove meta annotation. + meta.RemoveAnnotations(cd.Resource, annotationKeyReady) + } + + // Remove resource name annotation. + meta.RemoveAnnotations(cd.Resource, annotationKeyCompositionResourceName) + + // Add resource to the desired composed resources map. + name, found := obj.GetAnnotations()[annotationKeyCompositionResourceName] + if !found { + response.Fatal(rsp, errors.Errorf("cannot get composition resource name of %s", obj.GetName())) + return rsp, nil + } + + desiredComposed[resource.Name(name)] = cd + } + + f.log.Debug("desired composite resource", "desiredComposite:", desiredComposite) + f.log.Debug("constructed desired composed resources", "desiredComposed:", desiredComposed) + + if err := response.SetDesiredComposedResources(rsp, desiredComposed); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot desired composed resources")) + return rsp, nil + } + + if err := response.SetDesiredCompositeResource(rsp, desiredComposite); err != nil { + response.Fatal(rsp, errors.Wrap(err, "cannot set desired composite resource")) + return rsp, nil + } + + response.Normalf(rsp, "Successful run with %q source", in.Source) return rsp, nil } + +func convertToMap(req *fnv1beta1.RunFunctionRequest) (map[string]any, error) { + jReq, err := protojson.Marshal(req) + if err != nil { + return nil, errors.Wrap(err, "cannot marshal request from proto to json") + } + + var mReq map[string]any + if err := json.Unmarshal(jReq, &mReq); err != nil { + return nil, errors.Wrap(err, "cannot unmarshal json to map[string]any") + } + + return mReq, nil +} diff --git a/fn_test.go b/fn_test.go index 73977d2..1188819 100644 --- a/fn_test.go +++ b/fn_test.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "testing" "github.com/google/go-cmp/cmp" @@ -14,10 +15,32 @@ import ( fnv1beta1 "github.com/crossplane/function-sdk-go/proto/v1beta1" "github.com/crossplane/function-sdk-go/resource" "github.com/crossplane/function-sdk-go/response" + + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" ) -func TestRunFunction(t *testing.T) { +var ( + cd = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd"},"name":"cool-cd"}}` + cdTmpl = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd"},"name":"cool-cd","labels":{"belongsTo":{{.observed.composite.resource.metadata.name|quote}}}}}` + cdWrongTmpl = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"name":"cool-cd","labels":{"belongsTo":{{.invalid-key}}}}}` + cdMissingKind = `{"apiVersion":"example.org/v1"}` + cdMissingResourceName = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"name":"cool-cd"}}` + cdWithReadyWrong = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd","gotemplating.fn.crossplane.io/ready":"wrongValue"},"name":"cool-cd"}}` + cdWithReadyTrue = `{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{"gotemplating.fn.crossplane.io/composition-resource-name":"cool-cd","gotemplating.fn.crossplane.io/ready":"True"},"name":"cool-cd"}}` + + metaResourceInvalid = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"InvalidMeta"}` + metaResourceConDet = `{"apiVersion":"meta.gotemplating.fn.crossplane.io/v1alpha1","kind":"CompositeConnectionDetails","data":{"key":"dmFsdWU="}}` // encoded string "value" + xr = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2}}` + xrWithStatus = `{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"ready":"true"}}` + 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"}}}` + + path = "testdata/templates" + wrongPath = "testdata/wrong" +) + +func TestRunFunction(t *testing.T) { type args struct { ctx context.Context req *fnv1beta1.RunFunctionRequest @@ -32,25 +55,528 @@ func TestRunFunction(t *testing.T) { args args want want }{ - "ResponseIsReturned": { - reason: "The Function should return a fatal result if no input was specified", + "WrongInputSourceType": { + reason: "The Function should return a fatal result if the cd source type is wrong", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: "wrong", + }), + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "invalid function input: cannot get the function input: invalid input source: wrong", + }, + }, + }, + }, + }, + "NoInput": { + reason: "The Function should return a fatal result if no cd was specified", + args: args{ + req: &fnv1beta1.RunFunctionRequest{}, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "invalid function input: cannot get the function input: invalid input source: ", + }, + }, + }, + }, + }, + "NoResourceNameAnnotation": { + reason: "The Function should return a fatal result if the cd does not have a composition-resource-name annotation", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdMissingResourceName}, + }), + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "cannot get composition resource name of cool-cd", + }, + }, + }, + }, + }, + "CannotDecodeManifest": { + reason: "The Function should return a fatal result if the manifest cannot be decoded", args: args{ req: &fnv1beta1.RunFunctionRequest{ - Meta: &fnv1beta1.RequestMeta{Tag: "hello"}, - Input: resource.MustStructJSON(`{ - "apiVersion": "dummy.fn.crossplane.io", - "kind": "Input", - "example": "Hello, world!" - }`), + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdMissingKind}, + }), }, }, want: want{ rsp: &fnv1beta1.RunFunctionResponse{ - Meta: &fnv1beta1.ResponseMeta{Tag: "hello", Ttl: durationpb.New(response.DefaultTTL)}, + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf("cannot decode manifest: Object 'Kind' is missing in '%s'", cdMissingKind), + }, + }, + }, + }, + }, + "CannotParseTemplate": { + reason: "The Function should return a fatal result if the template cannot be parsed", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdWrongTmpl}, + }, + ), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "invalid function input: cannot parse the provided templates: template: manifests:1: bad character U+002D '-'", + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "ResponseIsReturnedWithNoChange": { + reason: "The Function should return the desired composite resource and cd composed resource without any changes.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "nochange"}, + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cd}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(cd), + }, + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "nochange", Ttl: durationpb.New(response.DefaultTTL)}, Results: []*fnv1beta1.Result{ { Severity: fnv1beta1.Severity_SEVERITY_NORMAL, - Message: "I was run with input \"Hello, world!\"", + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd"}}`), + }, + }, + }, + }, + }, + }, + "ResponseIsReturnedWithTemplating": { + reason: "The Function should return the desired composite resource and the templated composed resources.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "templates"}, + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdTmpl}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`), + }, + }, + }, + }, + }, + }, + "UpdateDesiredCompositeStatus": { + reason: "The Function should update the desired composite resource status.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "status"}, + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: xrWithStatus}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "status", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xrWithStatus), + }, + }, + }, + }, + }, + "UpdateDesiredCompositeNestedStatus": { + reason: "The Function should update the desired composite resource nested status.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "status"}, + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: xrWithNestedStatusBaz}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xrWithNestedStatusFoo), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xrWithNestedStatusFoo), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "status", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"XR","metadata":{"name":"cool-xr"},"spec":{"count":2},"status":{"state":{"foo":"bar","baz":"qux"}}}`), + }, + }, + }, + }, + }, + "ResponseIsReturnedWithTemplatingFS": { + reason: "The Function should return the desired composite resource and the templated composed resources with FileSystem cd.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Meta: &fnv1beta1.RequestMeta{Tag: "templates"}, + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.FileSystemSource, + FileSystem: &v1beta1.InputSourceFileSystem{DirPath: path}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Tag: "templates", Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.FileSystemSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion": "example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd","labels":{"belongsTo":"cool-xr"}}}`), + }, + }, + }, + }, + }, + }, + "CannotReadTemplatesFromFS": { + reason: "The Function should return a fatal result if the templates cannot be read from the filesystem.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.FileSystemSource, + FileSystem: &v1beta1.InputSourceFileSystem{DirPath: wrongPath}, + }, + ), + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: "invalid function input: cannot get the function input: cannot read tmpl from the folder {testdata/wrong}: lstat testdata/wrong: no such file or directory", + }, + }, + }, + }, + }, + "ReadyStatusAnnotationNotValid": { + reason: "The Function should return a fatal result if the ready annotation is not valid.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdWithReadyWrong}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf(errFmtInvalidFunction, fmt.Sprintf(errFmtInvalidReadyValue, "wrongValue")), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "ReadyStatusAnnotation": { + reason: "The Function should return desired composed resource with True ready state.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: cdWithReadyTrue}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(cd), + }, + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + Resources: map[string]*fnv1beta1.Resource{ + "cool-cd": { + Resource: resource.MustStructJSON(`{"apiVersion":"example.org/v1","kind":"CD","metadata":{"annotations":{},"name":"cool-cd"}}`), + Ready: 1, + }, + }, + }, + }, + }, + }, + "InvalidMetaKind": { + reason: "The Function should return a fatal result if the meta kind is invalid.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: metaResourceInvalid}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_FATAL, + Message: fmt.Sprintf(errFmtInvalidMetaType, "InvalidMeta"), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + }, + "CompositeConnectionDetails": { + reason: "The Function should return the desired composite with CompositeConnectionDetails.", + args: args{ + req: &fnv1beta1.RunFunctionRequest{ + Input: resource.MustStructObject( + &v1beta1.Input{ + Source: v1beta1.InlineSource, + Inline: &v1beta1.InputSourceInline{Template: metaResourceConDet}, + }), + Observed: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + }, + }, + }, + }, + want: want{ + rsp: &fnv1beta1.RunFunctionResponse{ + Meta: &fnv1beta1.ResponseMeta{Ttl: durationpb.New(response.DefaultTTL)}, + Results: []*fnv1beta1.Result{ + { + Severity: fnv1beta1.Severity_SEVERITY_NORMAL, + Message: fmt.Sprintf("Successful run with %q source", v1beta1.InlineSource), + }, + }, + Desired: &fnv1beta1.State{ + Composite: &fnv1beta1.Resource{ + Resource: resource.MustStructJSON(xr), + ConnectionDetails: map[string][]byte{"key": []byte("value")}, }, }, }, diff --git a/function_maps.go b/function_maps.go new file mode 100644 index 0000000..1243ef5 --- /dev/null +++ b/function_maps.go @@ -0,0 +1,30 @@ +package main + +import ( + "math/rand" + "text/template" + "time" + + sprig "github.com/Masterminds/sprig/v3" +) + +var funcMaps = []template.FuncMap{ + { + "randomChoice": func(choices ...string) string { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + return choices[r.Intn(len(choices))] + }, + }, +} + +func GetNewTemplateWithFunctionMaps() *template.Template { + tpl := template.New("manifests") + + for _, f := range funcMaps { + tpl.Funcs(f) + } + tpl.Funcs(sprig.FuncMap()) + + return tpl +} diff --git a/go.mod b/go.mod index 079b5f6..a0d842a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,10 @@ -module github.com/crossplane/function-template-go +module github.com/crossplane-contrib/function-go-templating go 1.20 require ( + dario.cat/mergo v1.0.0 + github.com/Masterminds/sprig/v3 v3.2.3 github.com/alecthomas/kong v0.8.1 github.com/crossplane/crossplane-runtime v1.14.0-rc.1 github.com/crossplane/function-sdk-go v0.0.0-20231025230156-142a5c0bc9cc @@ -13,7 +15,8 @@ require ( ) require ( - dario.cat/mergo v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect @@ -29,23 +32,29 @@ require ( github.com/google/gnostic-models v0.6.8 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mitchellh/copystructure v1.0.0 // indirect + github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/shopspring/decimal v1.2.0 // indirect github.com/spf13/afero v1.10.0 // indirect + github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.4 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect diff --git a/go.sum b/go.sum index 0342d05..4bd0c33 100644 --- a/go.sum +++ b/go.sum @@ -40,9 +40,13 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/alecthomas/assert/v2 v2.1.0 h1:tbredtNcQnoSd3QBhQWI7QZ3XHOVkw1Moklp2ojoH/0= -github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s= -github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/kong v0.8.1 h1:acZdn3m4lLRobeh3Zi2S2EpnXTd1mOL6U7xVml+vfkY= github.com/alecthomas/kong v0.8.1/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= github.com/alecthomas/repr v0.1.0 h1:ENn2e1+J3k09gyj2shc0dHr/yjaWSHRlrJ4DPMevDqE= @@ -59,25 +63,13 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/crossplane/crossplane-runtime v1.13.0 h1:EumInUbS8mXV7otwoI3xa0rPczexJOky4XLVlHxxjO0= -github.com/crossplane/crossplane-runtime v1.13.0/go.mod h1:FuKIC8Mg8hE2gIAMyf2wCPkxkFPz+VnMQiYWBq1/p5A= -github.com/crossplane/crossplane-runtime v1.14.0-rc.0.0.20231019152856-a5df125af6f8 h1:AWDPFzOHzOCNgyjMuc/TrE3xUlvzWVj7Wi/WG9IheRA= -github.com/crossplane/crossplane-runtime v1.14.0-rc.0.0.20231019152856-a5df125af6f8/go.mod h1:aOP+5W2wKpvthVs3pFNbVOe1jwrKYbJho0ThGNCVz9o= github.com/crossplane/crossplane-runtime v1.14.0-rc.1 h1:kyW6HY9IbvnKWT0x9CbbErIeE5g7o0akferrVWntSg8= github.com/crossplane/crossplane-runtime v1.14.0-rc.1/go.mod h1:aOP+5W2wKpvthVs3pFNbVOe1jwrKYbJho0ThGNCVz9o= -github.com/crossplane/function-sdk-go v0.0.0-20230929052952-230c7dfcb3b0 h1:CFI/N+D0Mv1q7twYdUEwPc5+wnTHnJjdBVkU+mQgJ8E= -github.com/crossplane/function-sdk-go v0.0.0-20230929052952-230c7dfcb3b0/go.mod h1:KzsTYRhUS+HGzGASlJMxnURTZ1tCnOihjfKSBM3AfKo= -github.com/crossplane/function-sdk-go v0.0.0-20230930011419-ec31b88ab696 h1:WwmZdPz2m+z9TxOZKxEamF84bSaAakEAsIHwd+iOtxI= -github.com/crossplane/function-sdk-go v0.0.0-20230930011419-ec31b88ab696/go.mod h1:KzsTYRhUS+HGzGASlJMxnURTZ1tCnOihjfKSBM3AfKo= -github.com/crossplane/function-sdk-go v0.0.0-20231024211620-66fe852bf9fc h1:6Pso3+n8eTpP2U9e1sf0+4+pP4HfEXIGWwOvM5zX6C0= -github.com/crossplane/function-sdk-go v0.0.0-20231024211620-66fe852bf9fc/go.mod h1:XyD0EfaqIENlXQ9HonceOoKl+3rdQj1QtRAf9A2hziY= github.com/crossplane/function-sdk-go v0.0.0-20231025230156-142a5c0bc9cc h1:joVogW0FRLjh6Ydwb6afc63yb5VTr5SA0lHvzIxy5LU= github.com/crossplane/function-sdk-go v0.0.0-20231025230156-142a5c0bc9cc/go.mod h1:v3yZsfPbDOG+FLC689u9aVMDs58mmh52ZKIfQg7toOk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.10.2 h1:hIovbnmBTLjHXkqEBUz3HGpXZdM7ZrE9fJIZIqlJLqE= -github.com/emicklei/go-restful/v3 v3.10.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -153,7 +145,6 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -173,11 +164,10 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 h1:n6vlPhxsA+BW/XsS5+uqi7GyzaLa5MH7qlSLBZtRdiA= +github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -186,9 +176,13 @@ github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -216,6 +210,10 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -226,7 +224,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -242,10 +239,12 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -253,6 +252,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -267,6 +267,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -288,7 +289,11 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -299,6 +304,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -323,8 +329,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -359,8 +365,8 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -372,8 +378,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU= golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -387,7 +391,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -425,14 +430,15 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -443,8 +449,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -502,14 +507,14 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -573,8 +578,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -593,8 +596,6 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.58.2 h1:SXUpjxeVF3FKrTYQI4f4KvbGD5u2xccdYdurwowix5I= -google.golang.org/grpc v1.58.2/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -621,6 +622,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -633,22 +635,15 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.28.2 h1:9mpl5mOb6vXZvqbQmankOfPIGiudghwCoLl1EYfUZbw= -k8s.io/api v0.28.2/go.mod h1:RVnJBsjU8tcMq7C3iaRSGMeaKt2TWEUXcpIt/90fjEg= k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= -k8s.io/apiextensions-apiserver v0.28.0 h1:CszgmBL8CizEnj4sj7/PtLGey6Na3YgWyGCPONv7E9E= -k8s.io/apiextensions-apiserver v0.28.0/go.mod h1:uRdYiwIuu0SyqJKriKmqEN2jThIJPhVmOWETm8ud1VE= +k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= -k8s.io/apimachinery v0.28.2 h1:KCOJLrc6gu+wV1BYgwik4AF4vXOlVJPdiqn0yAWWwXQ= -k8s.io/apimachinery v0.28.2/go.mod h1:RdzF87y/ngqk9H4z3EL2Rppv5jj95vGS/HaFXrLDApU= k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= -k8s.io/client-go v0.28.2 h1:DNoYI1vGq0slMBN/SWKMZMw0Rq+0EQW6/AK4v9+3VeY= -k8s.io/client-go v0.28.2/go.mod h1:sMkApowspLuc7omj1FOSUxSoqjr+d5Q0Yc0LOFnYFJY= k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= -k8s.io/component-base v0.28.0 h1:HQKy1enJrOeJlTlN4a6dU09wtmXaUvThC0irImfqyxI= +k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= @@ -658,8 +653,6 @@ k8s.io/utils v0.0.0-20230505201702-9f6742963106/go.mod h1:OLgZIPagt7ERELqWJFomSt rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= -sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= sigs.k8s.io/controller-tools v0.13.0 h1:NfrvuZ4bxyolhDBt/rCZhDnx3M2hzlhgo5n3Iv2RykI= @@ -668,7 +661,5 @@ sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMm sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/input/v1beta1/input.go b/input/v1beta1/input.go index 19d8a7e..e44df4b 100644 --- a/input/v1beta1/input.go +++ b/input/v1beta1/input.go @@ -1,6 +1,6 @@ // Package v1beta1 contains the input type for this Function // +kubebuilder:object:generate=true -// +groupName=template.fn.crossplane.io +// +groupName=gotemplating.fn.crossplane.io // +versionName=v1beta1 package v1beta1 @@ -11,10 +11,7 @@ import ( // This isn't a custom resource, in the sense that we never install its CRD. // It is a KRM-like object, so we generate a CRD to describe its schema. -// TODO: Add your input type here! It doesn't need to be called 'Input', you can -// rename it to anything you like. - -// Input can be used to provide input to this Function. +// Input is used to provide templates to this Function. // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:resource:categories=crossplane @@ -22,6 +19,28 @@ type Input struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // Example is an example field. Replace it with whatever input you need. :) - Example string `json:"example"` + // Source specifies the different types of input sources that can be used with this function + Source InputSource `json:"source"` + // Inline is the inline form input of the templates + Inline *InputSourceInline `json:"inline,omitempty"` + // FileSystem is the folder path where the templates are located + FileSystem *InputSourceFileSystem `json:"fileSystem,omitempty"` +} + +type InputSource string + +const ( + // InlineSource indicates that function will get its input as inline + InlineSource InputSource = "Inline" + + // FileSystemSource indicates that function will get its input from a folder + FileSystemSource InputSource = "FileSystem" +) + +type InputSourceInline struct { + Template string `json:"template,omitempty"` +} + +type InputSourceFileSystem struct { + DirPath string `json:"dirPath,omitempty"` } diff --git a/input/v1beta1/zz_generated.deepcopy.go b/input/v1beta1/zz_generated.deepcopy.go index 670dcf1..4bda3cb 100644 --- a/input/v1beta1/zz_generated.deepcopy.go +++ b/input/v1beta1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated // Code generated by controller-gen. DO NOT EDIT. @@ -14,6 +13,16 @@ func (in *Input) DeepCopyInto(out *Input) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Inline != nil { + in, out := &in.Inline, &out.Inline + *out = new(InputSourceInline) + **out = **in + } + if in.FileSystem != nil { + in, out := &in.FileSystem, &out.FileSystem + *out = new(InputSourceFileSystem) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Input. @@ -33,3 +42,33 @@ func (in *Input) DeepCopyObject() runtime.Object { } return nil } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InputSourceFileSystem) DeepCopyInto(out *InputSourceFileSystem) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InputSourceFileSystem. +func (in *InputSourceFileSystem) DeepCopy() *InputSourceFileSystem { + if in == nil { + return nil + } + out := new(InputSourceFileSystem) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InputSourceInline) DeepCopyInto(out *InputSourceInline) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InputSourceInline. +func (in *InputSourceInline) DeepCopy() *InputSourceInline { + if in == nil { + return nil + } + out := new(InputSourceInline) + in.DeepCopyInto(out) + return out +} diff --git a/package/crossplane.yaml b/package/crossplane.yaml index efc618d..a3106d8 100644 --- a/package/crossplane.yaml +++ b/package/crossplane.yaml @@ -2,5 +2,5 @@ apiVersion: meta.pkg.crossplane.io/v1beta1 kind: Function metadata: - name: function-template-go + name: function-go-templating spec: {} diff --git a/package/input/template.fn.crossplane.io_inputs.yaml b/package/input/gotemplating.fn.crossplane.io_inputs.yaml similarity index 62% rename from package/input/template.fn.crossplane.io_inputs.yaml rename to package/input/gotemplating.fn.crossplane.io_inputs.yaml index c39bc5c..8d25a61 100644 --- a/package/input/template.fn.crossplane.io_inputs.yaml +++ b/package/input/gotemplating.fn.crossplane.io_inputs.yaml @@ -3,10 +3,10 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.12.1 - name: inputs.template.fn.crossplane.io + controller-gen.kubebuilder.io/version: v0.13.0 + name: inputs.gotemplating.fn.crossplane.io spec: - group: template.fn.crossplane.io + group: gotemplating.fn.crossplane.io names: categories: - crossplane @@ -19,17 +19,25 @@ spec: - name: v1beta1 schema: openAPIV3Schema: - description: Input can be used to provide input to this Function. + description: Input is used to provide templates to this Function. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' type: string - example: - description: Example is an example field. Replace it with whatever input - you need. :) - type: string + fileSystem: + description: FileSystem is the folder path where the templates are located + properties: + dirPath: + type: string + type: object + inline: + description: Inline is the inline form input of the templates + properties: + template: + type: string + type: object kind: description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client @@ -37,8 +45,12 @@ spec: type: string metadata: type: object + source: + description: Source specifies the different types of input sources that + can be used with this function + type: string required: - - example + - source type: object served: true storage: true diff --git a/template.go b/template.go new file mode 100644 index 0000000..b16d0cd --- /dev/null +++ b/template.go @@ -0,0 +1,100 @@ +package main + +import ( + "os" + "path/filepath" + + "github.com/crossplane/crossplane-runtime/pkg/errors" + + "github.com/crossplane-contrib/function-go-templating/input/v1beta1" +) + +const dotCharacter = 46 + +// TemplateGetter interface is used to read templates from different sources +type TemplateGetter interface { + // GetTemplates returns the templates from the datasource + GetTemplates() string +} + +// NewTemplateSourceGetter returns a TemplateGetter based on the cd source +func NewTemplateSourceGetter(in *v1beta1.Input) (TemplateGetter, error) { + switch in.Source { + case v1beta1.InlineSource: + return newInlineSource(in) + case v1beta1.FileSystemSource: + return newFileSource(in) + default: + return nil, errors.Errorf("invalid input source: %s", in.Source) + } +} + +// InlineSource is a datasource that reads a template from the composition +type InlineSource struct { + Template string +} + +// FileSource is a datasource that reads a template from a folder +type FileSource struct { + FolderPath string + Template string +} + +// GetTemplates returns the inline template +func (is *InlineSource) GetTemplates() string { + return is.Template +} + +func newInlineSource(in *v1beta1.Input) (*InlineSource, error) { + return &InlineSource{ + Template: in.Inline.Template, + }, nil +} + +// GetTemplates returns the templates in the folder +func (fs *FileSource) GetTemplates() string { + return fs.Template +} + +func newFileSource(in *v1beta1.Input) (*FileSource, error) { + d := in.FileSystem.DirPath + + tmpl, err := readTemplates(d) + if err != nil { + return nil, errors.Errorf("cannot read tmpl from the folder %s: %s", *in.FileSystem, err) + } + + return &FileSource{ + FolderPath: in.FileSystem.DirPath, + Template: tmpl, + }, nil +} + +func readTemplates(dir string) (string, error) { + tmpl := "" + + if err := filepath.Walk(dir, func(path string, info os.FileInfo, e error) error { + if e != nil { + return e + } + + // check for directory and hidden files/folders + if info.IsDir() || info.Name()[0] == dotCharacter { + return nil + } + + data, err := os.ReadFile(path) + if err != nil { + return err + } + + tmpl += string(data) + tmpl += "\n---\n" + + return nil + }); err != nil { + return "", err + } + + return tmpl, nil +} diff --git a/testdata/templates/templates.yaml b/testdata/templates/templates.yaml new file mode 100644 index 0000000..c381b4c --- /dev/null +++ b/testdata/templates/templates.yaml @@ -0,0 +1,8 @@ +apiVersion: example.org/v1 +kind: CD +metadata: + name: cool-cd + annotations: + gotemplating.fn.crossplane.io/composition-resource-name: cool-cd + labels: + belongsTo: {{ .observed.composite.resource.metadata.name|quote }} \ No newline at end of file