Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
89eda90
feat: integration of gateway api
jvlxz Jun 5, 2026
4a2c112
fix: use proper golang version
jvlxz Jun 5, 2026
ed7e9a6
fix: ci
jvlxz Jun 5, 2026
2024bea
feat: document Gateway API RBAC and TLS secrets
jvlxz Jun 8, 2026
840b021
fix: honor HTTPRoute policy precedence over Ingress
jvlxz Jun 8, 2026
b0fcff0
feat: implement gateway upstream resolution precedence
jvlxz Jun 10, 2026
62a762b
fix: merge same-host policy variants
jvlxz Jul 1, 2026
dcf94fc
feat: Ignore maintenance ingresses when others exist
jvlxz Jul 2, 2026
57f2726
docs: add YggdrasilPolicy CRD RBAC rule to getting started guide
jvlxz Jun 10, 2026
3bb1739
feat: migrate to SourceRoute types and update tests
jvlxz Jun 10, 2026
c4340fe
feat: add Yggdrasil v1alpha1 API and generated clients
jvlxz Jun 10, 2026
91fcb9c
feat: update readme
jvlxz Jun 25, 2026
3d717d4
fix: merge same-host policy variants
jvlxz Jul 1, 2026
adf45d7
fix: handle gateway host conflicts precisely
jvlxz Jul 2, 2026
97ff505
Merge pull request #19 from numberly/feat/gateway-address-discovery
jvlxz Jul 3, 2026
7544082
fix: preserve "Ingress" source_kind metric label
jvlxz Jul 3, 2026
7721522
fix: skip empty policy signatures in conflict handling
jvlxz Jul 3, 2026
3cb5944
Merge pull request #18 from numberly/feat/crd-transition
jvlxz Jul 3, 2026
d680979
fix: skip gateway informers when gateway api crds absent
jvlxz Jul 3, 2026
21600f7
Merge pull request #20 from numberly/fix/gateway-informer-crd-guard
jvlxz Jul 3, 2026
a78bd57
fix: honor HTTPRoute TLS precedence and drop upstream SNI regression
jvlxz Jul 3, 2026
7362f9e
Merge pull request #21 from numberly/fix/tls-precedence-and-sni
jvlxz Jul 3, 2026
6bf63d7
docs: add OpenWiki documentation wiki
SoulKyu Jul 6, 2026
90c6874
Merge pull request #22 from numberly/docs/openwiki
jvlxz Jul 6, 2026
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
84 changes: 44 additions & 40 deletions .github/workflows/push.yaml
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
name: push

on: push

permissions:
contents: read
id-token: write

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: "1.17"
- run: make test
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- run: make test

build:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: "1.17"
- run: make build-linux
- uses: actions/upload-artifact@v4
with:
name: bin
path: bin/
- uses: actions/checkout@v4
- uses: actions/setup-go@v6
with:
go-version-file: go.mod
- run: make build-linux
- uses: actions/upload-artifact@v4
with:
name: bin
path: bin/

docker-build-push:
if: github.ref_name == 'master' || startsWith(github.ref, 'refs/tags/v')
needs: build
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: bin
path: bin/
- name: Login to Quay.io
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- id: meta
uses: docker/metadata-action@v5
with:
images: quay.io/uswitch/yggdrasil
tags: |
type=semver,pattern={{raw}}
type=sha,prefix=,format=long,
- uses: docker/build-push-action@v5
with:
context: .
labels: ${{ steps.meta.outputs.labels }}
push: true
tags: ${{ steps.meta.outputs.tags }}

- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: bin
path: bin/
- name: Login to Quay.io
uses: docker/login-action@v3
with:
registry: quay.io
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- id: meta
uses: docker/metadata-action@v5
with:
images: quay.io/uswitch/yggdrasil
tags: |
type=semver,pattern={{raw}}
type=sha,prefix=,format=long,
- uses: docker/build-push-action@v5
with:
context: .
labels: ${{ steps.meta.outputs.labels }}
push: true
tags: ${{ steps.meta.outputs.tags }}
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
golang 1.17.13
golang 1.25.0
61 changes: 61 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Agent instructions

## OpenWiki

This repository has documentation located in the /openwiki directory.

Start here:
- [OpenWiki quickstart](openwiki/quickstart.md)

OpenWiki includes repository overview, architecture notes, workflows, domain concepts, operations, integrations, testing guidance, and source maps.

When working in this repository, read the OpenWiki quickstart first, then follow its links to the relevant architecture, workflow, domain, operation, and testing notes.

## Project overview

