Skip to content

Commit 8ef0602

Browse files
committed
Initial structure.
1 parent 74cdb7f commit 8ef0602

50 files changed

Lines changed: 3163 additions & 2 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main"]
6+
pull_request:
7+
branches: ["main"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
go-version: ["1.21", "1.22"]
15+
steps:
16+
- name: Checkout
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: ${{ matrix.go-version }}
23+
cache: true
24+
25+
- name: Run tests
26+
run: make test
27+
28+
lint:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- name: Checkout
32+
uses: actions/checkout@v4
33+
34+
- name: Set up Go
35+
uses: actions/setup-go@v5
36+
with:
37+
go-version: '1.22'
38+
cache: true
39+
40+
- name: Check formatting
41+
run: |
42+
files=$(gofmt -l .)
43+
if [ -n "$files" ]; then
44+
echo "The following files need gofmt:" && echo "$files"
45+
exit 1
46+
fi
47+
48+
- name: Run go vet
49+
run: go vet ./...
50+
51+
- name: Install staticcheck
52+
run: go install honnef.co/go/tools/cmd/staticcheck@latest
53+
54+
- name: Run staticcheck
55+
run: staticcheck ./...

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ go.work.sum
2727
# env file
2828
.env
2929

30+
.gocache
31+
3032
# Editor/IDE
3133
# .idea/
3234
# .vscode/

‎AGENTS.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ opsorch-adapter-<provider>/
3434
- **opsorch-core**: routing, secret management, registry, HTTP layer.
3535
- **adapter repos**: implement the capability interfaces.
3636

37-
Adapters do not live inside opsorch-core.
37+
Adapters do not live inside opsorch-core. They are loaded at runtime either via registered constructors or local plugin binaries.
3838

3939
---
4040

@@ -122,6 +122,15 @@ Providers can be:
122122
- dynamically loaded
123123
- injected through config
124124

125+
### Plugin-Based Loading (no remote services)
126+
127+
OpsOrch Core can launch a local adapter binary as a child process (no network hops) when `OPSORCH_<CAP>_PLUGIN` is set or the provider config includes a `plugin` path. RPC is JSON over stdin/stdout:
128+
129+
- Request: `{ "method": "incident.list" | "log.query" | ..., "config": {...}, "payload": {...} }`
130+
- Response: `{ "result": <value>, "error": "<msg>" }`
131+
132+
The plugin process stays alive and receives multiple RPC calls on the same stdio stream.
133+
125134
---
126135

127136
## 6. Normalization Responsibilities
@@ -231,4 +240,3 @@ This guide explains:
231240
- how they evolve alongside schema changes
232241

233242
It **intentionally avoids prescribing model shapes**, because those will be defined as the system grows.
234-

‎Makefile‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Make targets for OpsOrch Core
2+
# Uses local caches to work in sandboxed environments and keeps Go version configurable.
3+
4+
GO ?= go
5+
GOCACHE ?= $(PWD)/.gocache
6+
GOMODCACHE ?= $(PWD)/.gocache/mod
7+
CACHE_ENV = GOCACHE=$(GOCACHE) GOMODCACHE=$(GOMODCACHE)
8+
9+
.PHONY: all fmt test tidy clean run build
10+
11+
all: test
12+
13+
fmt:
14+
$(GO)fmt ./...
15+
16+
# Runs unit tests with a local build cache to avoid sandbox permission issues.
17+
test:
18+
$(CACHE_ENV) $(GO) test ./...
19+
20+
tidy:
21+
$(GO) mod tidy
22+
23+
build:
24+
$(CACHE_ENV) $(GO) build ./...
25+
26+
run:
27+
$(CACHE_ENV) $(GO) run ./cmd/opsorch
28+
29+
clean:
30+
rm -rf $(GOCACHE)

‎README.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,50 @@ Adapters live in separate repos such as:
2020
- opsorch-adapter-jira
2121
- opsorch-adapter-slack
2222

23+
## Adapter Loading Model
24+
25+
OpsOrch Core never links vendor logic directly. Each capability is wired via an **in-process provider** that you import into the binary. The provider registers itself (e.g., `incident.RegisterProvider("pagerduty", pagerduty.New)`) and is selected with env `OPSORCH_<CAP>_PROVIDER`.
26+
27+
Environment variables for any capability (`incident`, `log`, `metric`, `dashboard`, `ticket`, `messaging`, `secret`):
28+
- `OPSORCH_<CAP>_PROVIDER=<registered name>`
29+
- `OPSORCH_<CAP>_CONFIG=<json>`
30+
31+
### Using an in-process provider
32+
1) Add the adapter dependency to the core binary you are building:
33+
```bash
34+
go get github.com/opsorch/opsorch-adapter-pagerduty
35+
```
36+
2) Import the adapter for side effects so it registers itself (create `cmd/opsorch/providers.go` if you prefer to keep imports separate):
37+
```go
38+
package main
39+
40+
import (
41+
_ "github.com/opsorch/opsorch-adapter-pagerduty/incident" // registers with incident registry
42+
_ "github.com/opsorch/opsorch-adapter-elasticsearch/log" // registers with log registry
43+
)
44+
```
45+
3) Select the provider via env and pass its config:
46+
```bash
47+
OPSORCH_INCIDENT_PROVIDER=pagerduty OPSORCH_INCIDENT_CONFIG='{"apiKey":"...","routingKey":"..."}' \
48+
OPSORCH_LOG_PROVIDER=elasticsearch OPSORCH_LOG_CONFIG='{"url":"http://..."}' \
49+
go run ./cmd/opsorch
50+
```
51+
52+
### Quick start: run locally and curl
53+
54+
Run the server (defaults to :8080) with a registered provider and its config:
55+
```bash
56+
OPSORCH_INCIDENT_PROVIDER=<registered> OPSORCH_INCIDENT_CONFIG='{"token":"..."}' go run ./cmd/opsorch
57+
```
58+
59+
Hit the API:
60+
```bash
61+
curl -s http://localhost:8080/incidents
62+
curl -s -X POST http://localhost:8080/incidents \
63+
-H "Content-Type: application/json" \
64+
-d '{"title":"test","status":"open","severity":"sev3"}'
65+
```
66+
2367
## Key Concepts
2468

