Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feature: Go net/http -> wasi:http Proxy #34

Merged
merged 4 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,25 @@ name: Go

ricochet marked this conversation as resolved.
Show resolved Hide resolved
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.22"

- name: Build
run: go build -v ./...
- name: Build
run: go build -v ./...

- name: Test
run: go test -v ./...
- name: Test
run: go test -v ./...

lint:
runs-on: ubuntu-latest
Expand All @@ -33,6 +32,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.58
version: v1.61
# temporary until current issues in the repo are fixed
only-new-issues: true
only-new-issues: true

2 changes: 2 additions & 0 deletions examples/http-server/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
http-server
/build
48 changes: 48 additions & 0 deletions examples/http-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# http-server

This example demonstrates how to forward requests to components exporting `wasi:http/incoming-handler`.

It starts a http server listening on port 8080 containing 2 routes:

- `/proxy`: Forwards the request to the component `http-component`
- `/`: Serve the request directly from the provider

# Internals

Proxying uses a custom `http.RoundTripper` implementation that forwards requests to the component.
In this example we forward to a single target ( `http-http_component` ).

```go
transport := wrpchttp.NewIncomingRoundTripper(wasmcloudprovider, wrpchttp.WithSingleTarget("http-http_component"))

wasiIncomingClient := &http.Client{
Transport: transport,
}

wasiIncomingClient.Get("http://localhost:8080/proxy")
```

You can also provide a custom `Director` function to select the target based on the request.

```go
func director(r *http.Request) string {
if r.URL.Host == "api" {
return "http-api"
}
return "http-ui"
})


transport := wrpchttp.NewIncomingRoundTripper(wasmcloudprovider, wrpchttp.WithDirector(director))

wasiIncomingClient := &http.Client{
Transport: transport,
}

// forward to http-api component
wasiIncomingClient.Get("http://api/users")

// forward to http-ui component
wasiIncomingClient.Get("http://ui/index.html")
wasiIncomingClient.Get("http://anyothername/index.html")
```
3 changes: 3 additions & 0 deletions examples/http-server/bindings/server.wrpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Generated by `wit-bindgen-wrpc-go` 0.8.0. DO NOT EDIT!
// server package contains wRPC bindings for `server` world
package server
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
// Generated by `wit-bindgen-wrpc-go` 0.8.0. DO NOT EDIT!
package monotonic_clock

import (
bytes "bytes"
context "context"
binary "encoding/binary"
errors "errors"
fmt "fmt"
wasi__io__poll "github.com/wasmCloud/provider-sdk-go/examples/http-server/bindings/wasi/io/poll"
io "io"
slog "log/slog"
utf8 "unicode/utf8"
wrpc "wrpc.io/go"
)

type Pollable = wasi__io__poll.Pollable

// An instant in time, in nanoseconds. An instant is relative to an
// unspecified initial value, and can only be compared to instances from
// the same monotonic-clock.
type Instant = uint64

// A duration of time, in nanoseconds.
type Duration = uint64

// Read the current value of the clock.
//
// The clock is monotonic, therefore calling this function repeatedly will
// produce a sequence of non-decreasing values.
func Now(ctx__ context.Context, wrpc__ wrpc.Invoker) (r0__ uint64, err__ error) {
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:clocks/monotonic-clock@0.2.0", "now", nil)
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `now`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "now", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "now", "err", cErr__)
}
r0__, err__ = func() (Instant, error) {
v, err := func(r io.ByteReader) (uint64, error) {
var x uint64
var s uint8
for i := 0; i < 10; i++ {
slog.Debug("reading u64 byte", "i", i)
b, err := r.ReadByte()
if err != nil {
if i > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return x, fmt.Errorf("failed to read u64 byte: %w", err)
}
if s == 63 && b > 0x01 {
return x, errors.New("varint overflows a 64-bit integer")
}
if b < 0x80 {
return x | uint64(b)<<s, nil
}
x |= uint64(b&0x7f) << s
s += 7
}
return x, errors.New("varint overflows a 64-bit integer")
}(r__)
return (Instant)(v), err
}()

if err__ != nil {
err__ = fmt.Errorf("failed to read result 0: %w", err__)
return
}
return
}

// Query the resolution of the clock. Returns the duration of time
// corresponding to a clock tick.
func Resolution(ctx__ context.Context, wrpc__ wrpc.Invoker) (r0__ uint64, err__ error) {
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:clocks/monotonic-clock@0.2.0", "resolution", nil)
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `resolution`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "resolution", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "resolution", "err", cErr__)
}
r0__, err__ = func() (Duration, error) {
v, err := func(r io.ByteReader) (uint64, error) {
var x uint64
var s uint8
for i := 0; i < 10; i++ {
slog.Debug("reading u64 byte", "i", i)
b, err := r.ReadByte()
if err != nil {
if i > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return x, fmt.Errorf("failed to read u64 byte: %w", err)
}
if s == 63 && b > 0x01 {
return x, errors.New("varint overflows a 64-bit integer")
}
if b < 0x80 {
return x | uint64(b)<<s, nil
}
x |= uint64(b&0x7f) << s
s += 7
}
return x, errors.New("varint overflows a 64-bit integer")
}(r__)
return (Duration)(v), err
}()

