From 3cd22afa9ce863a6e0e7df28da91a41568adf9c0 Mon Sep 17 00:00:00 2001 From: Rajat Jindal Date: Wed, 2 Apr 2025 13:51:10 +0530 Subject: [PATCH 1/2] feat(wasip2/kv): add support for wasip2 kv Signed-off-by: Rajat Jindal --- .github/workflows/pr.yaml | 4 +- v2/examples/kv/go.mod | 12 ++++ v2/examples/kv/go.sum | 4 ++ v2/examples/kv/main.go | 50 ++++++++++++++ v2/examples/kv/spin.toml | 17 +++++ v2/integration_test.go | 24 +++++++ v2/kv/kv.go | 103 +++++++++++++++++++++++++++++ v2/kv/testdata/key-value/go.mod | 12 ++++ v2/kv/testdata/key-value/go.sum | 4 ++ v2/kv/testdata/key-value/main.go | 50 ++++++++++++++ v2/kv/testdata/key-value/spin.toml | 17 +++++ 11 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 v2/examples/kv/go.mod create mode 100644 v2/examples/kv/go.sum create mode 100644 v2/examples/kv/main.go create mode 100644 v2/examples/kv/spin.toml create mode 100644 v2/kv/kv.go create mode 100644 v2/kv/testdata/key-value/go.mod create mode 100644 v2/kv/testdata/key-value/go.sum create mode 100644 v2/kv/testdata/key-value/main.go create mode 100644 v2/kv/testdata/key-value/spin.toml diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index ad174d9..bc0f585 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -1,8 +1,8 @@ name: Run Integration tests on: - push: + pull_request: branches: - - wasip2-http + - wasip2 workflow_dispatch: {} diff --git a/v2/examples/kv/go.mod b/v2/examples/kv/go.mod new file mode 100644 index 0000000..5496fc6 --- /dev/null +++ b/v2/examples/kv/go.mod @@ -0,0 +1,12 @@ +module github.com/spinframework/spin-go-sdk/v2/examples/kv + +go 1.24.1 + +require github.com/spinframework/spin-go-sdk/v2 v2.0.0 + +require ( + github.com/julienschmidt/httprouter v1.3.0 // indirect + go.bytecodealliance.org/cm v0.2.2 // indirect +) + +replace github.com/spinframework/spin-go-sdk/v2 => ../../ diff --git a/v2/examples/kv/go.sum b/v2/examples/kv/go.sum new file mode 100644 index 0000000..c1ebfdf --- /dev/null +++ b/v2/examples/kv/go.sum @@ -0,0 +1,4 @@ +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +go.bytecodealliance.org/cm v0.2.2 h1:M9iHS6qs884mbQbIjtLX1OifgyPG9DuMs2iwz8G4WQA= +go.bytecodealliance.org/cm v0.2.2/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI= diff --git a/v2/examples/kv/main.go b/v2/examples/kv/main.go new file mode 100644 index 0000000..012c946 --- /dev/null +++ b/v2/examples/kv/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + spinhttp "github.com/spinframework/spin-go-sdk/v2/http" + "github.com/spinframework/spin-go-sdk/v2/kv" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + store, err := kv.OpenDefault() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = store.Set("foo", []byte("bar")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + value, err := store.Get("foo") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if string(value) != "bar" { + http.Error(w, fmt.Sprintf("expected: %q, got: %q", "bar", value), http.StatusInternalServerError) + return + } + + keys, err := store.GetKeys() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(keys) + }) +} + +func main() {} diff --git a/v2/examples/kv/spin.toml b/v2/examples/kv/spin.toml new file mode 100644 index 0000000..b35d1c0 --- /dev/null +++ b/v2/examples/kv/spin.toml @@ -0,0 +1,17 @@ +spin_manifest_version = 2 + +[application] +authors = ["Rajat Jindal "] +description = "A simple Spin application written in (Tiny)Go." +name = "hello-kv" +version = "1.0.0" + +[[trigger.http]] +route = "/hello" +component = "hello" + +[component.hello] +source = "main.wasm" +key_value_stores = ["default"] +[component.hello.build] +command = "tinygo build -target=wasip2 --wit-package $(go list -mod=readonly -m -f '{{.Dir}}' github.com/spinframework/spin-go-sdk/v2)/wit --wit-world http-trigger -gc=leaking -no-debug -o main.wasm main.go" \ No newline at end of file diff --git a/v2/integration_test.go b/v2/integration_test.go index fb6e8e8..7fb4470 100644 --- a/v2/integration_test.go +++ b/v2/integration_test.go @@ -120,6 +120,30 @@ func TestHTTPTriger(t *testing.T) { } } +func TestKeyValue(t *testing.T) { + spin := startSpin(t, "kv/testdata/key-value") + defer spin.cancel() + + resp := retryGet(t, spin.url+"/hello") + spin.cancel() + if resp.Body == nil { + t.Fatal("body is nil") + } + t.Log(resp.Status) + b, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + t.Fatal(err) + } + + // assert response body + want := "[\"foo\"]\n" + got := string(b) + if want != got { + t.Fatalf("body is not equal: want = %q got = %q", want, got) + } +} + // TestBuildExamples ensures that the tinygo examples will build successfully. func TestBuildExamples(t *testing.T) { examples, err := os.ReadDir("examples") diff --git a/v2/kv/kv.go b/v2/kv/kv.go new file mode 100644 index 0000000..0a6eb6b --- /dev/null +++ b/v2/kv/kv.go @@ -0,0 +1,103 @@ +package kv + +import ( + "fmt" + + keyvalue "github.com/spinframework/spin-go-sdk/v2/internal/fermyon/spin/v2.0.0/key-value" + "go.bytecodealliance.org/cm" +) + +type Store struct { + store *keyvalue.Store +} + +// Open the store with the label. +func Open(label string) (*Store, error) { + result := keyvalue.StoreOpen(label) + if result.IsErr() { + return nil, errorVariantToError(*result.Err()) + } + + return &Store{ + store: result.OK(), + }, nil +} + +// Open the default store. +// +// This is equivalent to `kv.Open("default")`. +func OpenDefault() (*Store, error) { + return Open("default") +} + +// Set the key/value pair in store +func (s *Store) Set(key string, value []byte) error { + result := s.store.Set(key, cm.ToList(value)) + if result.IsErr() { + return errorVariantToError(*result.Err()) + } + + return nil +} + +// Get the value of provided key from the store +func (s *Store) Get(key string) ([]byte, error) { + result := s.store.Get(key) + if result.IsErr() { + return nil, errorVariantToError(*result.Err()) + } + + value := result.OK() + if value.None() { + return []byte(""), nil + } + + return value.Some().Slice(), nil +} + +// Delete the given key/value from the store +func (s *Store) Delete(key string) error { + result := s.store.Delete(key) + if result.IsErr() { + return errorVariantToError(*result.Err()) + } + + return nil +} + +// Exists check if a given key exist in the store +func (s *Store) Exists(key string) (bool, error) { + result := s.store.Exists(key) + if result.IsErr() { + return false, errorVariantToError(*result.Err()) + } + + return *result.OK(), nil +} + +// GetKets returns all the keys from the store +func (s *Store) GetKeys() ([]string, error) { + result := s.store.GetKeys() + if result.IsErr() { + return nil, errorVariantToError(*result.Err()) + } + + return result.OK().Slice(), nil +} + +func errorVariantToError(code keyvalue.Error) error { + switch code { + case keyvalue.ErrorAccessDenied(): + return fmt.Errorf("access denied") + case keyvalue.ErrorNoSuchStore(): + return fmt.Errorf("no such store") + case keyvalue.ErrorStoreTableFull(): + return fmt.Errorf("store table full") + default: + if code.Other() != nil { + return fmt.Errorf(*code.Other()) + } + + return fmt.Errorf("no error provided by host implementation") + } +} diff --git a/v2/kv/testdata/key-value/go.mod b/v2/kv/testdata/key-value/go.mod new file mode 100644 index 0000000..5528778 --- /dev/null +++ b/v2/kv/testdata/key-value/go.mod @@ -0,0 +1,12 @@ +module github.com/spinframework/spin-go-sdk/v2/http/testdata/kv + +go 1.24.1 + +require github.com/spinframework/spin-go-sdk/v2 v2.0.0 + +require ( + github.com/julienschmidt/httprouter v1.3.0 // indirect + go.bytecodealliance.org/cm v0.2.2 // indirect +) + +replace github.com/spinframework/spin-go-sdk/v2 => ../../../ diff --git a/v2/kv/testdata/key-value/go.sum b/v2/kv/testdata/key-value/go.sum new file mode 100644 index 0000000..c1ebfdf --- /dev/null +++ b/v2/kv/testdata/key-value/go.sum @@ -0,0 +1,4 @@ +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +go.bytecodealliance.org/cm v0.2.2 h1:M9iHS6qs884mbQbIjtLX1OifgyPG9DuMs2iwz8G4WQA= +go.bytecodealliance.org/cm v0.2.2/go.mod h1:JD5vtVNZv7sBoQQkvBvAAVKJPhR/bqBH7yYXTItMfZI= diff --git a/v2/kv/testdata/key-value/main.go b/v2/kv/testdata/key-value/main.go new file mode 100644 index 0000000..012c946 --- /dev/null +++ b/v2/kv/testdata/key-value/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + + spinhttp "github.com/spinframework/spin-go-sdk/v2/http" + "github.com/spinframework/spin-go-sdk/v2/kv" +) + +func init() { + spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) { + store, err := kv.OpenDefault() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = store.Set("foo", []byte("bar")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + value, err := store.Get("foo") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if string(value) != "bar" { + http.Error(w, fmt.Sprintf("expected: %q, got: %q", "bar", value), http.StatusInternalServerError) + return + } + + keys, err := store.GetKeys() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(keys) + }) +} + +func main() {} diff --git a/v2/kv/testdata/key-value/spin.toml b/v2/kv/testdata/key-value/spin.toml new file mode 100644 index 0000000..bfd7504 --- /dev/null +++ b/v2/kv/testdata/key-value/spin.toml @@ -0,0 +1,17 @@ +spin_manifest_version = 2 + +[application] +authors = ["Rajat Jindal "] +description = "A simple Spin application written in (Tiny)Go." +name = "key-value" +version = "1.0.0" + +[[trigger.http]] +route = "/hello" +component = "hello" + +[component.hello] +source = "main.wasm" +key_value_stores = ["default"] +[component.hello.build] +command = "tinygo build -target=wasip2 --wit-package $(go list -mod=readonly -m -f '{{.Dir}}' github.com/spinframework/spin-go-sdk/v2)/wit --wit-world http-trigger -gc=leaking -no-debug -o main.wasm main.go" \ No newline at end of file From 5b03b6d2a6426750941769c49424cee6b07c0177 Mon Sep 17 00:00:00 2001 From: Rajat Jindal Date: Wed, 2 Apr 2025 14:04:09 +0530 Subject: [PATCH 2/2] install wasm-tools and v3.2.0 of spin Signed-off-by: Rajat Jindal --- .github/workflows/pr.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index bc0f585..0fb900a 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -15,17 +15,20 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.23' + go-version: '1.24' - name: Setup TinyGo uses: acifani/setup-tinygo@v2 with: - tinygo-version: '0.33.0' + tinygo-version: '0.37.0' - name: Setup Spin uses: fermyon/actions/spin/setup@v1 with: - version: "v2.7.0" + version: "v3.2.0" + + - name: Setup `wasm-tools` + uses: bytecodealliance/actions/wasm-tools/setup@v1 - name: Run wasip2 integration tests run: make test-integration-wasip2