aastro package - github.com/starwalkn/aastro - Go Packages

aastro

package module
v0.10.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 27, 2026 License: Apache-2.0 Imports: 45 Imported by: 0

README

Aastro API Gateway

A lightweight, modular, high-performance API Gateway for modern microservices - parallel fan-out, declarative response aggregation, and .so plugins, configured in YAML.

Go Version License GitHub release Docker Pulls Coverage Status

Documentation · Configuration reference · Changelog · Discussions

Status: 0.x. The gateway is used in real deployments and every release is documented, but the configuration schema is still allowed to change in minor versions. Breaking changes are always called out in the changelog with a migration note. Pin an exact image tag.


What it does

One HTTP request in, several upstream calls out in parallel, one response back - merged, namespaced, or arrayed, with per-upstream timeouts, retries, and circuit breakers. Everything is described in a YAML file; no code is required to add a route.

# aastro.yaml - a complete, minimal configuration
schema: v1

gateway:
  server:
    port: 7805
  admin:
    port: 9090
    bind_addr: 0.0.0.0        # 127.0.0.1 by default; open it up inside a container

  routing:
    flows:
      - path: /api/v1/customers/{customer_id}
        method: GET

        aggregation:
          strategy: merge     # merge | array | namespace
          best_effort: true   # answer with partial data instead of failing

        upstreams:
          - name: users
            hosts: https://users.internal
            path: /v1/users/{customer_id}
            forward_params: ["customer_id"]
            timeout: 3s

          - name: orders
            hosts: https://orders.internal
            path: /v1/customers/{customer_id}/orders
            forward_params: ["customer_id"]
            timeout: 2s
            policy:
              retry:
                max_retries: 2
                retry_on_statuses: [502, 503, 504]
                backoff_delay: 100ms
$ curl -s localhost:7805/api/v1/customers/42
{
  "id": "42",
  "email": "ada@example.com",
  "orders": [
    { 
      "id": "9001", 
      "total": 1200
    }
  ]
}

If orders is down and best_effort is on, the client gets 206 Partial Content with the X-Partial-Errors header:

HTTP/1.1 206 Partial Content
X-Request-ID: 018f4a2b-7c3d-7e4f-a5b6-c7d8e9f0a1b2
X-Request-Fingerprint: 3f9a1c2b7e4d5061
X-Partial-Errors: UPSTREAM_UNAVAILABLE
Content-Type: application/json; charset=utf-8

{
  "data": {
    "id": "42",
    "email": "ada@example.com"
  }
}

Every configuration option, with comments, lives in sample.config.yaml.


Quick start

Docker
docker run \
  -p 7805:7805 \
  -v "$(pwd)/aastro.yaml:/etc/aastro/config.yaml:ro" \
  starwalkn/aastro:latest

/etc/aastro/config.yaml is the default config path. To mount it elsewhere, point AASTRO_CONFIG at it or pass -c /path/to/config.yaml after the image name.

From source

Building the gateway requires CGO_ENABLED=1 and a C toolchain, because plugins are Go shared objects (-buildmode=plugin).

git clone https://github.com/starwalkn/aastro.git
cd aastro

make all GOOS=<YOUR_OS> GOARCH=<YOUR_ARCH>   # builds .bin/aastro, .bin/aastroctl and the builtin .so files

./.bin/aastro -c aastro.yaml
Validate before you deploy
aastro -t -c aastro.yaml    # parse + validate, exit non-zero on error
aastro -T -c aastro.yaml    # same, plus dump the effective config (defaults applied) to stdout

Features

Routing & aggregation

  • Parallel fan-out to any number of upstreams, bounded by parallel_upstreams
  • merge, array, and namespace aggregation strategies with configurable conflict policy
  • Best-effort mode: 206 Partial Content instead of an all-or-nothing failure
  • Streaming flows for SSE, chunked, and long-lived streaming responses
  • Path parameter extraction and forwarding; header and query allow-lists