if err__ != nil {
err__ = fmt.Errorf("failed to read result 0: %w", err__)
return
}
return
}

// Create a `pollable` which will resolve once the specified instant
// occured.
func SubscribeInstant(ctx__ context.Context, wrpc__ wrpc.Invoker, when uint64) (r0__ wrpc.Own[Pollable], err__ error) {
var buf__ bytes.Buffer
write0__, err__ := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) {
b := make([]byte, binary.MaxVarintLen64)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing u64")
_, err = w.Write(b[:i])
return err
}(when, &buf__)
if err__ != nil {
err__ = fmt.Errorf("failed to write `when` parameter: %w", err__)
return
}
if write0__ != nil {
err__ = errors.New("unexpected deferred write for synchronous `when` parameter")
return
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:clocks/monotonic-clock@0.2.0", "subscribe-instant", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `subscribe-instant`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "subscribe-instant", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "subscribe-instant", "err", cErr__)
}
r0__, err__ = func(r interface {
io.ByteReader
io.Reader
}) (wrpc.Own[Pollable], error) {
var x uint32
var s uint
for i := 0; i < 5; i++ {
slog.Debug("reading owned resource ID length byte", "i", i)
b, err := r.ReadByte()
if err != nil {
if i > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return "", fmt.Errorf("failed to read owned resource ID length byte: %w", err)
}
if b < 0x80 {
if i == 4 && b > 1 {
return "", errors.New("owned resource ID length overflows a 32-bit integer")
}
x = x | uint32(b)<<s
buf := make([]byte, x)
slog.Debug("reading owned resource ID bytes", "len", x)
_, err = r.Read(buf)
if err != nil {
return "", fmt.Errorf("failed to read owned resource ID bytes: %w", err)
}
if !utf8.Valid(buf) {
return "", errors.New("owned resource ID is not valid UTF-8")
}
return wrpc.Own[Pollable](buf), nil
}
x |= uint32(b&0x7f) << s
s += 7
}
return "", errors.New("owned resource ID length overflows a 32-bit integer")
}(r__)
if err__ != nil {
err__ = fmt.Errorf("failed to read result 0: %w", err__)
return
}
return
}

// Create a `pollable` which will resolve once the given duration has
// elapsed, starting at the time at which this function was called.
// occured.
func SubscribeDuration(ctx__ context.Context, wrpc__ wrpc.Invoker, when uint64) (r0__ wrpc.Own[Pollable], err__ error) {
var buf__ bytes.Buffer
write0__, err__ := (func(wrpc.IndexWriter) error)(nil), func(v uint64, w io.Writer) (err error) {
b := make([]byte, binary.MaxVarintLen64)
i := binary.PutUvarint(b, uint64(v))
slog.Debug("writing u64")
_, err = w.Write(b[:i])
return err
}(when, &buf__)
if err__ != nil {
err__ = fmt.Errorf("failed to write `when` parameter: %w", err__)
return
}
if write0__ != nil {
err__ = errors.New("unexpected deferred write for synchronous `when` parameter")
return
}
var w__ wrpc.IndexWriteCloser
var r__ wrpc.IndexReadCloser
w__, r__, err__ = wrpc__.Invoke(ctx__, "wasi:clocks/monotonic-clock@0.2.0", "subscribe-duration", buf__.Bytes())
if err__ != nil {
err__ = fmt.Errorf("failed to invoke `subscribe-duration`: %w", err__)
return
}
defer func() {
if err := r__.Close(); err != nil {
slog.ErrorContext(ctx__, "failed to close reader", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "subscribe-duration", "err", err)
}
}()
if cErr__ := w__.Close(); cErr__ != nil {
slog.DebugContext(ctx__, "failed to close outgoing stream", "instance", "wasi:clocks/monotonic-clock@0.2.0", "name", "subscribe-duration", "err", cErr__)
}
r0__, err__ = func(r interface {
io.ByteReader
io.Reader
}) (wrpc.Own[Pollable], error) {
var x uint32
var s uint
for i := 0; i < 5; i++ {
slog.Debug("reading owned resource ID length byte", "i", i)
b, err := r.ReadByte()
if err != nil {
if i > 0 && err == io.EOF {
err = io.ErrUnexpectedEOF
}
return "", fmt.Errorf("failed to read owned resource ID length byte: %w", err)
}
if b < 0x80 {
if i == 4 && b > 1 {
return "", errors.New("owned resource ID length overflows a 32-bit integer")
}
x = x | uint32(b)<<s
buf := make([]byte, x)
slog.Debug("reading owned resource ID bytes", "len", x)
_, err = r.Read(buf)
if err != nil {
return "", fmt.Errorf("failed to read owned resource ID bytes: %w", err)
}
if !utf8.Valid(buf) {
return "", errors.New("owned resource ID is not valid UTF-8")
}
return wrpc.Own[Pollable](buf), nil
}
x |= uint32(b&0x7f) << s
s += 7
}
return "", errors.New("owned resource ID length overflows a 32-bit integer")
}(r__)
if err__ != nil {
err__ = fmt.Errorf("failed to read result 0: %w", err__)
return
}
return
}
Loading