Skip to content

Commit af7aa29

Browse files
committed
feat: wasmbus Policy & Config Service
Signed-off-by: Lucas Fontes <[email protected]>
1 parent 6530018 commit af7aa29

File tree

13 files changed

+523
-40
lines changed

13 files changed

+523
-40
lines changed

.github/workflows/wasmbus-go.yaml

+8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ jobs:
7878
working-directory: x/wasmbus/events
7979
run: go test -cover -v -wash-output
8080

81+
- name: wasmbus/policy
82+
working-directory: x/wasmbus/policy
83+
run: go test -cover -v -wash-output
84+
85+
- name: wasmbus/config
86+
working-directory: x/wasmbus/config
87+
run: go test -cover -v -wash-output
88+
8189
examples:
8290
# Context: https://github.com./golangci/golangci-lint-action/blob/v6.1.1/README.md#annotations
8391
permissions:

x/wasmbus/bus.go

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const (
2222
PrefixEvents = "wasmbus.evt"
2323
// PrefixControl is the prefix for Lattice RPC.
2424
PrefixCtlV1 = "wasmbus.ctl.v1"
25+
26+
PrefixConfig = "wasmbus.cfg"
2527
)
2628

2729
var (

x/wasmbus/config/api.go

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
var (
9+
ErrProtocol = fmt.Errorf("encoding error")
10+
ErrInternal = fmt.Errorf("internal error")
11+
)
12+
13+
type API interface {
14+
// Host is currently the only method exposed by the API.
15+
Host(ctx context.Context, req *HostRequest) (*HostResponse, error)
16+
}
17+
18+
var _ API = (*APIMock)(nil)
19+
20+
type APIMock struct {
21+
HostFunc func(ctx context.Context, req *HostRequest) (*HostResponse, error)
22+
}
23+
24+
func (m *APIMock) Host(ctx context.Context, req *HostRequest) (*HostResponse, error) {
25+
return m.HostFunc(ctx, req)
26+
}
27+
28+
type HostRequest struct {
29+
Labels map[string]string `json:"labels"`
30+
}
31+
32+
type HostResponse struct {
33+
RegistryCredentials map[string]RegistryCredential `json:"registryCredentials,omitempty"`
34+
}
35+
36+
type RegistryCredential struct {
37+
Username string `json:"username"`
38+
Password string `json:"password"`
39+
}

x/wasmbus/config/server.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
6+
"go.wasmcloud.dev/x/wasmbus"
7+
)
8+
9+
type Server struct {
10+
*wasmbus.Server
11+
Lattice string
12+
api API
13+
}
14+
15+
func NewServer(bus wasmbus.Bus, lattice string, api API) *Server {
16+
return &Server{
17+
Server: wasmbus.NewServer(bus),
18+
Lattice: lattice,
19+
api: api,
20+
}
21+
}
22+
23+
func (s *Server) Serve() error {
24+
subject := fmt.Sprintf("%s.%s.req", wasmbus.PrefixConfig, s.Lattice)
25+
handler := wasmbus.NewRequestHandler(HostRequest{}, HostResponse{}, s.api.Host)
26+
return s.RegisterHandler(subject, handler)
27+
}

x/wasmbus/config/server_test.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
"time"
8+
9+
"github.com./nats-io/nats.go"
10+
"go.wasmcloud.dev/x/wasmbus"
11+
"go.wasmcloud.dev/x/wasmbus/wasmbustest"
12+
)
13+
14+
func TestServer(t *testing.T) {
15+
defer wasmbustest.MustStartNats(t)()
16+
17+
nc, err := nats.Connect(nats.DefaultURL)
18+
if err != nil {
19+
t.Fatalf("failed to connect to nats: %v", err)
20+
}
21+
bus := wasmbus.NewNatsBus(nc)
22+
s := NewServer(bus, "test", &APIMock{
23+
HostFunc: func(ctx context.Context, req *HostRequest) (*HostResponse, error) {
24+
return &HostResponse{
25+
RegistryCredentials: map[string]RegistryCredential{
26+
"docker.io": {
27+
Username: "my-username",
28+
Password: "hunter2",
29+
},
30+
},
31+
}, nil
32+
},
33+
})
34+
if err := s.Serve(); err != nil {
35+
t.Fatalf("failed to start server: %v", err)
36+
}
37+
38+
req := wasmbus.NewMessage(fmt.Sprintf("%s.%s.req", wasmbus.PrefixConfig, "test"))
39+
req.Data = []byte(`{"labels":{"hostcore.arch":"aarch64","hostcore.os":"linux","hostcore.osfamily":"unix","kubernetes":"true","kubernetes.hostgroup":"default"}}`)
40+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
41+
defer cancel()
42+
43+
rawResp, err := bus.Request(ctx, req)
44+
if err != nil {
45+
t.Fatal(err)
46+
}
47+
48+
var resp HostResponse
49+
if err := wasmbus.Decode(rawResp, &resp); err != nil {
50+
t.Fatal(err)
51+
}
52+
53+
docker, ok := resp.RegistryCredentials["docker.io"]
54+
if !ok {
55+
t.Fatalf("expected docker.io registry credentials")
56+
}
57+
if want, got := "my-username", docker.Username; want != got {
58+
t.Fatalf("expected username %q, got %q", want, got)
59+
}
60+
61+
if want, got := "hunter2", docker.Password; want != got {
62+
t.Fatalf("expected password %q, got %q", want, got)
63+
}
64+
65+
if err := s.Drain(); err != nil {
66+
t.Fatalf("failed to drain server: %v", err)
67+
}
68+
}