Resilience

  • Retries with an idempotency guard - non-idempotent methods are never replayed
  • Circuit breaker per upstream, with state exported as a Prometheus metric
  • Load balancing across hosts: round_robin or least_conns
  • Per-IP sliding-window rate limiting with trusted-proxy-aware client IP resolution
  • Response size limits, status allow-lists, header blacklists

Security

  • TLS and mutual TLS on the inbound data port and per upstream
  • Zero-downtime certificate hot-reload - cert-manager, Vault Agent, and SPIFFE/SPIRE ready
  • Builtin JWT auth middleware
  • Admin port bound to localhost by default and never TLS-terminated

Observability

  • Prometheus exporter or OTLP push for metrics
  • Distributed tracing over OTLP with W3C Trace Context and Baggage propagation
  • X-Request-Fingerprint correlation across logs, metrics, and traces
  • /__health, /__ready, /metrics, and optional /debug/pprof/ on a separate admin port

Extensibility

  • Request- and response-phase plugins, plus per-flow middlewares, loaded as .so files
  • Builtin middlewares: auth, cors, compressor, logger, recoverer
  • Builtin plugins: camelify, snakeify, masker
  • Skeleton generator: aastroctl plugin init

Operations

  • Single YAML file, validated ahead of time by the same loader the gateway uses
  • OpenAPI 3.1/3.0 export and import via aastroctl
  • Multi-arch (amd64/arm64) distroless-style image on chainguard/wolfi-base, running as a non-root user

aastroctl: OpenAPI in both directions

The gateway config is the source of truth, and aastroctl turns it into a spec your clients can consume - or turns someone else's spec into a starting config.

# Generate an OpenAPI 3.1 document from the gateway configuration.
# Statuses are derived from the actual config: 206 only for best-effort fan-out,
# 429 only when the rate limiter is on, 401 only behind the auth middleware.
aastroctl openapi export -c aastro.yaml -o openapi.yaml

# Round-trippable export: embeds x-aastro snapshots (never secrets).
aastroctl openapi export -c aastro.yaml --extensions -o openapi.yaml

# Scaffold a gateway configuration from any OpenAPI 3.x document.
aastroctl openapi import -i openapi.yaml -o aastro.yaml --default-host https://backend.internal

Export output is deterministic and diff-friendly, so the generated spec can live in git and be checked in CI. Import validates its own output - it never emits a config the gateway would reject.


Zero-downtime TLS certificate rotation

Aastro reloads TLS material - on both the inbound data port and outbound upstream connections - without restarting the process, reloading the config, or dropping connections. It watches the certificate directories and atomically swaps the in-memory material when files change.

  • Hands-off with your cert manager. Directory-level watching covers both atomic replacement on a host (write-temp-then-rename) and Kubernetes secret mounts, where projected files rotate via symlink swap.
  • Safe by construction. New handshakes use the new certificate; in-flight connections finish on the old one. If a rotated certificate or CA bundle fails to parse, the previously loaded material stays live - a bad rotation cannot take the listener down.
  • No configuration required. Rotation works on your existing cert_file, key_file, and ca_file paths. There is no flag to enable.
gateway:
  server:
    tls:
      enabled: true
      cert_file: /etc/aastro/server.crt   # rotate this file → picked up automatically
      key_file: /etc/aastro/server.key
      client_auth: require
      client_ca_file: /etc/aastro/client-ca.crt

  routing:
    flows:
      - upstreams:
          - tls:
              enabled: true
              cert_file: /etc/aastro/clients/users.crt   # outbound mTLS, also hot-reloaded
              key_file: /etc/aastro/clients/users.key
              ca_file: /etc/aastro/internal-ca.crt

Plugins

A plugin is an ordinary Go package built with -buildmode=plugin that exports a NewPlugin (or NewMiddleware) factory:

aastroctl plugin init --type response --name tenant_masker --author you
CGO_ENABLED=1 go build -buildmode=plugin -trimpath -o /etc/aastro/plugins/tenant_masker.so ./tenant_masker
plugins:
  - name: tenant_masker
    source: file            # builtin | file
    path: /etc/aastro/plugins/    # directory; <name>.so is resolved inside it
    config:
      header: X-Tenant-Id

