Skip to content
Open
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
7 changes: 4 additions & 3 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ tasks:
- task --list

build:
desc: Build the full incident-commander binary with embedded UI
deps:
- go:build
desc: Build standalone plugin modules
cmds:
- task -d opensearch build

clean:
desc: Remove build artifacts and Task checksums
Expand All @@ -53,6 +53,7 @@ tasks:
tailwind-asset:
desc: Download the vendored Tailwind script used by the OIDC static page
cmds:
- mkdir -p auth/oidc/static
- curl -sL "https://cdn.tailwindcss.com/{{.TAILWIND_VERSION}}" -o {{.TAILWIND_JS}}
generates:
- "{{.TAILWIND_JS}}"
Expand Down
24 changes: 24 additions & 0 deletions opensearch/Plugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: mission-control.flanksource.com/v1
kind: Plugin
metadata:
name: opensearch
spec:
source: opensearch
version: "0.1.0"
selector:
types:
- MissionControl::Connection
- OpenSearch::Cluster
- OpenSearch::Index
- Elasticsearch::Cluster
- Elasticsearch::Index
connections:
opensearch: {}

# Optional plugin properties:
# defaultProfile: jaeger
# index: jaeger-span*
# profilesYaml: |
# custom:
# imports: [otel]
# index: traces-*
80 changes: 80 additions & 0 deletions opensearch/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
version: "3"

vars:
PLUGIN_NAME: opensearch
VERSION:
sh: git describe --tags --always --dirty 2>/dev/null || echo dev
BUILD_DATE:
sh: date -u "+%Y-%m-%d %H:%M:%S"
PLUGIN_PATH:
sh: echo "${MISSION_CONTROL_PLUGIN_PATH:-$HOME/.mission-control/plugins}"

tasks:
ui:install:
desc: Install pnpm dependencies for the OpenSearch plugin UI
dir: ui-src
cmds:
- PATH="/usr/local/opt/node/bin:$PATH" CI=true pnpm install --no-frozen-lockfile --prefer-offline
sources:
- package.json
status:
- test -f node_modules/.modules.yaml
- test node_modules/.modules.yaml -nt package.json

ui:build:
desc: Build the plugin UI bundle into ./ui
dir: ui-src
deps: [ui:install]
env:
PLUGIN_VERSION: "{{.VERSION}}"
PLUGIN_BUILD_DATE: "{{.BUILD_DATE}}"
cmds:
- PATH="/usr/local/opt/node/bin:$PATH" pnpm run build
sources:
- src/**/*.ts
- src/**/*.tsx
- src/**/*.css
- index.html
- package.json
- tsconfig.json
- vite.config.ts
generates:
- ../ui/index.html

generate:
desc: Regenerate ui_checksum.go from the built UI bundle
deps: [ui:build]
cmds:
- go generate .
sources:
- ui/**
- internal/gen-checksum/main.go
generates:
- ui_checksum.go

test:
desc: Run plugin unit tests
cmds:
- go test .

build:
desc: Build and install the plugin binary into $MISSION_CONTROL_PLUGIN_PATH
deps: [generate]
cmds:
- mkdir -p {{.PLUGIN_PATH}}
- >-
go build -o {{.PLUGIN_PATH}}/{{.PLUGIN_NAME}}
-ldflags "-X 'main.Version={{.VERSION}}' -X 'main.BuildDate={{.BUILD_DATE}}'"
.
- echo "Installed {{.PLUGIN_PATH}}/{{.PLUGIN_NAME}} version={{.VERSION}} built={{.BUILD_DATE}}"
sources:
- ./*.go
- profiles/**
- internal/**/*.go
- ui/**
- ui_checksum.go
- ../../plugin/**/*.go
- ../../go.mod
- ../../go.sum
generates:
- "{{.PLUGIN_PATH}}/{{.PLUGIN_NAME}}"
172 changes: 172 additions & 0 deletions opensearch/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package main

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"

"github.com/flanksource/incident-commander/plugin/sdk"
)

