diff --git a/.gitignore b/.gitignore index e43b0f9..be47843 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.env diff --git a/backend/executor/executor.go b/backend/executor/executor.go new file mode 100644 index 0000000..55945d5 --- /dev/null +++ b/backend/executor/executor.go @@ -0,0 +1,87 @@ +package executor + +import ( + "context" + "log" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/client" +) + +var ( + dockerClient *client.Client + containers []string +) + +const imageName = "alpine" + +func InitDockerClient() error { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + return err + } + dockerClient = cli + info, err := dockerClient.Info(context.Background()) + + if err != nil { + return err + } + + log.Printf("Docker client initialized: %s", info.Name) + + return nil +} + +func SpawnContainer(name string) (containerID string, err error) { + log.Printf("Spawning container %s\n", name) + + resp, err := dockerClient.ContainerCreate(context.Background(), &container.Config{ + Image: imageName, + Cmd: []string{"tail", "-f", "/dev/null"}, + }, nil, nil, nil, name) + + if err != nil { + return "", err + } + log.Printf("Container %s created\n", name) + + containerID = resp.ID + if err := dockerClient.ContainerStart(context.Background(), containerID, container.StartOptions{}); err != nil { + return "", err + } + log.Printf("Container %s started\n", name) + + containers = append(containers, containerID) + return containerID, nil +} + +func StopContainer(containerID string) error { + if err := dockerClient.ContainerStop(context.Background(), containerID, container.StopOptions{}); err != nil { + return err + } + log.Printf("Container %s stopped\n", containerID) + return nil +} + +func DeleteContainer(containerID string) error { + if err := StopContainer(containerID); err != nil { + return err + } + + if err := dockerClient.ContainerRemove(context.Background(), containerID, container.RemoveOptions{}); err != nil { + return err + } + log.Printf("Container %s removed\n", containerID) + return nil +} + +func Cleanup() error { + log.Println("Cleaning up containers") + + for _, containerID := range containers { + if err := DeleteContainer(containerID); err != nil { + return err + } + } + return nil +} diff --git a/backend/graph/schema.resolvers.go b/backend/graph/schema.resolvers.go index de21fbf..55ce564 100644 --- a/backend/graph/schema.resolvers.go +++ b/backend/graph/schema.resolvers.go @@ -10,6 +10,7 @@ import ( "errors" "fmt" + "github.com/semanser/ai-coder/executor" gmodel "github.com/semanser/ai-coder/graph/model" "github.com/semanser/ai-coder/models" "gorm.io/datatypes" @@ -28,6 +29,12 @@ func (r *mutationResolver) CreateFlow(ctx context.Context) (*gmodel.Flow, error) return nil, tx.Error } + _, err := executor.SpawnContainer("flow-" + fmt.Sprint(flow.ID)) + + if err != nil { + return nil, fmt.Errorf("failed to spawn container: %w", err) + } + return &gmodel.Flow{ ID: flow.ID, Name: flow.Name, diff --git a/backend/main.go b/backend/main.go index e0d2601..1e3d413 100644 --- a/backend/main.go +++ b/backend/main.go @@ -4,20 +4,26 @@ import ( "log" "net/http" "os" + "os/signal" + "syscall" "gorm.io/driver/postgres" "gorm.io/gorm" + "github.com/semanser/ai-coder/executor" "github.com/semanser/ai-coder/models" ) const defaultPort = "8080" func main() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + dsn := "postgresql://postgres@localhost/ai-coder" db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { - panic("failed to connect database") + log.Fatalf("failed to connect database: %v", err) } // Migrate the schema @@ -30,7 +36,27 @@ func main() { r := newRouter(db) - // Run the server - log.Printf("connect to http://localhost:%s/playground for GraphQL playground", port) - log.Fatal(http.ListenAndServe(":"+port, r)) + err = executor.InitDockerClient() + if err != nil { + log.Fatalf("failed to initialize Docker client: %v", err) + } + + // Run the server in a separate goroutine + go func() { + log.Printf("connect to http://localhost:%s/playground for GraphQL playground", port) + if err := http.ListenAndServe(":"+port, r); err != nil { + log.Fatalf("HTTP server error: %v", err) + } + }() + + // Wait for termination signal + <-sigChan + log.Println("Shutting down...") + + // Cleanup resources + if err := executor.Cleanup(); err != nil { + log.Printf("Error during cleanup: %v", err) + } + + log.Println("Shutdown complete") } diff --git a/backend/router.go b/backend/router.go index 5d35810..ef732e2 100644 --- a/backend/router.go +++ b/backend/router.go @@ -22,7 +22,7 @@ func newRouter(db *gorm.DB) *gin.Engine { // Configure CORS middleware config := cors.DefaultConfig() // TODO change to only allow specific origins - config.AllowOrigins = []string{"*"} + config.AllowAllOrigins = true config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} r.Use(cors.New(config)) diff --git a/frontend/src/graphql.ts b/frontend/src/graphql.ts index 20c914a..e3d130f 100644 --- a/frontend/src/graphql.ts +++ b/frontend/src/graphql.ts @@ -36,7 +36,7 @@ const wsClient = createWSClient({ }); export const graphqlClient = createClient({ - url: "http://" + import.meta.env.VITE_API_URL + "/graphql", + url: window.location.origin + "/graphql", fetchOptions: {}, exchanges: [ devtoolsExchange, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 094b950..69b8144 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,6 +6,11 @@ import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react(), vanillaExtractPlugin()], + server: { + proxy: { + "/graphql": "http://localhost:8080", + }, + }, resolve: { alias: { "@/generated": path.resolve(__dirname, "./generated"),