- Yggdrasil is a Go-based Envoy control plane.
- It watches Kubernetes routing resources across one or more clusters.
- It generates Envoy xDS configuration for listeners, routes, clusters, endpoints, TLS, health checks, retries, timeouts, access logs, and related traffic behavior.
- The project historically uses Kubernetes Ingresses plus Yggdrasil annotations.
- The project is also adding Gateway API support, especially HTTPRoute and YggdrasilPolicy.
- Treat Kubernetes objects as inputs that are converted into Yggdrasil's internal routing and policy model before Envoy configuration is produced.
- Important flow:
- Kubernetes resources are watched and aggregated in `pkg/k8s`.
- Source routes and policy sources are normalized into internal models.
- `pkg/envoy` converts those models into Envoy xDS snapshots.
- `cmd` wires configuration, Kubernetes clients, the aggregator, the snapshotter, and the gRPC/health servers.
- When changing behavior, follow the data from Kubernetes input to internal model to Envoy output.
- Prefer fixing the shared conversion/model layer over patching one caller.

## Keep it small

- Make the smallest correct change.
- Reuse existing code and patterns before adding new helpers or abstractions.

## Domain language

- Read `CONTEXT.md` before changing routing, policy, Gateway API, Kubernetes, or Envoy behavior.
- Use `CONTEXT.md` canonical terms in code, tests, docs, and PR text.

## Living document

- When the user corrects an agent, points out a repeated mistake, or contradicts an instruction, update this file with the smallest rule that prevents the same mistake in future sessions.

## Go style

- Keep code self-explanatory.
- Do not add comments unless required for exported Go API documentation or generated-code/tooling directives.
- Do not add a new dependency without explicit approval.
- Prefer the standard library and existing dependencies.

## Tests

- Run `make test` after any change to Go code, `go.mod`, `go.sum`, or behavior-affecting configuration.

## Documentation

- Update `README.md` or `docs/` in the same change when public behavior, flags, annotations, CRDs, or configuration semantics change.

## Git

- Do not commit, push, or open a pull request unless explicitly asked.
35 changes: 35 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Yggdrasil Context

## Routing Configuration

**Source Route**: A Kubernetes routing object that Yggdrasil accepts as an input for Envoy configuration. Current source routes are **Ingress** and **HTTPRoute**. _Avoid_: Generic ingress, route object.

**Route Policy**: Yggdrasil-specific traffic behavior attached to a **Source Route**, such as timeouts, health checks, retries, upstream behavior, connection limits, and sticky sessions. _Avoid_: Annotation config, CRD config.

**Policy Source**: The external Kubernetes representation from which a **Route Policy** is derived. Current policy sources are legacy Yggdrasil annotations and **YggdrasilPolicy**. _Avoid_: Policy backend.

**Effective Route Policy**: The single **Route Policy** Yggdrasil applies to a **Source Route** after resolving policy source precedence, fallback, and conflicts. _Avoid_: Merged annotations, final annotations.

**YggdrasilPolicy**: A Kubernetes custom resource that represents a typed **Policy Source** for an **HTTPRoute**. A **YggdrasilPolicy** is not itself the internal **Route Policy**.

**Gateway Address**: The data-plane address and port at which a Gateway implementation is reachable, used by Yggdrasil as upstream endpoints for **Source Routes** attached to that Gateway. Discovered from the cluster rather than statically configured. _Avoid_: Gateway service, upstream service.

## Relationships

An **Ingress** is a **Source Route** whose **Route Policy** may come from legacy Yggdrasil annotations.

An **HTTPRoute** is a **Source Route** whose **Route Policy** may come from a **YggdrasilPolicy** or, during migration, legacy Yggdrasil annotations.

A **Source Route** has at most one **Effective Route Policy**.

A **Policy Source** produces zero or one **Route Policy**.

## Resolved Language

**Dev:** "Should the CRD be converted into annotations?"

**Domain expert:** "No. Annotations and the CRD are policy sources. Both should be parsed into the same Route Policy model."

**Dev:** "Is YggdrasilPolicy the shared model?"

**Domain expert:** "No. YggdrasilPolicy is the Kubernetes custom resource. Route Policy is the shared domain model."
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Yggdrasil

Yggdrasil is an Envoy control plane that configures listeners and clusters based off Kubernetes ingresses from multiple Kube Clusters. This allows you to have an envoy cluster acting as a mutli-cluster loadbalancer for Kubernetes. This was something we needed as we wanted our apps to be highly available in the event of a cluster outage but did not want the solution to live inside of Kubernetes itself.

Yggdrasil also supports Gateway API resources: GatewayClass, Gateway, HTTPRoute, and ReferenceGrant. See [Getting Started](docs/GETTINGSTARTED.md) for RBAC details and [local Gateway API test setup](hack/local-gateway-test/README.md) for a local example.

