From ef1b68f6b75d7db00ceb80e630cc5a190ea3a305 Mon Sep 17 00:00:00 2001 From: Stephen Cathcart Date: Thu, 22 Feb 2024 09:45:42 +0000 Subject: [PATCH] Initial BenchKit backend implementation (#566) * added benchkit --- benchkit-backend/backend.go | 204 ++++++++++++++++ benchkit-backend/config.go | 78 ++++++ benchkit-backend/main.go | 40 ++++ benchkit-backend/workload.go | 450 +++++++++++++++++++++++++++++++++++ benchkit/Dockerfile | 23 ++ 5 files changed, 795 insertions(+) create mode 100644 benchkit-backend/backend.go create mode 100644 benchkit-backend/config.go create mode 100644 benchkit-backend/main.go create mode 100644 benchkit-backend/workload.go create mode 100644 benchkit/Dockerfile diff --git a/benchkit-backend/backend.go b/benchkit-backend/backend.go new file mode 100644 index 00000000..bedb56f1 --- /dev/null +++ b/benchkit-backend/backend.go @@ -0,0 +1,204 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + cfg "github.com/neo4j/neo4j-go-driver/v5/neo4j/config" + "github.com/neo4j/neo4j-go-driver/v5/neo4j/log" + "net/http" + "strings" +) + +var ctx = context.Background() + +type backend struct { + config config + driver neo4j.DriverWithContext + workloads workloads +} + +func (b *backend) readyHandler(w http.ResponseWriter, r *http.Request) { + b.ready(w, r) +} + +func (b *backend) workloadHandler(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + b.postWorkload(w, r) + case http.MethodPut: + b.putWorkload(w, r) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (b *backend) workloadWithIdHandler(w http.ResponseWriter, r *http.Request) { + // Extract the part of the URL path after "/workload/" + workloadId := strings.TrimPrefix(r.URL.Path, "/workload/") + if workloadId == "" { + http.Error(w, "invalid {workloadId}", http.StatusBadRequest) + return + } + + // Proceed based on the method + switch r.Method { + case http.MethodGet: + b.getWorkload(w, r, workloadId) + case http.MethodPatch: + b.patchWorkload(w, r, workloadId) + case http.MethodDelete: + b.deleteWorkload(w, r, workloadId) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (b *backend) ready(w http.ResponseWriter, r *http.Request) { + // Create driver and verify connectivity + err := b.createDriver() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Write response + w.WriteHeader(http.StatusOK) +} + +func (b *backend) postWorkload(w http.ResponseWriter, r *http.Request) { + var request workload + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + wl, err := newWorkload(request.Method, request.Queries, request.Database, request.Routing, request.Mode) + if err != nil { + http.Error(w, fmt.Sprintf("failed to create workload: %v", err), http.StatusBadRequest) + return + } + + // Store the workload + id := b.workloads.store(wl) + + // Set the location header + w.Header().Set("Location", fmt.Sprintf("/workload/%s", id)) + w.WriteHeader(http.StatusCreated) +} + +func (b *backend) putWorkload(w http.ResponseWriter, r *http.Request) { + var request workload + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + wl, err := newWorkload(request.Method, request.Queries, request.Database, request.Routing, request.Mode) + if err != nil { + http.Error(w, fmt.Sprintf("failed to create workload: %v", err), http.StatusBadRequest) + return + } + + // Execute workload + err = wl.execute(b.driver) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Write response + w.WriteHeader(http.StatusNoContent) +} + +func (b *backend) getWorkload(w http.ResponseWriter, r *http.Request, workloadId string) { + // Get workload from our store + workloadFromStore, ok := b.workloads.fetch(workloadId) + if !ok { + http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound) + return + } + + // Execute workload + err := workloadFromStore.execute(b.driver) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Write response + w.WriteHeader(http.StatusNoContent) +} + +func (b *backend) patchWorkload(w http.ResponseWriter, r *http.Request, workloadId string) { + var request workload + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "invalid request body", http.StatusBadRequest) + return + } + + // Get workload from our store + storedWorkload, ok := b.workloads.fetch(workloadId) + if !ok { + http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound) + return + } + + // Patch the stored workload with the requested workload. + if err := storedWorkload.patch(&request); err != nil { + http.Error(w, fmt.Sprintf("failed to update workload: %v", err), http.StatusBadRequest) + return + } + + // Write response + w.WriteHeader(http.StatusOK) +} + +func (b *backend) deleteWorkload(w http.ResponseWriter, r *http.Request, workloadId string) { + // Delete workload from our store + ok := b.workloads.delete(workloadId) + if !ok { + http.Error(w, fmt.Sprintf("workload {%s} not found", workloadId), http.StatusNotFound) + return + } + + // Write response + w.WriteHeader(http.StatusNoContent) +} + +func (b *backend) createDriver() error { + if b.driver == nil { + uri := fmt.Sprintf("%s://%s:%d", b.config.neo4jScheme, b.config.neo4jHost, b.config.neo4jPort) + driver, err := neo4j.NewDriverWithContext(uri, neo4j.BasicAuth(b.config.neo4jUser, b.config.neo4jPass, ""), func(config *cfg.Config) { + if b.config.driverDebug { + config.Log = log.ToConsole(log.DEBUG) + } + }) + if err != nil { + return fmt.Errorf("failed to create Neo4j driver: %w", err) + } + if err = driver.VerifyConnectivity(ctx); err != nil { + return fmt.Errorf("failed to connect to Neo4j: %w", err) + } + b.driver = driver + } + return nil +} diff --git a/benchkit-backend/config.go b/benchkit-backend/config.go new file mode 100644 index 00000000..69dbbbc0 --- /dev/null +++ b/benchkit-backend/config.go @@ -0,0 +1,78 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +import ( + "log" + "os" + "strconv" +) + +type config struct { + backendPort int + neo4jHost string + neo4jPort int + neo4jScheme string + neo4jUser string + neo4jPass string + driverDebug bool +} + +func makeConfig() config { + return config{ + backendPort: getEnv("TEST_BACKEND_PORT", 9000), + neo4jHost: getEnv("TEST_NEO4J_HOST", "localhost"), + neo4jPort: getEnv("TEST_NEO4J_PORT", 7687), + neo4jScheme: getEnv("TEST_NEO4J_SCHEME", "neo4j"), + neo4jUser: getEnv("TEST_NEO4J_USER", "neo4j"), + neo4jPass: getEnv("TEST_NEO4J_PASS", "password"), + driverDebug: getEnv("TEST_DRIVER_DEBUG", false), + } +} + +// getEnv is a generic function to get an environment variable and convert it to the type T. +// defaultValue is used if the environment variable is not set or if conversion fails. +func getEnv[T any](key string, defaultValue T) T { + valueStr, exists := os.LookupEnv(key) + if !exists { + return defaultValue + } + + var value T + switch any(value).(type) { + case string: + return any(valueStr).(T) + case int: + intValue, err := strconv.Atoi(valueStr) + if err != nil { + log.Printf("Warning: Failed to convert %s to int, using default value. Error: %v", key, err) + return defaultValue + } + return any(intValue).(T) + case bool: + boolValue, err := strconv.ParseBool(valueStr) + if err != nil { + log.Printf("Warning: Failed to convert %s to bool, using default value. Error: %v", key, err) + return defaultValue + } + return any(boolValue).(T) + default: + log.Printf("Warning: Unsupported type for environment variable conversion.") + return defaultValue + } +} diff --git a/benchkit-backend/main.go b/benchkit-backend/main.go new file mode 100644 index 00000000..cdefe27e --- /dev/null +++ b/benchkit-backend/main.go @@ -0,0 +1,40 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +import ( + "fmt" + "log" + "net/http" +) + +func main() { + // Create backend and initialize configuration + backend := &backend{config: makeConfig()} + + // Define endpoints + http.HandleFunc("/ready", backend.readyHandler) + http.HandleFunc("/workload", backend.workloadHandler) + http.HandleFunc("/workload/", backend.workloadWithIdHandler) + + // Start server + log.Printf("Starting server on port %d", backend.config.backendPort) + if err := http.ListenAndServe(fmt.Sprintf(":%d", backend.config.backendPort), nil); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} diff --git a/benchkit-backend/workload.go b/benchkit-backend/workload.go new file mode 100644 index 00000000..61ed9342 --- /dev/null +++ b/benchkit-backend/workload.go @@ -0,0 +1,450 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [https://neo4j.com] + * + * 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 + * + * https://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 main + +import ( + "errors" + "fmt" + "github.com/neo4j/neo4j-go-driver/v5/neo4j" + "strconv" + "sync" +) + +type workloads struct { + id int + workloads map[string]*workload +} + +type workload struct { + Method *string `json:"method,omitempty"` + Queries []workloadQuery `json:"queries,omitempty"` + Database *string `json:"database,omitempty"` + Routing *string `json:"routing,omitempty"` // Temporarily store AccessMode as string + Mode *string `json:"mode,omitempty"` + AccessMode neo4j.AccessMode `json:"-"` // Ignore during JSON decoding + +} + +type workloadQuery struct { + Text string `json:"text"` + Parameters map[string]interface{} `json:"parameters"` +} + +const ( + executeQueryParallelSessions = "executeQuery_parallelSessions" + executeQuerySequentialSessions = "executeQuery_sequentialSessions" + sessionRunParallelSessions = "sessionRun_parallelSessions" + sessionRunSequentialSessions = "sessionRun_sequentialSessions" + sessionRunSequentialTransactions = "sessionRun_sequentialTransactions" + executeReadParallelSessions = "executeRead_parallelSessions" + executeReadSequentialSessions = "executeRead_sequentialSessions" + executeReadSequentialTransactions = "executeRead_sequentialTransactions" + executeReadSequentialQueries = "executeRead_sequentialQueries" + executeWriteParallelSessions = "executeWrite_parallelSessions" + executeWriteSequentialSessions = "executeWrite_sequentialSessions" + executeWriteSequentialTransactions = "executeWrite_sequentialTransactions" + executeWriteSequentialQueries = "executeWrite_sequentialQueries" +) + +// newWorkload creates a new workload instance with proper initialization and validation. +func newWorkload(method *string, queries []workloadQuery, database *string, routing *string, mode *string) (*workload, error) { + wl := &workload{ + Method: method, + Queries: queries, + Database: database, + Routing: routing, + Mode: mode, + } + + // Default database to an empty string if nil + if wl.Database == nil { + defaultDB := "" + wl.Database = &defaultDB + } + + // Convert routing to AccessMode + if err := wl.convertRoutingToAccessMode(); err != nil { + return nil, err + } + return wl, nil +} + +func (w *workloads) store(wl *workload) string { + if w.workloads == nil { + w.id = 0 + w.workloads = make(map[string]*workload) + } + w.id++ + workloadId := strconv.Itoa(w.id) + w.workloads[workloadId] = wl + return workloadId +} + +func (w *workloads) fetch(workloadId string) (*workload, bool) { + value, ok := w.workloads[workloadId] + return value, ok +} + +func (w *workloads) delete(workloadId string) bool { + _, ok := w.workloads[workloadId] + delete(w.workloads, workloadId) + return ok +} + +func (w *workload) patch(update *workload) error { + if update.Method != nil { + w.Method = update.Method + } + if len(update.Queries) > 0 { + w.Queries = update.Queries + } + if update.Database != nil { + w.Database = update.Database + } + if update.Routing != nil { + w.Routing = update.Routing + } + if update.Mode != nil { + w.Mode = update.Mode + } + return w.convertRoutingToAccessMode() +} + +func (w *workload) convertRoutingToAccessMode() error { + if w.Routing == nil { + w.AccessMode = neo4j.AccessModeWrite // Default to write if not specified + return nil + } + + switch *w.Routing { + case "write": + w.AccessMode = neo4j.AccessModeWrite + case "read": + w.AccessMode = neo4j.AccessModeRead + default: + return fmt.Errorf("invalid routing value '%s'", *w.Routing) + } + return nil +} + +func (w *workload) execute(driver neo4j.DriverWithContext) error { + if driver == nil { + return errors.New("uninitialized driver: call /ready first") + } + if w.Method == nil || w.Mode == nil { + return errors.New("method or mode is nil") + } + + executionStrategy := fmt.Sprintf("%s_%s", *w.Method, *w.Mode) + + switch executionStrategy { + case executeQueryParallelSessions: + return w.executeQueryParallelSessions(driver) + case executeQuerySequentialSessions: + return w.executeQuerySequentialSessions(driver) + case sessionRunParallelSessions: + return w.sessionRunParallelSessions(driver) + case sessionRunSequentialSessions: + return w.sessionRunSequentialSessions(driver) + case sessionRunSequentialTransactions: + return w.sessionRunSequentialTransactions(driver) + case executeReadParallelSessions: + return w.executeReadParallelSessions(driver) + case executeReadSequentialSessions: + return w.executeReadSequentialSessions(driver) + case executeReadSequentialTransactions: + return w.executeReadSequentialTransactions(driver) + case executeReadSequentialQueries: + return w.executeReadSequentialQueries(driver) + case executeWriteParallelSessions: + return w.executeWriteParallelSessions(driver) + case executeWriteSequentialSessions: + return w.executeWriteSequentialSessions(driver) + case executeWriteSequentialTransactions: + return w.executeWriteSequentialTransactions(driver) + case executeWriteSequentialQueries: + return w.executeWriteSequentialQueries(driver) + default: + return fmt.Errorf("unsupported combination of method and mode: %s", executionStrategy) + } +} + +// executeParallel abstracts the common pattern of executing tasks in parallel. +// It takes a slice of workloadQuery and a function that defines how to execute each query. +func (w *workload) executeParallel( + driver neo4j.DriverWithContext, + queries []workloadQuery, + executeFunc func(neo4j.DriverWithContext, workloadQuery) error) error { + + // A wait group to wait for all goroutines to finish + var wg sync.WaitGroup + + // An error channel to capture errors from goroutines + errChan := make(chan error, len(queries)) + + // Execute each query in its own goroutine + for _, query := range queries { + wg.Add(1) + go func(q workloadQuery) { + defer wg.Done() // Ensure the wait group is marked as done after the goroutine completes + // Execute the query using the supplied executeFunc + if err := executeFunc(driver, q); err != nil { + errChan <- err + } + }(query) + } + + // Wait for all goroutines to finish + wg.Wait() + close(errChan) // Close the error channel after all goroutines have finished + + // Check for errors sent to the error channel + for err := range errChan { + if err != nil { + return err // Return the first error encountered + } + } + return nil +} + +func (w *workload) executeQuery(driver neo4j.DriverWithContext, query workloadQuery) error { + // Prepare a slice to hold the configuration options + var opts []neo4j.ExecuteQueryConfigurationOption + + // Append the database configuration option + opts = append(opts, neo4j.ExecuteQueryWithDatabase(*w.Database)) + + // Conditionally append the routing configuration option based on AccessMode + if w.AccessMode == neo4j.AccessModeRead { + opts = append(opts, neo4j.ExecuteQueryWithReadersRouting()) + } else if w.AccessMode == neo4j.AccessModeWrite { + opts = append(opts, neo4j.ExecuteQueryWithWritersRouting()) + } + + _, err := neo4j.ExecuteQuery(ctx, driver, query.Text, query.Parameters, neo4j.EagerResultTransformer, opts...) + if err != nil { + return err + } + return nil +} + +func (w *workload) executeQueryParallelSessions(driver neo4j.DriverWithContext) error { + return w.executeParallel(driver, w.Queries, w.executeQuery) +} + +func (w *workload) executeQuerySequentialSessions(driver neo4j.DriverWithContext) error { + for _, query := range w.Queries { + err := w.executeQuery(driver, query) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) createSession(driver neo4j.DriverWithContext) neo4j.SessionWithContext { + sessionConfig := neo4j.SessionConfig{ + AccessMode: w.AccessMode, + DatabaseName: *w.Database, + } + return driver.NewSession(ctx, sessionConfig) +} + +func (w *workload) sessionRun(session neo4j.SessionWithContext, query workloadQuery) error { + result, err := session.Run(ctx, query.Text, query.Parameters) + if err != nil { + return err + } + + // Consume the results to ensure they are fetched and processed + _, err = result.Consume(ctx) + return err +} + +func (w *workload) sessionRunParallelSessions(driver neo4j.DriverWithContext) error { + // Define how to execute a session run in parallel + sessionRunFunc := func(driver neo4j.DriverWithContext, query workloadQuery) error { + session := w.createSession(driver) + defer session.Close(ctx) + return w.sessionRun(session, query) + } + return w.executeParallel(driver, w.Queries, sessionRunFunc) +} + +func (w *workload) sessionRunSequentialSessions(driver neo4j.DriverWithContext) error { + for _, query := range w.Queries { + session := w.createSession(driver) + err := w.sessionRun(session, query) + session.Close(ctx) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) sessionRunSequentialTransactions(driver neo4j.DriverWithContext) error { + session := w.createSession(driver) + defer session.Close(ctx) + + for _, query := range w.Queries { + err := w.sessionRun(session, query) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) executeRead(session neo4j.SessionWithContext, query workloadQuery) error { + _, err := session.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (any, error) { + result, err := tx.Run(ctx, query.Text, query.Parameters) + if err != nil { + return nil, err + } + // Consume the results to ensure they are fetched and processed + return result.Consume(ctx) + }) + return err +} + +func (w *workload) executeReadParallelSessions(driver neo4j.DriverWithContext) error { + // Define how to execute an execute read in parallel + executeReadFunc := func(driver neo4j.DriverWithContext, query workloadQuery) error { + session := w.createSession(driver) + defer session.Close(ctx) + return w.executeRead(session, query) + } + return w.executeParallel(driver, w.Queries, executeReadFunc) +} + +func (w *workload) executeReadSequentialSessions(driver neo4j.DriverWithContext) error { + for _, query := range w.Queries { + session := w.createSession(driver) + err := w.executeRead(session, query) + session.Close(ctx) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) executeReadSequentialTransactions(driver neo4j.DriverWithContext) error { + session := w.createSession(driver) + defer session.Close(ctx) + + for _, query := range w.Queries { + err := w.executeRead(session, query) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) executeReadSequentialQueries(driver neo4j.DriverWithContext) error { + session := w.createSession(driver) + defer session.Close(ctx) + + _, err := session.ExecuteRead(ctx, func(tx neo4j.ManagedTransaction) (any, error) { + var err error + for _, query := range w.Queries { + result, err := tx.Run(ctx, query.Text, query.Parameters) + if err != nil { + return nil, err + } + // Consume the results to ensure they are fetched and processed + _, err = result.Consume(ctx) + if err != nil { + return nil, err + } + } + return nil, err + }) + return err +} + +func (w *workload) executeWrite(session neo4j.SessionWithContext, query workloadQuery) error { + _, err := session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (any, error) { + result, err := tx.Run(ctx, query.Text, query.Parameters) + if err != nil { + return nil, err + } + // Consume the results to ensure they are fetched and processed + return result.Consume(ctx) + }) + return err +} + +func (w *workload) executeWriteParallelSessions(driver neo4j.DriverWithContext) error { + // Define how to execute an execute write in parallel + executeWriteFunc := func(driver neo4j.DriverWithContext, query workloadQuery) error { + session := w.createSession(driver) + defer session.Close(ctx) + return w.executeWrite(session, query) + } + return w.executeParallel(driver, w.Queries, executeWriteFunc) +} + +func (w *workload) executeWriteSequentialSessions(driver neo4j.DriverWithContext) error { + for _, query := range w.Queries { + session := w.createSession(driver) + err := w.executeWrite(session, query) + session.Close(ctx) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) executeWriteSequentialTransactions(driver neo4j.DriverWithContext) error { + session := w.createSession(driver) + defer session.Close(ctx) + + for _, query := range w.Queries { + err := w.executeRead(session, query) + if err != nil { + return err + } + } + return nil +} + +func (w *workload) executeWriteSequentialQueries(driver neo4j.DriverWithContext) error { + session := w.createSession(driver) + defer session.Close(ctx) + + _, err := session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (any, error) { + var err error + for _, query := range w.Queries { + result, err := tx.Run(ctx, query.Text, query.Parameters) + if err != nil { + return nil, err + } + // Consume the results to ensure they are fetched and processed + _, err = result.Consume(ctx) + if err != nil { + return nil, err + } + } + return nil, err + }) + return err +} diff --git a/benchkit/Dockerfile b/benchkit/Dockerfile new file mode 100644 index 00000000..44a7e224 --- /dev/null +++ b/benchkit/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.18 as builder + +# Create and change to the driver directory. +WORKDIR /driver + +# Copy local files to the container's workspace. +COPY . /driver/ + +# Fetch driver dependencies (if any). +RUN go mod download +RUN go mod verify + +# Build the binary. +RUN CGO_ENABLED=0 GOOS=linux go build -v -o benchkit/benchkit ./benchkit-backend + +# Only copy the compiled binary from the builder stage. +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=builder /driver/benchkit/benchkit . + +# Run the benchkit binary. +CMD ["./benchkit"]