type resolvedOpenSearch struct {
URLs []string
Username string
Password string
Index string
InsecureSkipVerify bool
}

type openSearchClient struct {
conn resolvedOpenSearch
client *http.Client
}

func resolveOpenSearch(ctx context.Context, host sdk.HostClient, configItemID string) (resolvedOpenSearch, error) {
if host == nil {
return resolvedOpenSearch{}, fmt.Errorf("no host client available")
}
conn, err := host.GetConnection(ctx, "opensearch", configItemID)
if err != nil {
return resolvedOpenSearch{}, fmt.Errorf("get opensearch connection: %w", err)
}
if conn == nil {
return resolvedOpenSearch{}, fmt.Errorf("host returned no opensearch connection")
}
out := resolvedOpenSearch{
Username: conn.Username,
Password: conn.Password,
}
if conn.Url != "" {
out.URLs = append(out.URLs, conn.Url)
}
if conn.Properties != nil {
props := conn.Properties.AsMap()
if raw, ok := props["urls"].(string); ok {
for _, part := range strings.Split(raw, ",") {
if trimmed := strings.TrimSpace(part); trimmed != "" {
out.URLs = append(out.URLs, trimmed)
}
}
}
if values, ok := props["urls"].([]any); ok {
for _, value := range values {
if s, ok := value.(string); ok && strings.TrimSpace(s) != "" {
out.URLs = append(out.URLs, strings.TrimSpace(s))
}
}
}
if index, ok := props["index"].(string); ok {
out.Index = index
}
if insecure, ok := props["insecureSkipVerify"].(bool); ok {
out.InsecureSkipVerify = insecure
}
if insecure, ok := props["insecure_tls"].(bool); ok {
out.InsecureSkipVerify = insecure
}
}
out.URLs = uniqueStrings(out.URLs)
if len(out.URLs) == 0 {
return resolvedOpenSearch{}, fmt.Errorf("opensearch connection has no urls")
}
return out, nil
}

func newOpenSearchClient(conn resolvedOpenSearch) *openSearchClient {
transport := http.DefaultTransport.(*http.Transport).Clone()
if conn.InsecureSkipVerify {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
return &openSearchClient{
conn: conn,
client: &http.Client{
Timeout: 60 * time.Second,
Transport: transport,
},
}
}

func (c *openSearchClient) Check(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, strings.TrimRight(c.conn.URLs[0], "/"), nil)
if err != nil {
return err
}
c.authorize(req)
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
return fmt.Errorf("opensearch returned %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return nil
}

func (c *openSearchClient) Search(ctx context.Context, index string, limit int, query map[string]any) (map[string]any, error) {
if limit <= 0 {
limit = 100
}
body, err := json.Marshal(query)
if err != nil {
return nil, fmt.Errorf("marshal query: %w", err)
}
base := strings.TrimRight(c.conn.URLs[0], "/")
url := base + "/" + strings.Trim(index, "/") + "/_search"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("size", fmt.Sprintf("%d", limit))
req.URL.RawQuery = q.Encode()
req.Header.Set("Content-Type", "application/json")
c.authorize(req)
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("execute search: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read search response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("opensearch search returned %s: %s", resp.Status, strings.TrimSpace(string(respBody)))
}
var result map[string]any
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("decode search response: %w", err)
}
return result, nil
}

func (c *openSearchClient) authorize(req *http.Request) {
if c.conn.Username != "" || c.conn.Password != "" {
req.SetBasicAuth(c.conn.Username, c.conn.Password)
}
}

func uniqueStrings(values []string) []string {
seen := map[string]struct{}{}
out := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
out = append(out, value)
}
return out
}
27 changes: 27 additions & 0 deletions opensearch/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module github.com/flanksource/mission-control-plugins/opensearch

go 1.26.1

require (
github.com/flanksource/incident-commander v0.0.1747
gopkg.in/yaml.v3 v3.0.1
)


require (
github.com/fatih/color v1.18.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-plugin v1.8.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oklog/run v1.1.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.80.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
Loading
Loading