Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Adapters live in separate repos such as:

OpsOrch Core never links vendor logic directly. Each capability is resolved at runtime by either importing an **in-process provider** (Go package that registers itself) or by launching a **local plugin binary** that speaks OpsOrch's stdio RPC protocol. At startup OpsOrch checks for environment overrides first, then falls back to any persisted configuration stored via the secret provider.

Environment variables for any capability (`incident`, `alert`, `log`, `metric`, `ticket`, `messaging`, `service`, `deployment`, `secret`):
Environment variables for any capability (`incident`, `alert`, `log`, `metric`, `ticket`, `messaging`, `service`, `deployment`, `team`, `secret`):
- `OPSORCH_<CAP>_PROVIDER=<registered name>` – name passed to the corresponding registry
- `OPSORCH_<CAP>_CONFIG=<json>` – decrypted config map forwarded to the constructor
- `OPSORCH_<CAP>_PLUGIN=/path/to/binary` – optional local plugin that overrides `OPSORCH_<CAP>_PROVIDER`
Expand Down Expand Up @@ -118,6 +118,23 @@ curl -s -X POST http://localhost:8080/deployments/query \

# Get a specific deployment (requires deployment provider)
curl -s http://localhost:8080/deployments/deploy-123

# Query Teams (requires team provider)
curl -s -X POST http://localhost:8080/teams/query \
-H "Content-Type: application/json" \
-d '{
"name": "backend",
"tags": {"type": "team"},
"scope": {
"service": "api-service"
}
}'

# Get a specific team (requires team provider)
curl -s http://localhost:8080/teams/engineering

# Get team members (requires team provider)
curl -s http://localhost:8080/teams/engineering/members
```

Add `-H "Authorization: Bearer <token>"` to each curl when `OPSORCH_BEARER_TOKEN` is set.
Expand Down Expand Up @@ -145,6 +162,7 @@ If only one is provided the server will refuse to start.
Pre-built multi-platform Docker images (linux/amd64, linux/arm64) are automatically published to GitHub Container Registry (GHCR) on every release.

**Pull and run the latest version:**

```bash
docker pull ghcr.io/opsorch/opsorch-core:latest
docker run --rm -p 8080:8080 ghcr.io/opsorch/opsorch-core:latest
Expand Down Expand Up @@ -219,6 +237,7 @@ OpsOrch exposes API endpoints for:
- Messaging
- Services
- Deployments
- Teams

Schemas live under `schema/` and evolve as the system matures.

Expand Down
2 changes: 2 additions & 0 deletions api/capability.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func normalizeCapability(name string) (string, bool) {
return "service", true
case "deployment", "deployments":
return "deployment", true
case "team", "teams":
return "team", true
default:
return "", false
}
Expand Down
25 changes: 25 additions & 0 deletions api/plugin_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,28 @@ func (p deploymentPluginProvider) Get(ctx context.Context, id string) (schema.De
var res schema.Deployment
return res, p.runner.call(ctx, "deployment.get", map[string]any{"id": id}, &res)
}

// Team plugin provider -------------------------------------------------------

type teamPluginProvider struct {
runner *pluginRunner
}

func newTeamPluginProvider(path string, cfg map[string]any) teamPluginProvider {
return teamPluginProvider{runner: newPluginRunner(path, cfg)}
}

func (p teamPluginProvider) Query(ctx context.Context, query schema.TeamQuery) ([]schema.Team, error) {
var res []schema.Team
return res, p.runner.call(ctx, "team.query", query, &res)
}

func (p teamPluginProvider) Get(ctx context.Context, id string) (schema.Team, error) {
var res schema.Team
return res, p.runner.call(ctx, "team.get", map[string]any{"id": id}, &res)
}

func (p teamPluginProvider) Members(ctx context.Context, teamID string) ([]schema.TeamMember, error) {
var res []schema.TeamMember
return res, p.runner.call(ctx, "team.members", map[string]any{"teamID": teamID}, &res)
}
3 changes: 3 additions & 0 deletions api/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/opsorch/opsorch-core/metric"
"github.com/opsorch/opsorch-core/orcherr"
"github.com/opsorch/opsorch-core/service"
"github.com/opsorch/opsorch-core/team"
"github.com/opsorch/opsorch-core/ticket"
)

