Skip to content

Commit 0285da1

Browse files
authored
Experimental REST adapter for qconf (#16)
* Add REST adapter for qconf interface
1 parent a59463d commit 0285da1

File tree

11 files changed

+534
-1040
lines changed

11 files changed

+534
-1040
lines changed

Makefile

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,36 @@ build:
2929
@echo "Building the Open Cluster Scheduler image..."
3030
docker build -t $(IMAGE_NAME):$(IMAGE_TAG) .
3131

32+
# Running apptainers in containers requires more permissions. You can drop
33+
# the --privileged flag and the --cap-add SYS_ADMIN flag if you don't need
34+
# to run apptainers in containers.
3235
.PHONY: run
3336
run: build
3437
@echo "Running the container..."
3538
mkdir -p ./installation
3639
docker run --rm -it -h master --privileged -v /dev/fuse:/dev/fuse --cap-add SYS_ADMIN --name $(CONTAINER_NAME) -v ./installation:/opt/cs-install -v ./:/root/go/src/github.com/hpc-gridware/go-clusterscheduler $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash
3740

41+
# Running apptainers in containers requires more permissions. You can drop
42+
# the --privileged flag and the --cap-add SYS_ADMIN flag if you don't need
43+
# to run apptainers in containers.
3844
.PHONY: simulate
3945
simulate:
4046
@echo "Running the container in simulation mode using cluster.json"
4147
mkdir -p ./installation
4248
docker run --rm -it -h master --privileged --cap-add SYS_ADMIN --name $(CONTAINER_NAME) -v ./installation:/opt/cs-install -v ./:/root/go/src/github.com/hpc-gridware/go-clusterscheduler $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash -c "cd /root/go/src/github.com/hpc-gridware/go-clusterscheduler/cmd/simulator && go build . && ./simulator run ../../cluster.json && /bin/bash"
4349

44-
.PHONY: simulate
45-
simulate:
46-
@echo "Running the container in simulation mode using cluster.json"
50+
#.PHONY: simulate
51+
#simulate:
52+
# @echo "Running the container in simulation mode using cluster.json"
53+
# mkdir -p ./installation
54+
# docker run --rm -it -h master --name $(CONTAINER_NAME) -v ./installation:/opt/cs-install -v ./:/root/go/src/github.com/hpc-gridware/go-clusterscheduler $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash -c "cd /root/go/src/github.com/hpc-gridware/go-clusterscheduler/cmd/simulator && go build . && ./simulator run ../../cluster.json && /bin/bash"
55+
56+
.PHONY: adapter
57+
adapter:
58+
@echo "Running the adapter on port 8282...POST to http://localhost:8282/api/v0/command"
59+
@echo "Example: curl -X POST http://localhost:8282/api/v0/command -d '{\"method\": \"ShowExecHosts\"}'"
4760
mkdir -p ./installation
48-
docker run --rm -it -h master --name $(CONTAINER_NAME) -v ./installation:/opt/cs-install -v ./:/root/go/src/github.com/hpc-gridware/go-clusterscheduler $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash -c "cd /root/go/src/github.com/hpc-gridware/go-clusterscheduler/cmd/simulator && go build . && ./simulator run ../../cluster.json && /bin/bash"
61+
docker run --rm -it -h master -p 8282:8282 --name $(CONTAINER_NAME) -v ./installation:/opt/cs-install -v ./:/root/go/src/github.com/hpc-gridware/go-clusterscheduler $(IMAGE_NAME):$(IMAGE_TAG) /bin/bash -c "cd /root/go/src/github.com/hpc-gridware/go-clusterscheduler/pkg/adapter && go build . && ./adapter"
4962

5063
.PHONY: clean
5164
clean:

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ require (
1212
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
1313
github.com/google/go-cmp v0.6.0 // indirect
1414
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect
15+
github.com/stretchr/testify v1.9.0 // indirect
1516
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
1617
golang.org/x/net v0.27.0 // indirect
17-
golang.org/x/sys v0.22.0 // indirect
18+
golang.org/x/sys v0.25.0 // indirect
1819
golang.org/x/text v0.16.0 // indirect
1920
golang.org/x/tools v0.23.0 // indirect
2021
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,14 @@ github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
1414
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
1515
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1616
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
17-
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
18-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
17+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
18+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
1919
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
2020
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
2121
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
2222
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
23-
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
24-
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
23+
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
24+
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2525
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
2626
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
2727
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=

pkg/adapter/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
adapter

pkg/adapter/adapter.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
/*___INFO__MARK_BEGIN__*/
2+
/*************************************************************************
3+
* Copyright 2024 HPC-Gridware GmbH
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
************************************************************************/
18+
/*___INFO__MARK_END__*/
19+
20+
package main
21+
22+
import (
23+
"context"
24+
"encoding/json"
25+
"fmt"
26+
"net/http"
27+
"reflect"
28+
"time"
29+
30+
"go.opentelemetry.io/contrib/bridges/otelslog"
31+
"go.opentelemetry.io/otel"
32+
"go.opentelemetry.io/otel/attribute"
33+
"go.opentelemetry.io/otel/exporters/stdout/stdoutlog"
34+
"go.opentelemetry.io/otel/exporters/stdout/stdoutmetric"
35+
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"
36+
"go.opentelemetry.io/otel/log/global"
37+
"go.opentelemetry.io/otel/sdk/log"
38+
"go.opentelemetry.io/otel/sdk/metric"
39+
"go.opentelemetry.io/otel/sdk/trace"
40+
)
41+
42+
const name = "go.hpc-gridware.com/example/qconf"
43+
44+
var (
45+
tracer = otel.Tracer(name)
46+
meter = otel.Meter(name)
47+
logger = otelslog.NewLogger(name)
48+
)
49+
50+
func init() {
51+
52+
}
53+
54+
// Usage
55+
// router := mux.NewRouter()
56+
// router.Handle("/api/v0/command", adapter.NewAdapter(qconf)).Methods("POST")
57+
58+
// A JSON request body is expected with the following structure:
59+
// {
60+
// "method": "<method name>",
61+
// "args": [
62+
// "arg1",
63+
// "arg2",
64+
// ...
65+
// ]
66+
// }
67+
68+
type CommandRequest struct {
69+
MethodName string `json:"method"`
70+
Args []json.RawMessage `json:"args"`
71+
}
72+
73+
// NewAdapter creates an http.Handler that for any Go interface.
74+
// The method name and arguments are expected in the JSON request body.
75+
// The response is the return value of the method also in JSON format.
76+
// The arguments and the return values must have a JSON serializable format.
77+
// Only 1 or 2 return values are supported. In case of an error of the
78+
// executed function an http status code 500 is returned.
79+
//
80+
// The adapter uses OpenTelemetry to trace the method calls and log the errors.
81+
func NewAdapter(instance interface{}) http.Handler {
82+
loggerProvider, err := newLoggerProvider()
83+
if err != nil {
84+
panic(err)
85+
}
86+
global.SetLoggerProvider(loggerProvider)
87+
88+
tracerProvider, err := newTraceProvider()
89+
if err != nil {
90+
panic(err)
91+
}
92+
otel.SetTracerProvider(tracerProvider)
93+
94+
meterProvider, err := newMeterProvider()
95+
if err != nil {
96+
panic(err)
97+
}
98+
otel.SetMeterProvider(meterProvider)
99+
100+
return &adapter{
101+
instance: instance,
102+
}
103+
}
104+
105+
type adapter struct {
106+
instance interface{}
107+
}
108+
109+
func (a *adapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
110+
ctx, span := tracer.Start(r.Context(), "request")
111+
defer span.End()
112+
113+
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
114+
defer cancel()
115+
116+
var req CommandRequest
117+
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
118+
logErr := fmt.Errorf("invalid request payload: %w", err)
119+
a.fail(ctx, w, r, http.StatusBadRequest, logErr.Error(), err)
120+
return
121+
}
122+
123+
logger.InfoContext(ctx, "request", req.MethodName)
124+
125+
method := reflect.ValueOf(a.instance).MethodByName(req.MethodName)
126+
if !method.IsValid() {
127+
logErr := fmt.Errorf("method not found: %s", req.MethodName)
128+
a.fail(ctx, w, r, http.StatusNotFound, logErr.Error(), nil)
129+
return
130+
}
131+
132+
methodType := method.Type()
133+
if methodType.NumIn() != len(req.Args) {
134+
logErr := fmt.Errorf("invalid number of arguments for method %s: %d but should be %d",
135+
req.MethodName, len(req.Args), methodType.NumIn())
136+
a.fail(ctx, w, r, http.StatusBadRequest,
137+
logErr.Error(), nil)
138+
return
139+
}
140+
141+
args := make([]reflect.Value, len(req.Args))
142+
for i, arg := range req.Args {
143+
argType := methodType.In(i)
144+
argValue := reflect.New(argType).Interface()
145+
if err := json.Unmarshal(arg, argValue); err != nil {
146+
a.fail(ctx, w, r, http.StatusBadRequest,
147+
fmt.Sprintf("Invalid argument %d", i), err)
148+
return
149+
}
150+
args[i] = reflect.Indirect(reflect.ValueOf(argValue))
151+
}
152+
153+
qconfCallValueAttr := attribute.String("qconf.command", req.MethodName)
154+
span.SetAttributes(qconfCallValueAttr)
155+
//requestCounter.Add(ctx, 1, metric.WithAttributes(qconfCallValueAttr))
156+
157+
results := method.Call(args)
158+
if len(results) > 1 {
159+
if err, ok := results[1].Interface().(error); ok && err != nil {
160+
logErr := fmt.Errorf("method call %s failed: %w", req.MethodName,
161+
results[0].Interface().(error))
162+
a.fail(ctx, w, r, http.StatusInternalServerError, logErr.Error(), err)
163+
return
164+
}
165+
}
166+
167+
if len(results) > 0 {
168+
// check if the result is an error
169+
if _, ok := results[0].Interface().(error); ok {
170+
logErr := fmt.Errorf("method call %s failed: %w", req.MethodName,
171+
results[0].Interface().(error))
172+
a.fail(ctx, w, r, http.StatusInternalServerError, logErr.Error(),
173+
results[0].Interface().(error))
174+
return
175+
}
176+
177+
w.Header().Set("Content-Type", "application/json")
178+
if err := json.NewEncoder(w).Encode(results[0].Interface()); err != nil {
179+
logErr := fmt.Errorf("failed to encode response for method %s: %w",
180+
req.MethodName, err)
181+
a.fail(ctx, w, r, http.StatusInternalServerError,
182+
logErr.Error(), err)
183+
return
184+
}
185+
}
186+
logger.InfoContext(ctx, "request successfully processed", req.MethodName)
187+
}
188+
189+
func (a *adapter) fail(ctx context.Context, w http.ResponseWriter, r *http.Request, status int, message string, err error) {
190+
w.WriteHeader(status)
191+
response := map[string]string{"error": message}
192+
logger.InfoContext(ctx, message, "URL", r.URL.Path)
193+
if err := json.NewEncoder(w).Encode(response); err != nil {
194+
logger.ErrorContext(ctx, "Failed to encode error response", err)
195+
}
196+
// write the error to the response body
197+
w.Write([]byte(message))
198+
}
199+
200+
func newLoggerProvider() (*log.LoggerProvider, error) {
201+
logExporter, err := stdoutlog.New()
202+
if err != nil {
203+
return nil, err
204+
}
205+
206+
loggerProvider := log.NewLoggerProvider(
207+
log.WithProcessor(log.NewBatchProcessor(logExporter)),
208+
)
209+
return loggerProvider, nil
210+
}
211+
212+
func newTraceProvider() (*trace.TracerProvider, error) {
213+
traceExporter, err := stdouttrace.New(
214+
stdouttrace.WithPrettyPrint())
215+
if err != nil {
216+
return nil, err
217+
}
218+
219+
traceProvider := trace.NewTracerProvider(
220+
trace.WithBatcher(traceExporter,
221+
// Default is 5s. Set to 1s for demonstrative purposes.
222+
trace.WithBatchTimeout(time.Second)),
223+
)
224+
return traceProvider, nil
225+
}
226+
227+
func newMeterProvider() (*metric.MeterProvider, error) {
228+
metricExporter, err := stdoutmetric.New()
229+
if err != nil {
230+
return nil, err
231+
}
232+
233+
meterProvider := metric.NewMeterProvider(
234+
metric.WithReader(metric.NewPeriodicReader(metricExporter,
235+
// Default is 1m. Set to 10s for demonstrative purposes.
236+
metric.WithInterval(10*time.Second))),
237+
)
238+
return meterProvider, nil
239+
}

pkg/adapter/go.mod

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
module github.com/hpc-gridware/go-clusterscheduler/pkg/adapter
2+
3+
go 1.23.1
4+
5+
require (
6+
github.com/gorilla/mux v1.8.1
7+
github.com/hpc-gridware/go-clusterscheduler v0.0.0-20240914052507-a59463d8ccd2
8+
go.opentelemetry.io/contrib/bridges/otelslog v0.5.0
9+
go.opentelemetry.io/otel v1.30.0
10+
go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.6.0
11+
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.30.0
12+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.30.0
13+
go.opentelemetry.io/otel/log v0.6.0
14+
go.opentelemetry.io/otel/metric v1.30.0
15+
go.opentelemetry.io/otel/sdk v1.30.0
16+
go.opentelemetry.io/otel/sdk/log v0.6.0
17+
)
18+
19+
require (
20+
github.com/go-logr/logr v1.4.2 // indirect
21+
github.com/go-logr/stdr v1.2.2 // indirect
22+
github.com/google/uuid v1.6.0 // indirect
23+
go.opentelemetry.io/otel/sdk/metric v1.30.0 // indirect
24+
go.opentelemetry.io/otel/trace v1.30.0 // indirect
25+
golang.org/x/sys v0.25.0 // indirect
26+
)

0 commit comments

Comments
 (0)