Skip to content

Commit

Permalink
feat: add inmem fs and simple builtin controller
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgeMac committed Jul 22, 2023
1 parent 525b0f4 commit 914366c
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 7 deletions.
8 changes: 3 additions & 5 deletions pkg/api/core/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package core

import (
"encoding/json"

"github.com/xeipuuv/gojsonschema"
)

// ResourceDefinition represents a definition of a particular resource Kind and its versions
Expand All @@ -22,9 +20,9 @@ type Names struct {
}

type ResourceDefinitionSpec struct {
Group string `json:"group"`
Controller ResourceDefinitionController `json:"controller"`
Versions map[string]*gojsonschema.Schema `json:"schema"`
Group string `json:"group"`
Controller ResourceDefinitionController `json:"controller"`
Versions map[string]json.RawMessage `json:"schema,omitempty"`
}

type ResourceDefinitionController struct {
Expand Down
21 changes: 20 additions & 1 deletion pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"sort"
"sync"

Expand Down Expand Up @@ -36,6 +37,7 @@ type FilesystemStore interface {
// Controller is the core controller interface for handling interactions with a
// single resource type.
type Controller interface {
Definition() *core.ResourceDefinition
Get(context.Context, *controller.GetRequest) (*core.Resource, error)
List(context.Context, *controller.ListRequest) ([]*core.Resource, error)
Put(context.Context, *controller.PutRequest) error
Expand Down Expand Up @@ -69,24 +71,41 @@ func NewServer(fs FilesystemStore) (*Server, error) {
return s, nil
}

// ServeHTTP delegates to the underlying chi.Mux router.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()

s.mux.ServeHTTP(w, r)
}

func (s *Server) RegisterController(source string, def *core.ResourceDefinition, cntl Controller) {
func (s *Server) addDefinition(source string, gvk string, def *core.ResourceDefinition) {
src, ok := s.sources[source]
if !ok {
src = map[string]*core.ResourceDefinition{}
s.sources[source] = src
}

src[gvk] = def
}

// RegisterController adds a new controller and definition for a particular source to the server.
// This potentially will happen dynamically in the future, so it is guarded with a write lock.
func (s *Server) RegisterController(source string, cntl Controller) {
s.mu.Lock()
defer s.mu.Unlock()

def := cntl.Definition()
for version := range def.Spec.Versions {
var (
version = version
prefix = fmt.Sprintf("/apis/%s/%s/%s/%s/namespaces/{ns}", source, def.Spec.Group, version, def.Names.Plural)
named = prefix + "/{name}"
)

// update sources map
s.addDefinition(source, path.Join(def.Spec.Group, version, def.Names.Kind), def)

// list kind
s.mux.Get(prefix, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := s.fs.View(r.Context(), source, s.rev, func(f controller.FSConfig) error {
Expand Down
81 changes: 81 additions & 0 deletions pkg/api/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package api_test

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.flipt.io/cup/pkg/api"
"go.flipt.io/cup/pkg/api/core"
"go.flipt.io/cup/pkg/controllers/simple"
"go.flipt.io/cup/pkg/fs/mem"
)

var testDef = &core.ResourceDefinition{
APIVersion: "cup.flipt.io/v1alpha1",
Kind: "ResourceDefinition",
Metadata: core.Metadata{
Name: "resources.test.cup.flipt.io",
},
Names: core.Names{
Kind: "Resource",
Singular: "resource",
Plural: "resources",
},
Spec: core.ResourceDefinitionSpec{
Group: "test.cup.flipt.io",
Controller: core.ResourceDefinitionController{},
Versions: map[string]json.RawMessage{
"v1alpha1": []byte("null"),
},
},
}

func Test_Server_Source(t *testing.T) {
fss := mem.New()
server, err := api.NewServer(fss)
require.NoError(t, err)

cntrl := simple.New(testDef)
server.RegisterController("cup", cntrl)

srv := httptest.NewServer(server)
t.Cleanup(srv.Close)

resp, err := http.Get(srv.URL + "/apis")
require.NoError(t, err)

defer resp.Body.Close()

var sources []string
require.NoError(t, json.NewDecoder(resp.Body).Decode(&sources))

assert.Equal(t, []string{"cup"}, sources)
}

func Test_Server_SourceDefinitions(t *testing.T) {
fss := mem.New()
server, err := api.NewServer(fss)
require.NoError(t, err)

cntrl := simple.New(testDef)
server.RegisterController("cup", cntrl)

srv := httptest.NewServer(server)
t.Cleanup(srv.Close)

resp, err := http.Get(srv.URL + "/apis/cup")
require.NoError(t, err)

defer resp.Body.Close()

var definitions map[string]*core.ResourceDefinition
require.NoError(t, json.NewDecoder(resp.Body).Decode(&definitions))

assert.Equal(t, map[string]*core.ResourceDefinition{
"test.cup.flipt.io/v1alpha1/Resource": testDef,
}, definitions)
}
File renamed without changes.
16 changes: 15 additions & 1 deletion pkg/controller/fs.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package controller

import "io/fs"
import (
"io/fs"
"os"
)

// FSConfig encapsulates the configuration required to establish the root
// directory of the wazero runtime when performing controller actions.
Expand All @@ -21,3 +24,14 @@ func NewDirFSConfig(dir string) FSConfig {
dir: &dir,
}
}

// ToFS returns either the configured fs.FS implementation or it
// adapts the desired directory into an fs.FS using os.DirFS
// depending on how the config was configured
func (c *FSConfig) ToFS() fs.FS {
if c.dir != nil {
return os.DirFS(*c.dir)
}

return c.fs
}
118 changes: 118 additions & 0 deletions pkg/controllers/simple/simple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package simple

import (
"context"
"fmt"
"io"
"io/fs"
"path"

"go.flipt.io/cup/pkg/api/core"
"go.flipt.io/cup/pkg/containers"
"go.flipt.io/cup/pkg/controller"
"go.flipt.io/cup/pkg/encoding"
)

type ResourceEncoding interface {
Extension() string
NewEncoder(io.Writer) encoding.TypedEncoder[core.Resource]
NewDecoder(io.Reader) encoding.TypedDecoder[core.Resource]
}

// Controller is mostly used for testing purposes (for now).
// It is a built-in controller implementation for cup.
// It simply organizes resources on the underlying filesystem by { namespace }/{ name }
// encoding them using the provided marshaller.
type Controller struct {
definition *core.ResourceDefinition
encoding ResourceEncoding
}

// New constructs and configures a new *Controller.
// By default it uses a JSON encoding which can be overriden via WithResourceEncoding.
func New(def *core.ResourceDefinition, opts ...containers.Option[Controller]) *Controller {
controller := &Controller{
definition: def,
encoding: encoding.NewJSONEncoding[core.Resource](),
}

containers.ApplyAll(controller, opts...)

return controller
}

// WithResourceEncoding overrides the default resource encoding.
func WithResourceEncoding(e ResourceEncoding) containers.Option[Controller] {
return func(c *Controller) {
c.encoding = e
}
}

// Definition returns the core resource definition handled by the Controller.
func (c *Controller) Definition() *core.ResourceDefinition {
return c.definition
}

func (c *Controller) Get(_ context.Context, req *controller.GetRequest) (*core.Resource, error) {
fi, err := req.FSConfig.ToFS().Open(path.Join(req.Namespace, req.Name+"."+c.encoding.Extension()))
if err != nil {
return nil, fmt.Errorf("get: %w", err)
}
defer fi.Close()

return c.encoding.NewDecoder(fi).Decode()
}

// List finds all the resources on the provided FS in the folder { namespace }
// The result set is filtered by any specified labels.
func (c *Controller) List(_ context.Context, req *controller.ListRequest) (resources []*core.Resource, _ error) {
ffs := req.FSConfig.ToFS()
return resources, fs.WalkDir(req.FSConfig.ToFS(), req.Namespace, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}

if d.IsDir() {
return fs.SkipDir
}

if ext := path.Ext(p); ext == "" || ext[1:] != c.encoding.Extension() {
// skip files without expected extension
return nil
}

fi, err := ffs.Open(p)
if err != nil {
return err
}

defer fi.Close()

resource, err := c.encoding.NewDecoder(fi).Decode()
if err != nil {
return err
}

for _, kv := range req.Labels {
// skip adding resource if any of the specified labels
// do not match as expected
if v, ok := resource.Metadata.Labels[kv[0]]; !ok || v != kv[1] {
return nil
}
}

resources = append(resources, resource)

return nil
})
}

// Put for now is a silent noop as we dont have a writable filesystem abstraction
func (c *Controller) Put(_ context.Context, _ *controller.PutRequest) error {
return nil
}

// Delete for now is a silent noop as we dont have a writable filesystem abstraction
func (c *Controller) Delete(_ context.Context, _ *controller.DeleteRequest) error {
return nil
}
26 changes: 26 additions & 0 deletions pkg/encoding/decoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package encoding

import "io"

type AnyDecoder interface {
Decode(any) error
}

type DecodeBuilder interface {
NewDecoder(io.Reader) AnyDecoder
}

type Decoder[B DecodeBuilder, T any] struct {
decoder AnyDecoder
}

func NewDecoder[B DecodeBuilder, T any](r io.Reader) Decoder[B, T] {
var b DecodeBuilder

return Decoder[B, T]{decoder: b.NewDecoder(r)}
}

func (d Decoder[B, T]) Decode() (*T, error) {
var t T
return &t, d.decoder.Decode(&t)
}
59 changes: 59 additions & 0 deletions pkg/encoding/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package encoding

import (
"io"
)

type AnyEncoder interface {
Encode(any) error
}

type EncoderBuilder interface {
NewEncoder(io.Writer) AnyEncoder
}

type Encoder[B EncoderBuilder, T any] struct {
encoder AnyEncoder
}

func NewEncoder[B EncoderBuilder, T any](w io.Writer) Encoder[B, T] {
var b EncoderBuilder

return Encoder[B, T]{encoder: b.NewEncoder(w)}
}

func (e Encoder[B, T]) Encode(t *T) error {
return e.encoder.Encode(t)
}

type AnyEncodingBuilder interface {
Extension() string
EncoderBuilder
DecodeBuilder
}

type TypedEncoder[T any] interface {
Encode(*T) error
}

type TypedDecoder[T any] interface {
Decode() (*T, error)
}

type EncodingBuilder[B AnyEncodingBuilder, T any] struct {
b B
Encoder[B, T]
Decoder[B, T]
}

func (e EncodingBuilder[B, T]) Extension() string {
return e.b.Extension()
}

func (e EncodingBuilder[B, T]) NewEncoder(w io.Writer) TypedEncoder[T] {
return NewEncoder[B, T](w)
}

func (e EncodingBuilder[B, T]) NewDecoder(r io.Reader) TypedDecoder[T] {
return NewDecoder[B, T](r)
}
Loading

0 comments on commit 914366c

Please sign in to comment.