2569
### Unified API Layer

‎api/capability.go‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package api
2+
3+
import "strings"
4+
5+
// normalizeCapability maps plural or variant path segments to canonical capability keys.
6+
func normalizeCapability(name string) (string, bool) {
7+
switch strings.ToLower(name) {
8+
case "incident", "incidents":
9+
return "incident", true
10+
case "log", "logs":
11+
return "log", true
12+
case "metric", "metrics":
13+
return "metric", true
14+
case "dashboard", "dashboards":
15+
return "dashboard", true
16+
case "ticket", "tickets":
17+
return "ticket", true
18+
case "message", "messages", "messaging":
19+
return "messaging", true
20+
default:
21+
return "", false
22+
}
23+
}

‎api/common.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/http"
7+
8+
"github.com/opsorch/opsorch-core/orcherr"
9+
)
10+
11+
func decodeJSON(r *http.Request, out any) error {
12+
dec := json.NewDecoder(r.Body)
13+
dec.DisallowUnknownFields()
14+
return dec.Decode(out)
15+
}
16+
17+
func writeJSON(w http.ResponseWriter, status int, payload any) {
18+
w.Header().Set("Content-Type", "application/json")
19+
w.WriteHeader(status)
20+
_ = json.NewEncoder(w).Encode(payload)
21+
}
22+
23+
func writeError(w http.ResponseWriter, status int, err orcherr.OpsOrchError) {
24+
writeJSON(w, status, map[string]string{"code": err.Code, "message": err.Message})
25+
}
26+
27+
func writeProviderError(w http.ResponseWriter, err error) {
28+
var oe orcherr.OpsOrchError
29+
if errors.As(err, &oe) {
30+
status := http.StatusBadGateway
31+
switch oe.Code {
32+
case "not_found":
33+
status = http.StatusNotFound
34+
case "bad_request":
35+
status = http.StatusBadRequest
36+
}
37+
writeError(w, status, oe)
38+
return
39+
}
40+
writeError(w, http.StatusBadGateway, orcherr.OpsOrchError{Code: "provider_error", Message: "provider error"})
41+
}

‎api/dashboard_handler.go‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strings"
7+
8+
"github.com/opsorch/opsorch-core/dashboard"
9+
"github.com/opsorch/opsorch-core/orcherr"
10+
)
11+
12+
// DashboardHandler wraps provider wiring for dashboards.
13+
type DashboardHandler struct {
14+
provider dashboard.Provider
15+
}
16+
17+
func newDashboardHandlerFromEnv(sec SecretProvider) (DashboardHandler, error) {
18+
name, cfg, pluginPath, err := loadProviderConfig(sec, "dashboard", "OPSORCH_DASHBOARD_PROVIDER", "OPSORCH_DASHBOARD_CONFIG", "OPSORCH_DASHBOARD_PLUGIN")
19+
if err != nil || (name == "" && pluginPath == "") {
20+
return DashboardHandler{}, err
21+
}
22+
if pluginPath != "" {
23+
return DashboardHandler{provider: newDashboardPluginProvider(pluginPath, cfg)}, nil
24+
}
25+
constructor, ok := dashboard.LookupProvider(name)
26+
if !ok {
27+
return DashboardHandler{}, fmt.Errorf("dashboard provider %s not registered", name)
28+
}
29+
provider, err := constructor(cfg)
30+
if err != nil {
31+
return DashboardHandler{}, err
32+
}
33+
return DashboardHandler{provider: provider}, nil
34+
}
35+
36+
func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) bool {
37+
if !strings.HasPrefix(r.URL.Path, "/dashboards") {
38+
return false
39+
}
40+
if s.dashboard.provider == nil {
41+
writeError(w, http.StatusNotImplemented, orcherr.OpsOrchError{Code: "dashboard_provider_missing", Message: "dashboard provider not configured"})
42+
return true
43+
}
44+
45+
path := strings.TrimSuffix(r.URL.Path, "/")
46+
segments := strings.Split(strings.Trim(path, "/"), "/")
47+
48+
switch {
49+
case len(segments) == 1 && r.Method == http.MethodGet:
50+
views, err := s.dashboard.provider.List(r.Context())
51+
if err != nil {
52+
writeProviderError(w, err)
53+
return true
54+
}
55+
writeJSON(w, http.StatusOK, views)
56+
return true
57+
case len(segments) == 2 && r.Method == http.MethodGet:
58+
id := segments[1]
59+
view, err := s.dashboard.provider.Get(r.Context(), id)
60+
if err != nil {
61+
writeProviderError(w, err)
62+
return true
63+
}
64+
writeJSON(w, http.StatusOK, view)
65+
return true
66+
default:
67+
return false
68+
}
69+
}

0 commit comments

Comments
 (0)