Plugins must be compiled with the exact Go version and dependency set used for the gateway binary - Go's plugin ABI is unforgiving. See the plugin guide and CONTRIBUTING.md.


Roadmap

Development is driven by demonstrated demand rather than a fixed feature list. Open a discussion or upvote an existing issue - that is genuinely how the next milestone gets picked.


Contributing

Bug reports, plugins, benchmarks, and documentation fixes are all welcome. Start with CONTRIBUTING.md.

License

Apache-2.0 - see LICENSE.


Made with ❤️ in Go

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ValidateConfig added in v0.8.0

func ValidateConfig(cfg *Config) error

func WriteError

func WriteError(w http.ResponseWriter, code ClientError, status int)

WriteError writes a single-cause RFC 9457 Problem Details response: rate limiting, request-size limits, plugin failures, and any other gateway-side rejection that never reaches an upstream at all.

Types

type AddrList

type AddrList []string

func (*AddrList) UnmarshalYAML

func (a *AddrList) UnmarshalYAML(value *yaml.Node) error

type AdminConfig

type AdminConfig struct {
	Port          int           `yaml:"port"    validate:"required,min=1,max=65535"`
	BindAddr      string        `yaml:"bind_addr" default:"127.0.0.1"`
	Timeout       time.Duration `yaml:"timeout"   default:"5m"`
	HeaderTimeout time.Duration `yaml:"header_timeout" default:"5s"`
	EnablePprof   bool          `yaml:"enable_pprof" default:"false"`
}

type AggregationConfig

type AggregationConfig struct {
	BestEffort bool              `yaml:"best_effort"`
	Strategy   string            `yaml:"strategy"    validate:"required,oneof=array merge namespace"`
	OnConflict *OnConflictConfig `yaml:"on_conflict" validate:"required_if=Strategy merge"`
}

type CircuitBreakerConfig

type CircuitBreakerConfig struct {
	Enabled      bool          `yaml:"enabled"`
	MaxFailures  int           `yaml:"max_failures"`
	ResetTimeout time.Duration `yaml:"reset_timeout"`
}

type ClientError

type ClientError string
const (
	ClientErrRateLimitExceeded    ClientError = "RATE_LIMIT_EXCEEDED"
	ClientErrPayloadTooLarge      ClientError = "PAYLOAD_TOO_LARGE"
	ClientErrUpstreamBodyTooLarge ClientError = "UPSTREAM_BODY_TOO_LARGE"
	ClientErrUpstreamUnavailable  ClientError = "UPSTREAM_UNAVAILABLE"
	ClientErrUpstreamError        ClientError = "UPSTREAM_ERROR"
	ClientErrUpstreamClientError  ClientError = "UPSTREAM_CLIENT_ERROR"
	ClientErrUpstreamRedirect     ClientError = "UPSTREAM_REDIRECT"
	ClientErrUpstreamMalformed    ClientError = "UPSTREAM_MALFORMED"
	ClientErrInternal             ClientError = "INTERNAL"
	ClientErrAborted              ClientError = "ABORTED"
	ClientErrValueConflict        ClientError = "VALUE_CONFLICT"
	ClientErrUnauthorized         ClientError = "UNAUTHORIZED"
)

func (ClientError) String

func (err ClientError) String() string

type Config

type Config struct {
	Schema  string        `yaml:"schema" validate:"required,oneof=v1"`
	Debug   bool          `yaml:"debug"`
	Gateway GatewayConfig `yaml:"gateway" validate:"required"`
}

func LoadConfig

func LoadConfig(path string) (Config, error)

LoadConfig reads, parses, applies defaults, validates, and returns the config.

func (*Config) Marshal

func (c *Config) Marshal() ([]byte, error)

func (*Config) WriteTo

func (c *Config) WriteTo(w io.Writer) (int64, error)

type FlowConfig

