gw-08 step 01 — A minimal xDS control plane driving a real Envoy

Goal

Build an xDS server with go-control-plane that serves a listener, route, cluster, and endpoints to a real Envoy, and watch config flow from your Go process into the live proxy.

Code — src/go/snapshot.go

package cp

import (
	cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
	core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
	endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
	listener "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
	route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
	"github.com/envoyproxy/go-control-plane/pkg/cache/types"
	"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
	"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
)

// MakeSnapshot builds a consistent set of {cluster, endpoints, route,
// listener} at a version. Everything references "playback".
func MakeSnapshot(version string, endpoints []string) *cache.Snapshot {
	const clusterName = "playback"

	cl := &cluster.Cluster{
		Name:                 clusterName,
		ClusterDiscoveryType: &cluster.Cluster_Type{Type: cluster.Cluster_EDS},
		EdsClusterConfig: &cluster.Cluster_EdsClusterConfig{
			EdsConfig: &core.ConfigSource{ // endpoints come via EDS (ADS)
				ConfigSourceSpecifier: &core.ConfigSource_Ads{Ads: &core.AggregatedConfigSource{}},
			},
		},
		LbPolicy: cluster.Cluster_LEAST_REQUEST, // P2C (gw-06)
	}

	eds := makeEndpoints(clusterName, endpoints) // []*endpoint.ClusterLoadAssignment
	rt := makeRoute("local_route", clusterName)  // *route.RouteConfiguration
	ls := makeHTTPListener("listener_0", "local_route", 10000) // *listener.Listener

	snap, _ := cache.NewSnapshot(version, map[resource.Type][]types.Resource{
		resource.ClusterType:  {cl},
		resource.EndpointType: {eds},
		resource.RouteType:    {rt},
		resource.ListenerType: {ls},
	})
	return snap
}

func makeEndpoints(clusterName string, addrs []string) *endpoint.ClusterLoadAssignment {
	var lbs []*endpoint.LbEndpoint
	for _, a := range addrs {
		host, port := splitHostPort(a)
		lbs = append(lbs, &endpoint.LbEndpoint{
			HostIdentifier: &endpoint.LbEndpoint_Endpoint{Endpoint: &endpoint.Endpoint{
				Address: &core.Address{Address: &core.Address_SocketAddress{
					SocketAddress: &core.SocketAddress{
						Address:       host,
						PortSpecifier: &core.SocketAddress_PortValue{PortValue: port},
					}}},
			}},
		})
	}
	return &endpoint.ClusterLoadAssignment{
		ClusterName: clusterName,
		Endpoints:   []*endpoint.LocalityLbEndpoints{{LbEndpoints: lbs}},
	}
}

Code — src/go/server.go (the xDS gRPC server)

package cp

import (
	"context"
	"net"

	clusterservice "github.com/envoyproxy/go-control-plane/envoy/service/cluster/v3"
	discovery "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
	endpointservice "github.com/envoyproxy/go-control-plane/envoy/service/endpoint/v3"
	listenerservice "github.com/envoyproxy/go-control-plane/envoy/service/listener/v3"
	routeservice "github.com/envoyproxy/go-control-plane/envoy/service/route/v3"
	"github.com/envoyproxy/go-control-plane/pkg/cache/v3"
	"github.com/envoyproxy/go-control-plane/pkg/server/v3"
	"google.golang.org/grpc"
)

func Run(ctx context.Context, snapCache cache.SnapshotCache, addr string) error {
	srv := server.NewServer(ctx, snapCache, nil)
	grpcServer := grpc.NewServer()

	// Register the ADS endpoint (aggregates all xDS on one stream).
	discovery.RegisterAggregatedDiscoveryServiceServer(grpcServer, srv)
	// (Per-type services can also be registered for non-ADS Envoys.)
	endpointservice.RegisterEndpointDiscoveryServiceServer(grpcServer, srv)
	clusterservice.RegisterClusterDiscoveryServiceServer(grpcServer, srv)
	routeservice.RegisterRouteDiscoveryServiceServer(grpcServer, srv)
	listenerservice.RegisterListenerDiscoveryServiceServer(grpcServer, srv)

	lis, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return grpcServer.Serve(lis)
}

Code — main.go

func main() {
	snapCache := cache.NewSnapshotCache(true, cache.IDHash{}, nil) // ADS=true
	const nodeID = "edge-envoy-1"

	snap := cp.MakeSnapshot("v1", []string{"127.0.0.1:9001", "127.0.0.1:9002"})
	_ = snap.Consistent() // verify clusters/endpoints/routes line up
	_ = snapCache.SetSnapshot(context.Background(), nodeID, snap)

	cp.Run(context.Background(), snapCache, ":18000")
}

Envoy bootstrap (points Envoy at your control plane)

envoy-bootstrap.yaml:

node: { id: edge-envoy-1, cluster: edge }
dynamic_resources:
  ads_config:
    api_type: GRPC
    transport_api_version: V3
    grpc_services: [{ envoy_grpc: { cluster_name: xds_cluster } }]
  cds_config: { ads: {}, resource_api_version: V3 }
  lds_config: { ads: {}, resource_api_version: V3 }
static_resources:
  clusters:
  - name: xds_cluster
    type: STRICT_DNS
    typed_extension_protocol_options:   # h2 to the control plane
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config: { http2_protocol_options: {} }
    load_assignment:
      cluster_name: xds_cluster
      endpoints: [{ lb_endpoints: [{ endpoint: { address: { socket_address: { address: 127.0.0.1, port_value: 18000 }}}}]}]
admin: { address: { socket_address: { address: 127.0.0.1, port_value: 9901 }}}
go run .                                   # control plane on :18000
func-e run -c envoy-bootstrap.yaml         # Envoy fetches config via ADS
# two mock origins:
python3 -m http.server 9001 & python3 -m http.server 9002 &

# Traffic flows through Envoy (listener on :10000) to the origins:
curl -v localhost:10000/

# See what Envoy ACTUALLY applied (and the version it ACKed):
curl -s localhost:9901/config_dump | jq '.. | .version_info? // empty' | sort -u
curl -s localhost:9901/clusters     # endpoints Envoy got via EDS

Tasks

  1. Build the control plane, boot Envoy against it, and curl through Envoy to the origins — config came entirely from your Go process.
  2. In Envoy's /config_dump, find the version_info and confirm it matches the "v1" you set (Envoy ACKed it).
  3. Break the snapshot on purpose (route references a nonexistent cluster); confirm snap.Consistent() catches it, or that Envoy NACKs and /config_dump stays on the last good version.

Acceptance

  • A request through Envoy reaches an origin using only dynamically- pushed config (no static routes/clusters).
  • You can read the applied version from Envoy's admin and tie it to your snapshot version.
  • A bad snapshot is rejected (consistency check or NACK), and Envoy keeps serving last-known-good.

Discussion prompts

  • Where is the control plane relative to the request path, and what happens to live traffic if you kill your Go process? (Last-known- good.)
  • Why does the snapshot bundle clusters + endpoints + routes + listeners at one version? What goes wrong if endpoints update without the cluster?
  • What does a NACK mean operationally, and why must you alert on it?