x/wasmbus/go.mod

+8-7
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@ require (
1010
)
1111

1212
require (
13-
github.com./google/uuid v1.1.1 // indirect
14-
github.com./json-iterator/go v1.1.10 // indirect
13+
github.com./google/go-cmp v0.6.0 // indirect
14+
github.com./google/uuid v1.6.0 // indirect
15+
github.com./json-iterator/go v1.1.12 // indirect
1516
github.com./klauspost/compress v1.17.11 // indirect
1617
github.com./minio/highwayhash v1.0.3 // indirect
17-
github.com./modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
18-
github.com./modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
18+
github.com./modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
19+
github.com./modern-go/reflect2 v1.0.2 // indirect
1920
github.com./nats-io/jwt/v2 v2.7.3 // indirect
2021
github.com./nats-io/nkeys v0.4.9 // indirect
2122
github.com./nats-io/nuid v1.0.1 // indirect
22-
github.com./stretchr/testify v1.9.0 // indirect
23-
go.uber.org/atomic v1.4.0 // indirect
24-
go.uber.org/multierr v1.1.0 // indirect
23+
github.com./stretchr/testify v1.10.0 // indirect
24+
go.uber.org/atomic v1.9.0 // indirect
25+
go.uber.org/multierr v1.9.0 // indirect
2526
go.uber.org/zap v1.10.0 // indirect
2627
golang.org/x/crypto v0.31.0 // indirect
2728
golang.org/x/sys v0.28.0 // indirect

x/wasmbus/go.sum

+16-17
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,22 @@ github.com./davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
55
github.com./davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
66
github.com./goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg=
77
github.com./goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
8-
github.com./google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
9-
github.com./google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
8+
github.com./google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
9+
github.com./google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1010
github.com./google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
11-
github.com./google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
12-
github.com./google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13-
github.com./json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
14-
github.com./json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
11+
github.com./google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
12+
github.com./google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
13+
github.com./json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
14+
github.com./json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
1515
github.com./klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
1616
github.com./klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
1717
github.com./minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q=
1818
github.com./minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
19-
github.com./modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
2019
github.com./modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
21-
github.com./modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
22-
github.com./modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
20+
github.com./modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
21+
github.com./modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
22+
github.com./modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
23+
github.com./modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
2324
github.com./nats-io/jwt/v2 v2.7.3 h1:6bNPK+FXgBeAqdj4cYQ0F8ViHRbi7woQLq4W29nUAzE=
2425
github.com./nats-io/jwt/v2 v2.7.3/go.mod h1:GvkcbHhKquj3pkioy5put1wvPxs78UlZ7D/pY+BgZk4=
2526
github.com./nats-io/nats-server/v2 v2.10.24 h1:KcqqQAD0ZZcG4yLxtvSFJY7CYKVYlnlWoAiVZ6i/IY4=
@@ -36,14 +37,14 @@ github.com./pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
3637
github.com./pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
3738
github.com./stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
3839
github.com./stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
39-
github.com./stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
40-
github.com./stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
40+
github.com./stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
41+
github.com./stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
4142
github.com./valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
4243
github.com./valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
43-
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
44-
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
45-
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
46-
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
44+
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
45+
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
46+
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
47+
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
4748
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
4849
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
4950
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
@@ -53,7 +54,5 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
5354
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
5455
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
5556
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
56-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
57-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
5857
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
5958
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

x/wasmbus/policy/api.go

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package policy
2+
3+
import (
4+
"context"
5+
"fmt"
6+
)
7+
8+
var (
9+
ErrProtocol = fmt.Errorf("encoding error")
10+
ErrInternal = fmt.Errorf("internal error")
11+
)
12+
13+
type API interface {
14+
// PerformInvocation is called when a component is invoked
15+
PerformInvocation(ctx context.Context, req *PerformInvocationRequest) (*Response, error)
16+
// StartComponent is called when a component is started
17+
StartComponent(ctx context.Context, req *StartComponentRequest) (*Response, error)
18+
// StartProvider is called when a provider is started
19+
StartProvider(ctx context.Context, req *StartProviderRequest) (*Response, error)
20+
}
21+
22+
var _ API = (*APIMock)(nil)
23+
24+
type APIMock struct {
25+
PerformInvocationFunc func(ctx context.Context, req *PerformInvocationRequest) (*Response, error)
26+
StartComponentFunc func(ctx context.Context, req *StartComponentRequest) (*Response, error)
27+
StartProviderFunc func(ctx context.Context, req *StartProviderRequest) (*Response, error)
28+
}
29+
30+
func (m *APIMock) PerformInvocation(ctx context.Context, req *PerformInvocationRequest) (*Response, error) {
31+
return m.PerformInvocationFunc(ctx, req)
32+
}
33+
34+
func (m *APIMock) StartComponent(ctx context.Context, req *StartComponentRequest) (*Response, error) {
35+
return m.StartComponentFunc(ctx, req)
36+
}
37+
38+
func (m *APIMock) StartProvider(ctx context.Context, req *StartProviderRequest) (*Response, error) {
39+
return m.StartProviderFunc(ctx, req)
40+
}
41+
42+
// Request is the structure of the request sent to the policy engine
43+
type BaseRequest[T any] struct {
44+
Id string `json:"requestId"`
45+
Kind string `json:"kind"`
46+
Version string `json:"version"`
47+
Host Host `json:"host"`
48+
Request T `json:"request"`
49+
}
50+
51+
// Decision is a helper function to create a response
52+
func (r BaseRequest[T]) Decision(allowed bool, msg string) *Response {
53+
return &Response{
54+
Id: r.Id,
55+
Permitted: allowed,
56+
Message: msg,
57+
}
58+
}
59+
60+
// Deny is a helper function to create a response with a deny decision
61+
func (r BaseRequest[T]) Deny(msg string) *Response {
62+
return r.Decision(false, msg)
63+
}
64+
65+
// Allow is a helper function to create a response with an allow decision
66+
func (r BaseRequest[T]) Allow(msg string) *Response {
67+
return r.Decision(true, msg)
68+
}
69+
70+
// Response is the structure of the response sent by the policy engine
71+
type Response struct {
72+
Id string `json:"requestId"`
73+
Permitted bool `json:"permitted"`
74+
Message string `json:"message,omitempty"`
75+
}
76+
77+
type Claims struct {
78+
PublicKey string `json:"publicKey"`
79+
Issuer string `json:"issuer"`
80+
IssuedAt int `json:"issuedAt"`
81+
ExpiresAt int `json:"expiresAt"`
82+
Expired bool `json:"expired"`
83+
}
84+
85+
type StartComponentPayload struct {
86+
ComponentId string `json:"componentId"`
87+
ImageRef string `json:"imageRef"`
88+
MaxInstances int `json:"maxInstances"`
89+
Annotations map[string]string `json:"annotations"`
90+
}
91+
92+
type StartComponentRequest = BaseRequest[StartComponentPayload]
93+
94+
type StartProviderPayload struct {
95+
ProviderId string `json:"providerId"`
96+
ImageRef string `json:"imageRef"`
97+
Annotations map[string]string `json:"annotations"`
98+
}
99+
100+
type StartProviderRequest = BaseRequest[StartProviderPayload]
101+
102+
type PerformInvocationPayload struct {
103+
Interface string `json:"interface"`
104+
Function string `json:"function"`
105+
// NOTE(lxf): this covers components but not providers. wut?!?
106+
Target InvocationTarget `json:"target"`
107+
}
108+
109+
type PerformInvocationRequest = BaseRequest[PerformInvocationPayload]
110+
111+
type InvocationTarget struct {
112+
ComponentId string `json:"componentId"`
113+
ImageRef string `json:"imageRef"`
114+
MaxInstances int `json:"maxInstances"`
115+
Annotations map[string]string `json:"annotations"`
116+
}
117+
118+
type Host struct {
119+
PublicKey string `json:"publicKey"`
120+
Lattice string `json:"lattice"`
121+
Labels map[string]string `json:"labels"`
122+
}

0 commit comments

Comments
 (0)