feat: add basic http server (#6)

This commit is contained in:
Kurtis Van Gent
2024-07-26 16:59:01 -05:00
committed by GitHub
parent 1539ee56dd
commit e09ae30a90
7 changed files with 177 additions and 5 deletions

View File

@@ -15,10 +15,13 @@
package cmd
import (
"context"
_ "embed"
"fmt"
"os"
"strings"
"github.com/googleapis/genai-toolbox/internal/server"
"github.com/spf13/cobra"
)
@@ -55,6 +58,7 @@ func Execute() {
// Command represents an invocation of the CLI.
type Command struct {
*cobra.Command
cfg server.Config
}
// NewCommand returns a Command object representing an invocation of the CLI.
@@ -62,17 +66,27 @@ func NewCommand() *Command {
rootCmd := &cobra.Command{
Use: "toolbox",
Version: versionString,
RunE: root,
}
c := &Command{
Command: rootCmd,
cfg: server.Config{},
}
// wrap RunE command so that we have access to original Command object
rootCmd.RunE = func(*cobra.Command, []string) error { return run(c) }
return c
}
func root(cmd *cobra.Command, args []string) error {
cmd.Printf("Toolbox running v%s!", versionString)
func run(cmd *Command) error {
ctx := context.Background()
// run server
s := server.NewServer(cmd.cfg)
err := s.ListenAndServe(ctx)
if err != nil {
return fmt.Errorf("Error while serving: %w", err)
}
return nil
}

View File

@@ -52,5 +52,4 @@ func TestVersion(t *testing.T) {
if !strings.Contains(got, want) {
t.Errorf("cli did not return correct version: want %q, got %q", want, got)
}
}

5
go.mod
View File

@@ -2,7 +2,10 @@ module github.com/googleapis/genai-toolbox
go 1.22.2
require github.com/spf13/cobra v1.8.1
require (
github.com/go-chi/chi/v5 v5.1.0
github.com/spf13/cobra v1.8.1
)
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect

2
go.sum
View File

@@ -1,4 +1,6 @@
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

29
internal/server/config.go Normal file
View File

@@ -0,0 +1,29 @@
// 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
type Config struct {
// Address is the address of the interface the server will listen on.
Address string
// Port is the port the server will listen on.
Port string
}
func NewConfig() Config {
c := Config{
Address: "127.0.0.1",
Port: "5000",
}
return c
}

63
internal/server/server.go Normal file
View File

@@ -0,0 +1,63 @@
// 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 (
"context"
"fmt"
"net"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
// Server contains info for running an instance of Toolbox. Should be instantiated with NewServer().
type Server struct {
conf Config
router chi.Router
}
// NewServer returns a Server object based on provided Config.
func NewServer(conf Config) *Server {
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: conf,
router: r,
}
return s
}
// ListenAndServe starts an HTTP server for the given Server instance.
func (s *Server) ListenAndServe(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
addr := net.JoinHostPort(s.conf.Address, s.conf.Port)
lc := net.ListenConfig{KeepAlive: 30 * time.Second}
l, err := lc.Listen(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("failed to open listener for %q: %w", addr, err)
}
return http.Serve(l, s.router)
}

View File

@@ -0,0 +1,62 @@
// 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_test
import (
"context"
"net"
"testing"
"time"
"github.com/googleapis/genai-toolbox/internal/server"
)
// tryDial is a utility function that dials an address up to 'attempts' number of times.
func tryDial(addr string, attempts int) bool {
for i := 0; i < attempts; i++ {
conn, err := net.Dial("tcp", addr)
if err != nil {
time.Sleep(100 * time.Millisecond)
continue
}
_ = conn.Close()
return true
}
return false
}
func TestListenAndServe(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
host, port := "127.0.0.1", "5000"
cfg := server.NewConfig()
s := server.NewServer(cfg)
// start server in background
errCh := make(chan error)
go func() {
err := s.ListenAndServe(ctx)
defer close(errCh)
if err != nil {
errCh <- err
}
}()
if !tryDial(net.JoinHostPort(host, port), 10) {
t.Fatalf("Unable to dial server!")
}
}