From 802a5de87e305c92bade7bb3a6ca27408ded647e Mon Sep 17 00:00:00 2001 From: Alexei Mikhailov Date: Sun, 23 Jun 2024 13:12:24 +0300 Subject: [PATCH 1/2] Replace faux `base64` module with functions `btoa` and `atob` Browsers already have built-in functions `btoa` and `atob`: https://developer.mozilla.org/en-US/docs/Glossary/Base64 Goja, on the other hand, doesn't implement these functions: https://github.com/dop251/goja/discussions/541 Providing these functions instead of a faux module IMO, would be less confusing and more in line with a "normal" JavaScript. --- README.md | 43 ++++++++++++++++++++++++++----------- example/composition.yaml | 3 +-- fn_test.go | 2 +- internal/js/runtime.go | 3 ++- internal/js/runtime_test.go | 16 +++++++------- internal/modules/base64.go | 16 +++++++------- response.go | 9 ++++++-- 7 files changed, 57 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index f63b503..79698ce 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ spec: spec: source: inline: | - import * as Base64 from 'base64'; - export default (req, rsp) => { const composite = req.observed.composite.resource; @@ -41,8 +39,12 @@ spec: }); if (req.observed.resources?.bucket) { - // expose some connection details, get value from a resource generated within this function - rsp.setConnectionDetails({ bucketName: req.observed.resources.bucket.resource.metadata.name }); + // Expose some connection details, get value from a resource generated within this function. + // The function expects Base64-encoded strings. Use "btoa" function to encode plain strings. + // ConnectionDetails from observed resources are already Base64-encoded. + rsp.setConnectionDetails({ + bucketName: btoa(req.observed.resources.bucket.resource.metadata.name) + }); // patch composite resource status rsp.updateCompositeStatus({ bucketName: req.observed.resources.bucket.resource.metadata.name }); @@ -103,16 +105,23 @@ a default function. The exported function is called with 2 arguments: * `response.setConnectionDetails(details)` - sets the desired composite resource connection details. - Note that connection details from other observed resources are base64-encoded, and - if you want to use them in other composed resources, or in the composition connection - details, you need to decode them first: + Connection details values must be Base64-encoded, use function `btoa` to encode + plain strings to Base64. + + Connection details from other observed resources are already Base64-encoded, so + you can pass their values to `setConnectionDetails` function as is: ```javascript import * as Base64 from 'base64'; export default function (req, rsp) { // ...skip for brevity - const username = Base64.decode(req.observed.resources.user.connectionDetails.username); - rsp.setConnectionDetails({ username }); + const username = req.observed.resources.user.connectionDetails.username; + const host = "localhost"; + + rsp.setConnectionDetails({ + username, + host: btoa(host) + }); } ``` * `response.updateCompositeStatus(properties)` - merges the desired composite resource status in the @@ -145,11 +154,18 @@ For convenience, the runtime includes some "faux" external packages: console.error('Error'); } ``` -* `base64` - includes functions for working with Base64 encoding: +* `btoa`, `atob` - functions for working with Base64 encoding: + ```javascript + const enc = btoa('string'); + const dec = atob(enc); // => 'string' + ``` + + **NB!** Unlike functions [`Window.btoa()`][base64] and [`Window.atob()`][base64] available + in browsers, these functions work natively with UTF-8 strings and don't require additional + manipulations: ```javascript - import * as Base64 from 'base64'; - const enc = Base64.encode('string'); - const dec = Base64.decode(enc); // => 'string' + // this will work in your composition function, but won't work in browsers + btoa("a Ā 𐀀 文 🦄") ``` * `yaml` - includes functions for encoding and decoding objects into YAML format: ```javascript @@ -188,3 +204,4 @@ $ make xpkg.build [resp]: https://buf.build/crossplane/crossplane/docs/main:apiextensions.fn.proto.v1beta1#apiextensions.fn.proto.v1beta1.RunFunctionResponse [esbuild]: https://esbuild.github.io/ [webpack]: https://webpack.js.org/ +[base64]: https://developer.mozilla.org/en-US/docs/Glossary/Base64 diff --git a/example/composition.yaml b/example/composition.yaml index 1eec026..82b2908 100644 --- a/example/composition.yaml +++ b/example/composition.yaml @@ -23,7 +23,6 @@ spec: source: inline: | import * as YAML from 'yaml'; - import * as Base64 from 'base64'; export default (req, rsp) => { console.log("request", JSON.stringify(req, null, 2)); @@ -46,7 +45,7 @@ spec: region: region, compositeRegion: req.observed.composite.resource.spec.region, yamlTest: YAML.stringify({ foo: "bar", bax: [12, 23] }), - b64test: Base64.encode('abcdefgh') + b64test: btoa('abcdefgh') } } }); diff --git a/fn_test.go b/fn_test.go index c5773a2..7e4a601 100644 --- a/fn_test.go +++ b/fn_test.go @@ -152,7 +152,7 @@ func TestRunFunction(t *testing.T) { forProvider: { region: req.observed.composite.resource.spec.region } } }); - rsp.setConnectionDetails({ key: 'value' }); + rsp.setConnectionDetails({ key: btoa('value') }); };`), Observed: &fnv1beta1.State{ Composite: &fnv1beta1.Resource{ diff --git a/internal/js/runtime.go b/internal/js/runtime.go index c877daa..f2fb8fa 100644 --- a/internal/js/runtime.go +++ b/internal/js/runtime.go @@ -27,8 +27,9 @@ func NewRuntime() *Runtime { registry := new(require.Registry) registry.Enable(vm) - registry.RegisterNativeModule("base64", modules.Base64) registry.RegisterNativeModule("yaml", modules.YAML) + + modules.Base64.Enable(vm) console.Enable(vm) return &Runtime{vm: vm} diff --git a/internal/js/runtime_test.go b/internal/js/runtime_test.go index c5cd8c0..2189748 100644 --- a/internal/js/runtime_test.go +++ b/internal/js/runtime_test.go @@ -126,20 +126,20 @@ func TestRuntime_RunScript(t *testing.T) { ok: false, }, { - desc: "Base64.encode", - script: `import * as Base64 from "base64"; export default function() { return Base64.encode('abcd') }`, - expected: "YWJjZA==", + desc: "btoa", + script: `export default function() { return btoa('Hēłłõ, wöřłď') }`, + expected: "SMSTxYLFgsO1LCB3w7bFmcWCxI8=", ok: true, }, { - desc: "Base64.decode", - script: `import * as Base64 from "base64"; export default function() { return Base64.decode('YWJjZA==') }`, - expected: "abcd", + desc: "atob", + script: `export default function() { return atob('SMSTxYLFgsO1LCB3w7bFmcWCxI8=') }`, + expected: "Hēłłõ, wöřłď", ok: true, }, { - desc: "Base64.decode error", - script: `import * as Base64 from "base64"; export default function() { return Base64.decode('YWJjZA=') }`, + desc: "atob error", + script: `export default function() { return atob('YWJjZA=') }`, ok: false, }, } diff --git a/internal/modules/base64.go b/internal/modules/base64.go index abae513..059d9bf 100644 --- a/internal/modules/base64.go +++ b/internal/modules/base64.go @@ -10,20 +10,20 @@ import ( // // Example (js): // -// import * as Base64 from "base64"; -// export default () => { -// return Base64.decode(Base64.encode("foo")) -// }; -func Base64(runtime *goja.Runtime, module *goja.Object) { - o := module.Get("exports").(*goja.Object) +// const encoded = btoa('hello'); +// const decoded = atob(encoded); +var Base64 = &Base64module{} - _ = o.Set("encode", func(call goja.FunctionCall) goja.Value { +type Base64module struct{} + +func (b *Base64module) Enable(runtime *goja.Runtime) { + _ = runtime.Set("btoa", func(call goja.FunctionCall) goja.Value { str := call.Argument(0).ToString().String() result := base64.StdEncoding.EncodeToString([]byte(str)) return runtime.ToValue(result) }) - _ = o.Set("decode", func(call goja.FunctionCall) goja.Value { + _ = runtime.Set("atob", func(call goja.FunctionCall) goja.Value { str := call.Argument(0).ToString().String() result, err := base64.StdEncoding.DecodeString(str) if err != nil { diff --git a/response.go b/response.go index c5b20ec..a0ab0f7 100644 --- a/response.go +++ b/response.go @@ -1,6 +1,8 @@ package main import ( + "encoding/base64" + "dario.cat/mergo" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" @@ -103,8 +105,11 @@ func (r *Response) UpdateCompositeStatus(status map[string]any) error { // SetConnectionDetails sets the desired composite resource connection details // in the function response. -func (r *Response) SetConnectionDetails(details resource.ConnectionDetails) { - r.desiredComposite.ConnectionDetails = details +func (r *Response) SetConnectionDetails(details map[string]string) { + for key, val := range details { + decoded, _ := base64.StdEncoding.DecodeString(val) + r.desiredComposite.ConnectionDetails[key] = decoded + } } func (r *Response) setFunctionResponse(rsp *fnv1beta1.RunFunctionResponse) error { From 593ac5a7906249044e3ea989f457d6bee2157893 Mon Sep 17 00:00:00 2001 From: Alexei Mikhailov Date: Sun, 23 Jun 2024 13:18:03 +0300 Subject: [PATCH 2/2] Bump version to 0.2.0 --- README.md | 2 +- example/functions.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79698ce..b51910e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ kind: Function metadata: name: function-javascript spec: - package: docker.io/salemove/crossplane-function-javascript:v0.1.0 + package: docker.io/salemove/crossplane-function-javascript:v0.2.0 EOF ``` diff --git a/example/functions.yaml b/example/functions.yaml index 1e392a0..cdca716 100644 --- a/example/functions.yaml +++ b/example/functions.yaml @@ -8,4 +8,4 @@ metadata: render.crossplane.io/runtime: Development spec: # This is ignored when using the Development runtime. - package: function-javascript + package: docker.io/salemove/crossplane-function-javascript:v0.2.0