type FlowConfig struct {
	Path      string `yaml:"path"   validate:"required,startswith=/"`
	Method    string `yaml:"method" validate:"required,oneof=GET POST PUT PATCH DELETE HEAD OPTIONS QUERY"`
	Streaming bool   `yaml:"streaming"`

	// Aggregation is required only for flows with more than one upstream -
	// a single-upstream flow is proxied directly and never aggregates
	// (enforced in validateFlows, since that depends on len(Upstreams)).
	Aggregation *AggregationConfig `yaml:"aggregation"`
	Upstreams   []UpstreamConfig   `yaml:"upstreams"    validate:"required,min=1,dive,required"`
	Plugins     []PluginConfig     `yaml:"plugins"      validate:"omitempty,dive"`
	Middlewares []MiddlewareConfig `yaml:"middlewares"  validate:"omitempty,dive"`
}

type GatewayConfig

type GatewayConfig struct {
	Service       ServiceConfig       `yaml:"service"`
	Server        ServerConfig        `yaml:"server"  validate:"required"`
	Admin         AdminConfig         `yaml:"admin" validate:"required"`
	Observability ObservabilityConfig `yaml:"observability"`
	Routing       RoutingConfig       `yaml:"routing" validate:"required"`
}

type LoadBalancingConfig

type LoadBalancingConfig struct {
	Mode string `yaml:"mode"`
}

type MetricsConfig

type MetricsConfig struct {
	Enabled  bool       `yaml:"enabled"`
	Exporter string     `yaml:"exporter" validate:"required_if=Enabled true,omitempty,oneof=otlp prometheus"`
	OTLP     OTLPConfig `yaml:"otlp"`
}

type MiddlewareConfig

type MiddlewareConfig struct {
	Name   string                 `yaml:"name"   validate:"required"`
	Source string                 `yaml:"source" validate:"required,oneof=builtin file"`
	Path   string                 `yaml:"path"   validate:"required_if=Source file,omitempty"`
	Config map[string]interface{} `yaml:"config"`
}

type OTLPConfig

type OTLPConfig struct {
	Endpoint string        `yaml:"endpoint"`
	Insecure bool          `yaml:"insecure"`
	Interval time.Duration `yaml:"interval"`
}

type ObservabilityConfig

type ObservabilityConfig struct {
	Tracing TracingConfig `yaml:"tracing"`
	Metrics MetricsConfig `yaml:"metrics"`
}

type OnConflictConfig

type OnConflictConfig struct {
	Policy   string `yaml:"policy"          validate:"oneof=overwrite error first prefer"`
	Upstream string `yaml:"prefer_upstream" validate:"required_if=Policy prefer"`
}

type PluginConfig

type PluginConfig struct {
	Name   string                 `yaml:"name"   validate:"required"`
	Source string                 `yaml:"source" validate:"required,oneof=builtin file"`
	Path   string                 `yaml:"path"   validate:"required_if=Source file"`
	Config map[string]interface{} `yaml:"config"`
}

type PolicyConfig

type PolicyConfig struct {
	HeaderBlacklist     []string `yaml:"header_blacklist"`
	RequireBody         bool     `yaml:"require_body"`
	MaxResponseBodySize int64    `yaml:"max_response_body_size"`
	FollowRedirects     bool     `yaml:"follow_redirects"`

	RetryConfig          RetryConfig          `yaml:"retry"`
	CircuitBreakerConfig CircuitBreakerConfig `yaml:"circuit_breaker"`
	LoadBalancingConfig  LoadBalancingConfig  `yaml:"load_balancing"`
}

type ProblemDetails added in v0.10.0

