Backend Services

Each extension backend is a Go HTTP server that queries the underlying system and exposes a JSON API consumed by the UI via ArgoCD’s proxy.

HTTP handlers

Use Go 1.22+ routing patterns:

mux := http.NewServeMux()
mux.HandleFunc("GET /api/v1/metrics", s.handleMetrics)
mux.HandleFunc("GET /api/v1/logs", s.handleLogs)
mux.HandleFunc("GET /api/v1/logs/label/{name}/values", s.handleLabelValues)

ArgoCD identity headers

Backend services receive identity headers from ArgoCD’s proxy. Use them for authorization and scoping:

func userFromRequest(r *http.Request) (username, userID string, groups []string) {
    username = r.Header.Get("Argocd-Username")
    userID = r.Header.Get("Argocd-User-Id")
    groups = strings.Split(r.Header.Get("Argocd-User-Groups"), ",")
    return
}

func clusterFromRequest(r *http.Request) (name, url string) {
    name = r.Header.Get("Argocd-Target-Cluster-Name")
    url = r.Header.Get("Argocd-Target-Cluster-URL")
    return
}

Namespace authorization

Every ArgoPlane backend enforces namespace scoping. The Argocd-Application-Name header (format namespace:name, set by ArgoCD’s proxy) identifies the calling Application. Before serving data, the backend fetches that Application via the Kubernetes API and checks that the requested namespace is one the Application manages: the destination namespace plus any namespace in status.resources. Anything else gets a 403.

If you build a new backend, reuse this pattern. It needs a ServiceAccount with a ClusterRole granting get on applications.argoproj.io (the Helm chart wires this up per extension via serviceAccount.create and rbac.rules).

Configuration

Use envconfig struct tags. Load in main(), pass values to constructors:

type Config struct {
    Port          string `envconfig:"PORT" default:"8080"`
    PrometheusURL string `envconfig:"PROMETHEUS_URL" required:"true"`
    LogLevel      string `envconfig:"LOG_LEVEL" default:"info"`
}

Error handling

Always wrap errors with context:

if err != nil {
    return nil, fmt.Errorf("failed to fetch flows: %w", err)
}

Logging

Use log/slog with structured key-value pairs:

slog.Info("querying prometheus", "url", config.PrometheusURL, "query", query)
slog.Error("failed to fetch flows", "error", err, "namespace", ns)

Import organization

Three groups separated by blank lines: stdlib, external, internal:

import (
    "context"
    "fmt"
    "net/http"

    "github.com/prometheus/client_golang/api"

    "github.com/natrontech/argoplane/extensions/metrics/internal/query"
)

Graceful shutdown

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

srv := &http.Server{Addr: ":" + config.Port, Handler: mux}

go func() {
    <-ctx.Done()
    srv.Shutdown(context.Background())
}()

srv.ListenAndServe()

Naming conventions

  • Constructors: New<Type>(...) (*Type, error)
  • Method receivers: short names (s *Server, c *Client)
  • No Get/List prefixes: Flows() not GetFlows()
  • Packages: single lowercase word (metrics, logs, proxy)