`Note:` Currently we support versions 1.20.x to 1.34.x of Envoy.</br>
`Note:` Yggdrasil now uses [Go modules](https://github.com/golang/go/wiki/Modules) to handle dependencies.

Expand Down
19 changes: 10 additions & 9 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import (
)

type clusterConfig struct {
APIServer string `json:"apiServer"`
Ca string `json:"ca"`
Token string `json:"token"`
TokenPath string `json:"tokenPath"`
Maintenance bool `json:"maintenance"`
KubernetesClusterName string `json:"kubernetesClusterName"`
APIServer string `json:"apiServer"`
Ca string `json:"ca"`
Token string `json:"token"`
TokenPath string `json:"tokenPath"`
Maintenance bool `json:"maintenance"`
KubernetesClusterName string `json:"kubernetesClusterName"`
GatewayClasses []k8s.GatewayClassConfig `json:"gatewayClasses"`
}

type config struct {
Expand Down Expand Up @@ -323,7 +324,7 @@ func createSources(clusters []clusterConfig) ([]k8s.KubernetesConfig, error) {
return nil, err
}

kubernetesConfig := k8s.NewKubernetesConfig(cluster.Maintenance, clientSet, cluster.KubernetesClusterName)
kubernetesConfig := k8s.NewKubernetesConfig(cluster.Maintenance, clientSet, config, cluster.KubernetesClusterName, cluster.GatewayClasses...)

envoy.KubernetesClusterInMaintenance.WithLabelValues(cluster.APIServer).Set(float64(0))

Expand All @@ -337,7 +338,7 @@ func createSources(clusters []clusterConfig) ([]k8s.KubernetesConfig, error) {
sources = append(sources, *kubernetesConfig)
}

if allInMaintenance {
if len(clusters) > 0 && allInMaintenance {
log.Fatal("All clusters are in maintenance mode")
}

Expand All @@ -357,7 +358,7 @@ func configFromKubeConfig(paths []string) ([]k8s.KubernetesConfig, error) {
return sources, err
}

kubernetesConfig := k8s.NewKubernetesConfig(false, clientSet, "")
kubernetesConfig := k8s.NewKubernetesConfig(false, clientSet, config, "")

sources = append(sources, *kubernetesConfig)
}
Expand Down
125 changes: 125 additions & 0 deletions deploy/crds/yggdrasil.uswitch.com_yggdrasilpolicies.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: yggdrasilpolicies.yggdrasil.uswitch.com
spec:
group: yggdrasil.uswitch.com
names:
kind: YggdrasilPolicy
listKind: YggdrasilPolicyList
plural: yggdrasilpolicies
singular: yggdrasilpolicy
scope: Namespaced
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
required: ["spec"]
properties:
spec:
type: object
required: ["targetRef"]
properties:
targetRef:
type: object
required: ["group", "kind", "name"]
description: >-
The same-namespace HTTPRoute this policy applies to. Only
group gateway.networking.k8s.io and kind HTTPRoute are
valid targets.
properties:
group:
type: string
enum: ["gateway.networking.k8s.io"]
kind:
type: string
enum: ["HTTPRoute"]
name:
type: string
minLength: 1
healthCheck:
type: object
properties:
path:
type: string
host:
type: string
description: >-
Overrides the health check host, used with wildcard
route hosts.
timeouts:
type: object
description: Go duration strings, e.g. "30s" or "1m30s".
properties:
default:
type: string
description: Sets route, per-try and cluster timeouts at once.
route:
type: string
perTry:
type: string
cluster:
type: string
retry:
type: object
properties:
retryOn:
type: array
items:
type: string
enum:
- "5xx"
- "gateway-error"
- "reset"
- "connect-failure"
- "envoy-ratelimited"
- "retriable-4xx"
- "refused-stream"
- "retriable-status-codes"
- "retriable-headers"
- "http3-post-connect-failure"
upstream:
type: object
properties:
httpVersion:
type: string
enum: ["1.1", "2"]
weight:
type: integer
format: int32
minimum: 0
description: >-
Load balancing weight of this route's upstreams. An
explicit 0 excludes the upstreams.
connection:
type: object
properties:
idleTimeout:
type: string
maxConnectionDuration:
type: string
maxRequestsPerConnection:
type: integer
format: int32
minimum: 1
stickySessions:
type: object
required: ["enabled"]
properties:
enabled:
type: boolean
cookieName:
type: string
cookiePath:
type: string
cookieTTL:
type: string
description: Go duration string, e.g. "1h".
changeOnFailure:
type: boolean
description: >-
Whether sessions move away from unhealthy backends.
Defaults to true.
Loading
Loading