type ProblemDetails struct {
	// Type is always "about:blank" (RFC 9457 §4.2's own default for "no
	// further-specific type"): it is never a dereferencable URI on purpose.
	// A real one - even a stable, non-existent one under the gateway's own
	// docs domain - names the software fronting this upstreams to anyone
	// who receives an error, which is itself reconnaissance: it tells a
	// caller they're behind an aggregating gateway and invites probing for
	// what that implies about the backend topology. Errors carries the
	// actual discriminator instead - a closed, generic enum that says
	// nothing about what's behind the gateway.
	Type   string `json:"type"`
	Title  string `json:"title"`
	Status int    `json:"status"`
	Detail string `json:"detail,omitempty"`
	// Errors lists every distinct underlying ClientError - always at least
	// one. This is the one machine-readable discriminator (Type is constant,
	// see above); Title is for humans and may change wording over time.
	// More than one entry only for a multi-upstream flow where several
	// upstreams failed differently. This is a problem type extension member
	// (RFC 9457 §3.2), not a spec violation.
	Errors []ClientError `json:"errors"`
}

ProblemDetails is the RFC 9457 ("Problem Details for HTTP APIs") body used for every gateway-authored response that carries no upstream data of its own - rate limiting, payload limits, plugin failures, and upstream/gateway failures that never produced anything worth returning as data.

A response that *does* carry data (a full or partial aggregation success) is never wrapped: the body is the aggregated payload itself, exactly as a client of the upstream(s) directly would see it. See Router.buildResponse.

type RateLimiterConfig

type RateLimiterConfig struct {
	Enabled bool                   `yaml:"enabled"`
	Config  map[string]interface{} `yaml:"config" validate:"required"`
}

type RetryConfig

type RetryConfig struct {
	MaxRetries      int           `yaml:"max_retries"`
	RetryOnStatuses []int         `yaml:"retry_on_statuses"`
	BackoffDelay    time.Duration `yaml:"backoff_delay"`
}

type Router

type Router struct {
	// contains filtered or unexported fields
}

func (*Router) Close

func (r *Router) Close() error

func (*Router) ServeHTTP

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

ServeHTTP handles incoming HTTP requests through the full router pipeline:

  1. Rate limiting - rejects requests exceeding the configured limit.
  2. Flow matching - chi router finds the flow by method and path (404 if none).
  3. Middleware execution - per-flow middlewares wrap the handler.
  4. Request plugins - run before the upstream call; may modify the request.
  5. Upstream dispatch - a streaming flow is piped through unbuffered (handleStreaming); a single-upstream flow is called directly and its status/headers/body are proxied as-is (buildProxyResponse); a multi-upstream flow fans out and aggregates (merge/array/namespace, with bestEffort support).
  6. Response plugins - run after dispatch; may modify headers or body.
  7. Response writing - status, headers, and body sent to the client.

A single-upstream flow forwards the upstream's own status/body verbatim on success or on a client error/redirect. A multi-upstream flow's success or partial-success (206, bestEffort) body is the aggregated data itself, with no gateway-added wrapper - a client never has to unwrap a response to get at the payload. Only a response with no data at all (a hard failure with nothing to aggregate, a rejected request that never reached an upstream) is a body, and that body is an RFC 9457 Problem Details document (application/problem+json), not a bespoke shape. Status codes: 200 on full success, 206 on partial, 502/500 on failure.

type RouterBundle

type RouterBundle struct {
	Router         *Router
	TLSRegistry    *tlsutil.Registry
	MeterProvider  otelcommon.Provider
	TracerProvider otelcommon.Provider
	PromRegistry   *prometheus.Registry // nil unless metrics.exporter == "prometheus"
}

func NewRouter

func NewRouter(ctx context.Context, cfgSet RoutingConfigSet, log *zap.Logger) (RouterBundle, error)

type RoutingConfig

type RoutingConfig struct {
	RateLimiter    RateLimiterConfig `yaml:"rate_limiter" validate:"omitempty"`
	TrustedProxies []string          `yaml:"trusted_proxies"`
	Flows          []FlowConfig      `yaml:"flows" validate:"min=1,dive,required"`
}

type RoutingConfigSet

type RoutingConfigSet struct {
	Routing        RoutingConfig
	Service        ServiceConfig
	ServiceVersion string // injected via ldflags
	Metrics        MetricsConfig
	Tracing        TracingConfig
}

type ServerConfig

