feat: stub basic control plane functionality (#9)

Stub's out some basic control plane functionality. This also required
setting up some Source and Tools initialization.
This commit is contained in:
Kurtis Van Gent
2024-09-20 14:12:16 -06:00
committed by GitHub
parent b9ba364fb6
commit 336bdc4d56
13 changed files with 192 additions and 15 deletions

View File

@@ -116,7 +116,10 @@ func run(cmd *Command) error {
}
// run server
s := server.NewServer(cmd.cfg)
s, err := server.NewServer(cmd.cfg)
if err != nil {
return fmt.Errorf("Toolbox failed to start with the following error: %w", err)
}
err = s.ListenAndServe(ctx)
if err != nil {
return fmt.Errorf("Toolbox crashed with the following error: %w", err)

View File

@@ -196,6 +196,7 @@ func TestParseToolFile(t *testing.T) {
`,
wantSources: sources.Configs{
"my-pg-instance": sources.CloudSQLPgConfig{
Name: "my-pg-instance",
Kind: sources.CloudSQLPgKind,
Project: "my-project",
Region: "my-region",
@@ -205,6 +206,7 @@ func TestParseToolFile(t *testing.T) {
},
wantTools: tools.Configs{
"example_tool": tools.CloudSQLPgGenericConfig{
Name: "example_tool",
Kind: tools.CloudSQLPgSQLGenericKind,
Source: "my-pg-instance",
Description: "some description",

2
go.mod
View File

@@ -4,12 +4,14 @@ go 1.22.2
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/go-chi/render v1.0.3
github.com/google/go-cmp v0.6.0
github.com/spf13/cobra v1.8.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/ajg/form v1.5.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)

4
go.sum
View File

@@ -1,6 +1,10 @@
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=

61
internal/server/api.go Normal file
View File

@@ -0,0 +1,61 @@
// Copyright 2024 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// apiRouter creates a router that represents the routes under /api
func apiRouter(s *Server) chi.Router {
r := chi.NewRouter()
r.Get("/toolset/{toolsetName}", toolsetHandler(s))
// TODO: make this POST
r.Get("/tool/{toolName}", toolHandler(s))
return r
}
func toolsetHandler(s *Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
toolsetName := chi.URLParam(r, "toolsetName")
_, _ = w.Write([]byte(fmt.Sprintf("Stub for toolset %s manifest!", toolsetName)))
}
}
func toolHandler(s *Server) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
toolName := chi.URLParam(r, "toolName")
tool, ok := s.tools[toolName]
if !ok {
render.Status(r, http.StatusInternalServerError)
_, _ = w.Write([]byte(fmt.Sprintf("Tool %q does not exist", toolName)))
return
}
res, err := tool.Invoke()
if err != nil {
render.Status(r, http.StatusInternalServerError)
return
}
_, _ = w.Write([]byte(fmt.Sprintf("Tool Result: %s", res)))
}
}

View File

@@ -24,28 +24,60 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/googleapis/genai-toolbox/internal/sources"
"github.com/googleapis/genai-toolbox/internal/tools"
)
// Server contains info for running an instance of Toolbox. Should be instantiated with NewServer().
type Server struct {
conf Config
router chi.Router
conf Config
root chi.Router
sources map[string]sources.Source
tools map[string]tools.Tool
}
// NewServer returns a Server object based on provided Config.
func NewServer(cfg Config) *Server {
func NewServer(cfg Config) (*Server, error) {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("🧰 Hello world! 🧰"))
})
s := &Server{
conf: cfg,
router: r,
// initalize and validate the sources
sources := make(map[string]sources.Source)
for name, sc := range cfg.SourceConfigs {
s, err := sc.Initialize()
if err != nil {
return nil, fmt.Errorf("Unable to initialize tool %s: %w", name, err)
}
sources[name] = s
}
return s
fmt.Printf("Initalized %d sources.\n", len(sources))
// initalize and validate the tools
tools := make(map[string]tools.Tool)
for name, tc := range cfg.ToolConfigs {
t, err := tc.Initialize(sources)
if err != nil {
return nil, fmt.Errorf("Unable to initialize tool %s: %w", name, err)
}
tools[name] = t
}
fmt.Printf("Initalized %d tools.\n", len(tools))
s := &Server{
conf: cfg,
root: r,
sources: sources,
tools: tools,
}
r.Mount("/api", apiRouter(s))
return s, nil
}
// ListenAndServe starts an HTTP server for the given Server instance.
@@ -60,5 +92,5 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
return fmt.Errorf("failed to open listener for %q: %w", addr, err)
}
return http.Serve(l, s.router)
return http.Serve(l, s.root)
}

View File

@@ -47,7 +47,10 @@ func TestServe(t *testing.T) {
Address: addr,
Port: port,
}
s := server.NewServer(cfg)
s, err := server.NewServer(cfg)
if err != nil {
t.Fatalf("Unable initialize server!")
}
// start server in background
errCh := make(chan error)

View File

@@ -20,6 +20,7 @@ const CloudSQLPgKind string = "cloud-sql-postgres"
var _ Config = CloudSQLPgConfig{}
type CloudSQLPgConfig struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Project string `yaml:"project"`
Region string `yaml:"region"`
@@ -30,3 +31,18 @@ type CloudSQLPgConfig struct {
func (r CloudSQLPgConfig) sourceKind() string {
return CloudSQLPgKind
}
func (r CloudSQLPgConfig) Initialize() (Source, error) {
s := CloudSQLPgSource{
Name: r.Name,
Kind: CloudSQLPgKind,
}
return s, nil
}
var _ Source = CloudSQLPgSource{}
type CloudSQLPgSource struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
}

View File

@@ -42,6 +42,7 @@ func TestParseFromYaml(t *testing.T) {
`,
want: sources.Configs{
"my-pg-instance": sources.CloudSQLPgConfig{
Name: "my-pg-instance",
Kind: sources.CloudSQLPgKind,
Project: "my-project",
Region: "my-region",

View File

@@ -22,6 +22,7 @@ import (
type Config interface {
sourceKind() string
Initialize() (Source, error)
}
// validate interface
@@ -48,7 +49,7 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
}
switch k.Kind {
case CloudSQLPgKind:
actual := CloudSQLPgConfig{}
actual := CloudSQLPgConfig{Name: name}
if err := n.Decode(&actual); err != nil {
return fmt.Errorf("unable to parse as %q: %w", k.Kind, err)
}
@@ -60,3 +61,6 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
}
return nil
}
type Source interface {
}

View File

@@ -14,12 +14,19 @@
package tools
import (
"fmt"
"github.com/googleapis/genai-toolbox/internal/sources"
)
const CloudSQLPgSQLGenericKind string = "cloud-sql-postgres-generic"
// validate interface
var _ Config = CloudSQLPgGenericConfig{}
type CloudSQLPgGenericConfig struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Source string `yaml:"source"`
Description string `yaml:"description"`
@@ -30,3 +37,38 @@ type CloudSQLPgGenericConfig struct {
func (r CloudSQLPgGenericConfig) toolKind() string {
return CloudSQLPgSQLGenericKind
}
func (r CloudSQLPgGenericConfig) Initialize(srcs map[string]sources.Source) (Tool, error) {
// verify source exists
rawS, ok := srcs[r.Source]
if !ok {
return nil, fmt.Errorf("No source named %q configured!", r.Source)
}
// verify the source is the right kind
s, ok := rawS.(sources.CloudSQLPgSource)
if !ok {
return nil, fmt.Errorf("Sources for %q tools must be of kind %q!", CloudSQLPgSQLGenericKind, sources.CloudSQLPgKind)
}
// finish tool setup
t := CloudSQLPgGenericTool{
Name: r.Name,
Kind: CloudSQLPgSQLGenericKind,
Source: s,
}
return t, nil
}
// validate interface
var _ Tool = CloudSQLPgGenericTool{}
type CloudSQLPgGenericTool struct {
Name string `yaml:"name"`
Kind string `yaml:"kind"`
Source sources.CloudSQLPgSource
}
func (t CloudSQLPgGenericTool) Invoke() (string, error) {
return fmt.Sprintf("Stub tool call for %q!", t.Name), nil
}

View File

@@ -46,6 +46,7 @@ func TestParseFromYaml(t *testing.T) {
`,
want: tools.Configs{
"example_tool": tools.CloudSQLPgGenericConfig{
Name: "example_tool",
Kind: tools.CloudSQLPgSQLGenericKind,
Source: "my-pg-instance",
Description: "some description",

View File

@@ -17,16 +17,18 @@ package tools
import (
"fmt"
"github.com/googleapis/genai-toolbox/internal/sources"
"gopkg.in/yaml.v3"
)
// SourceConfigs is a type used to allow unmarshal of the data source config map
type Configs map[string]Config
type Config interface {
toolKind() string
Initialize(map[string]sources.Source) (Tool, error)
}
// SourceConfigs is a type used to allow unmarshal of the data source config map
type Configs map[string]Config
// validate interface
var _ yaml.Unmarshaler = &Configs{}
@@ -48,7 +50,7 @@ func (c *Configs) UnmarshalYAML(node *yaml.Node) error {
}
switch k.Kind {
case CloudSQLPgSQLGenericKind:
actual := CloudSQLPgGenericConfig{}
actual := CloudSQLPgGenericConfig{Name: name}
if err := n.Decode(&actual); err != nil {
return fmt.Errorf("unable to parse as %q: %w", k.Kind, err)
}
@@ -67,3 +69,7 @@ type Parameter struct {
Description string `yaml:"description"`
Required bool `yaml:"required"`
}
type Tool interface {
Invoke() (string, error)
}