Expand Down Expand Up @@ -47,6 +48,8 @@ func (s *Server) handleProviders(w http.ResponseWriter, r *http.Request) bool {
providers = service.Providers()
case "deployment":
providers = deployment.Providers()
case "team":
providers = team.Providers()
}
writeJSON(w, http.StatusOK, map[string]any{"providers": providers})
return true
Expand Down
9 changes: 9 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Server struct {
messaging MessagingHandler
service ServiceHandler
deployment DeploymentHandler
team TeamHandler
secret SecretProvider
}

Expand Down Expand Up @@ -81,6 +82,12 @@ func NewServerFromEnv(ctx context.Context) (*Server, error) {
log.Printf("Failed to initialize deployment provider: %v", err)
dep = DeploymentHandler{} // Empty handler with nil provider
}
tm, err := newTeamHandlerFromEnv(sec)
if err != nil {
// Log the error but continue startup with team capability disabled
log.Printf("Failed to initialize team provider: %v", err)
tm = TeamHandler{} // Empty handler with nil provider
}

_ = ctx // reserved for future use

Expand All @@ -97,6 +104,7 @@ func NewServerFromEnv(ctx context.Context) (*Server, error) {
messaging: msg,
service: svc,
deployment: dep,
team: tm,
secret: sec,
}, nil
}
Expand Down Expand Up @@ -139,6 +147,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case s.handleMessaging(w, r):
case s.handleService(w, r):
case s.handleDeployment(w, r):
case s.handleTeam(w, r):
default:
http.NotFound(w, r)
}
Expand Down
87 changes: 87 additions & 0 deletions api/team_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package api

import (
"fmt"
"net/http"
"strings"

"github.com/opsorch/opsorch-core/orcherr"
"github.com/opsorch/opsorch-core/schema"
"github.com/opsorch/opsorch-core/team"
)

// TeamHandler wraps provider wiring for teams.
type TeamHandler struct {
provider team.Provider
}

func newTeamHandlerFromEnv(sec SecretProvider) (TeamHandler, error) {
name, cfg, pluginPath, err := loadProviderConfig(sec, "team", "OPSORCH_TEAM_PROVIDER", "OPSORCH_TEAM_CONFIG", "OPSORCH_TEAM_PLUGIN")
if err != nil || (name == "" && pluginPath == "") {
return TeamHandler{}, err
}
if pluginPath != "" {
return TeamHandler{provider: newTeamPluginProvider(pluginPath, cfg)}, nil
}
constructor, ok := team.LookupProvider(name)
if !ok {
return TeamHandler{}, fmt.Errorf("team provider %s not registered", name)
}
provider, err := constructor(cfg)
if err != nil {
return TeamHandler{}, err
}
return TeamHandler{provider: provider}, nil
}

func (s *Server) handleTeam(w http.ResponseWriter, r *http.Request) bool {
if !strings.HasPrefix(r.URL.Path, "/teams") {
return false
}
if s.team.provider == nil {
writeError(w, http.StatusNotImplemented, orcherr.OpsOrchError{Code: "team_provider_missing", Message: "team provider not configured"})
return true
}

path := strings.TrimSuffix(r.URL.Path, "/")
segments := strings.Split(strings.Trim(path, "/"), "/")

switch {
case len(segments) == 2 && segments[1] == "query" && r.Method == http.MethodPost:
var query schema.TeamQuery
if err := decodeJSON(r, &query); err != nil {
writeError(w, http.StatusBadRequest, orcherr.OpsOrchError{Code: "bad_request", Message: err.Error()})
return true
}
teams, err := s.team.provider.Query(r.Context(), query)
if err != nil {
writeProviderError(w, err)
return true
}
logAudit(r, "team.query")
writeJSON(w, http.StatusOK, teams)
return true
case len(segments) == 2 && r.Method == http.MethodGet:
id := segments[1]
team, err := s.team.provider.Get(r.Context(), id)
if err != nil {
writeProviderError(w, err)
return true
}
logAudit(r, "team.get")
writeJSON(w, http.StatusOK, team)
return true
case len(segments) == 3 && segments[2] == "members" && r.Method == http.MethodGet:
teamID := segments[1]
members, err := s.team.provider.Members(r.Context(), teamID)
if err != nil {
writeProviderError(w, err)
return true
}
logAudit(r, "team.members")
writeJSON(w, http.StatusOK, members)
return true
default:
return false
}
}
Loading
Loading