type ServerConfig struct {
	Port          int             `yaml:"port"    validate:"required,min=1,max=65535"`
	Timeout       time.Duration   `yaml:"timeout" default:"5s"`
	HeaderTimeout time.Duration   `yaml:"header_timeout" default:"5s"`
	TLS           ServerTLSConfig `yaml:"tls"`
}

type ServerTLSConfig

type ServerTLSConfig struct {
	Enabled      bool   `yaml:"enabled"`
	CertFile     string `yaml:"cert_file"      validate:"required_if=Enabled true"`
	KeyFile      string `yaml:"key_file"       validate:"required_if=Enabled true"`
	MinVersion   string `yaml:"min_version"    default:"1.2" validate:"omitempty,oneof=1.2 1.3"`
	ClientAuth   string `yaml:"client_auth"    default:"none" validate:"omitempty,oneof=require optional none"`
	ClientCAFile string `yaml:"client_ca_file" validate:"required_unless=ClientAuth none"`
}

type ServiceConfig

type ServiceConfig struct {
	Name string `yaml:"name" default:"aastro"`
}

type TLSConfig

type TLSConfig struct {
	Enabled            bool   `yaml:"enabled"`
	CertFile           string `yaml:"cert_file" validate:"required_with=KeyFile"`
	KeyFile            string `yaml:"key_file" validate:"required_with=CertFile"`
	CAFile             string `yaml:"ca_file"`
	ServerName         string `yaml:"server_name"`
	InsecureSkipVerify bool   `yaml:"insecure_skip_verify"`
	MinVersion         string `yaml:"min_version" default:"1.2" validate:"omitempty,oneof=1.2 1.3"`
}

type TracingConfig

type TracingConfig struct {
	Enabled       bool       `yaml:"enabled"`
	Exporter      string     `yaml:"exporter" validate:"required_if=Enabled true,omitempty,oneof=otlp"`
	SamplingRatio float64    `yaml:"sampling_ratio" default:"1.0" validate:"min=0,max=1"`
	OTLP          OTLPConfig `yaml:"otlp"`
}

type TransportConfig

type TransportConfig struct {
	MaxIdleConns        int           `yaml:"max_idle_conns"         default:"100"`
	MaxIdleConnsPerHost int           `yaml:"max_idle_conns_per_host" default:"50"`
	IdleConnTimeout     time.Duration `yaml:"idle_conn_timeout"      default:"90s"`
}

type UpstreamConfig

type UpstreamConfig struct {
	Name    string        `yaml:"name" validate:"required"`
	Hosts   AddrList      `yaml:"hosts" validate:"min=1,dive"`
	Path    string        `yaml:"path"`
	Method  string        `yaml:"method"`
	Timeout time.Duration `yaml:"timeout" default:"3s"`

	ForwardHeaders []string `yaml:"forward_headers"`
	ForwardQueries []string `yaml:"forward_queries"`
	ForwardParams  []string `yaml:"forward_params"`

	Policy    PolicyConfig    `yaml:"policy"`
	Transport TransportConfig `yaml:"transport"`
	TLS       TLSConfig       `yaml:"tls"`
}

func DefaultUpstreamConfig added in v0.8.0

func DefaultUpstreamConfig() UpstreamConfig

Directories

Path Synopsis
builtin
plugins/masker command
cmd
aastro command
aastroctl command
internal
openapi
Package openapi generates OpenAPI 3.x documents from an aastro gateway configuration.
Package openapi generates OpenAPI 3.x documents from an aastro gateway configuration.
otelcommon
Package otelcommon builds the shared OpenTelemetry Resource used by both the metric and tracing providers, so signals from the same process share a consistent identity (service.name, service.version, host, process, …) in the observability backend.
Package otelcommon builds the shared OpenTelemetry Resource used by both the metric and tracing providers, so signals from the same process share a consistent identity (service.name, service.version, host, process, …) in the observability backend.
testutil/certgen
Package certgen provides in-process certificate generation and TLS probes for tests.
Package certgen provides in-process certificate generation and TLS probes for tests.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL