diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..d608af42 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# build hmruntime binary +FROM golang:alpine as runtime-builder +WORKDIR /src +COPY hmruntime/ ./ +RUN go build . + +# build example plugin +FROM node:20-alpine as plugin-builder +WORKDIR /src +COPY plugins/as/ ./ +WORKDIR /src/hmplugin1 +RUN npm install +RUN npm run build:release + +# build runtime image +FROM ubuntu:20.04 +LABEL maintainer="Hypermode " +COPY --from=runtime-builder /src/hmruntime /usr/bin/hmruntime +COPY --from=plugin-builder /src/hmplugin1/build/release.wasm /plugins/hmplugin1.wasm + +ENTRYPOINT ["hmruntime", "--plugins=/plugins"] diff --git a/hmruntime/Dockerfile b/hmruntime/Dockerfile deleted file mode 100644 index 25cfd35d..00000000 --- a/hmruntime/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM golang:alpine as builder -LABEL maintainer="Hypermode " - -# build hmruntime binary -WORKDIR /src -COPY go.mod go.sum ./ -COPY *.go ./ -RUN go build . - -FROM ubuntu:20.04 - -COPY --from=builder /src/hmruntime /usr/bin/hmruntime -CMD ["hmruntime"] diff --git a/hmruntime/README.md b/hmruntime/README.md index 95e5a82f..99a0e1d4 100644 --- a/hmruntime/README.md +++ b/hmruntime/README.md @@ -14,13 +14,16 @@ The following must be installed on your development workstation or build server: To build the Hypermode runtime server: `go build` -To build the docker image: `docker build -t hmruntime .` +To build the docker image, from the root directory: `docker build -t hypermode/runtime .` ## Running - To run the compiled program, invoke the `hmruntime` binary. - To run from code (while developing), use `go run .` instead. -- To run using docker containers, use `docker run -p 8686:8686 -v :/plugins hmruntime:latest hmruntime --dgraph=http://host.docker.internal:8080`. +- To run using docker containers, first decide where plugins will be loaded from. + - To just use the default `hmplugin1` sample plugin, use `docker run -p 8686:8686 hypermode/runtime --dgraph=http://host.docker.internal:8080`. + - Or, mount a plugins directory on the host, use `docker run -p 8686:8686 -v :/plugins hypermode/runtime --dgraph=http://host.docker.internal:8080`. + - For example `-v ./plugins/as:/plugins` ## Notes diff --git a/hmruntime/main.go b/hmruntime/main.go index 8fdcd1b4..7f9669d3 100644 --- a/hmruntime/main.go +++ b/hmruntime/main.go @@ -32,6 +32,7 @@ var compiledModules = make(map[string]wazero.CompiledModule) var functionsMap = make(map[string]functionInfo) var dgraphUrl *string +var pluginsPath *string func main() { ctx := context.Background() @@ -39,6 +40,7 @@ func main() { // Parse command-line flags var port = flag.Int("port", 8686, "The HTTP port to listen on.") dgraphUrl = flag.String("dgraph", "http://localhost:8080", "The Dgraph url to connect to.") + pluginsPath = flag.String("plugins", "../plugins/as", "The path to the plugins directory.") flag.Parse() // Initialize the WebAssembly runtime @@ -71,22 +73,45 @@ func main() { } func loadPlugins(ctx context.Context) error { + entries, err := os.ReadDir(*pluginsPath) + if err != nil { + return fmt.Errorf("failed to read plugins directory: %v", err) + } + + for _, entry := range entries { + + // Determine if the entry represents a plugin. + var pluginName string + entryName := entry.Name() + if entry.IsDir() { + pluginName = entryName + path := fmt.Sprintf("%s/%s/build/debug.wasm", *pluginsPath, pluginName) + if _, err := os.Stat(path); err != nil { + continue + } + } else if strings.HasSuffix(entryName, ".wasm") { + pluginName = strings.TrimSuffix(entryName, ".wasm") + } else { + continue + } + + // Load the plugin + err := loadPlugin(ctx, pluginName) + if err != nil { + log.Printf("Failed to load plugin '%s': %v\n", pluginName, err) + } + } - // For now, we have just one hardcoded plugin. - const pluginName = "hmplugin1" + return nil +} - // TODO: This will need work: - // - Plugins should probably be loaded from a repository, not from disk. - // - We'll need to figure out how to handle plugin updates. - // - We'll need to figure out hot/warm/cold plugin loading. +func loadPlugin(ctx context.Context, pluginName string) error { err := loadPluginModule(ctx, pluginName) if err != nil { return err } - // Temporarily, watch for changes to the plugin so we can reload it. - // TODO: Remove this when we have a better way to handle plugin updates. err = watchForPluginChanges(ctx, pluginName) if err != nil { return err @@ -169,8 +194,11 @@ func loadPluginModule(ctx context.Context, name string) error { fmt.Printf("Loading plugin '%s'\n", name) } - // TODO: Load the plugin from some repository instead of disk. - path := getPathForPlugin(name) + path, err := getPathForPlugin(name) + if err != nil { + return fmt.Errorf("failed to get path for plugin: %v", err) + } + plugin, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to load the plugin: %v", err) @@ -228,10 +256,21 @@ func getModuleInstance(ctx context.Context, pluginName string) (wasm.Module, buf return mod, buf, nil } -func getPathForPlugin(name string) string { - // TODO: Decide whether to load the plugin in debug or release mode. - // For now use debug, so we get better stack traces on errors. - return "../plugins/as/" + name + "/build/debug.wasm" +func getPathForPlugin(name string) (string, error) { + + // Normally the plugin will be directly in the plugins directory, by filename. + path := *pluginsPath + "/" + name + ".wasm" + if _, err := os.Stat(path); err == nil { + return path, nil + } + + // For local development, the plugin will be in a subdirectory and we'll use the debug.wasm file. + path = *pluginsPath + "/" + name + "/build/debug.wasm" + if _, err := os.Stat(path); err == nil { + return path, nil + } + + return "", fmt.Errorf("compiled wasm file not found for plugin '%s'", name) } func watchForPluginChanges(ctx context.Context, name string) error { @@ -269,8 +308,12 @@ func watchForPluginChanges(ctx context.Context, name string) error { } }() - path := getPathForPlugin(name) - err := w.Add(path) + path, err := getPathForPlugin(name) + if err != nil { + return fmt.Errorf("failed to get path for plugin: %v", err) + } + + err = w.Add(path) if err != nil { return fmt.Errorf("failed to watch plugin file: %v", err) }