Consuming Configuration Updates
protoconf provides a gRPC service that allows your application to subscribe to configuration updates. This service is provided by the protoconf agent, which can run in development mode and listen on 0.0.0.0:4300.
There are two ways to read a config:
SubscribeForConfigopens a stream and pushes every update as it lands. This is what long-running services should use.GetConfigreads the current value once and returns. Added in v0.2.0, for jobs, scripts and init containers that do not outlive a single read. It is also reachable over plain HTTP.
This guide will walk you through how to subscribe to configuration updates using the protoconf agent in various languages: Go, Python, Node.js, Rust, and Java.
protoconf Agent
To start the protoconf agent in development mode, use the following command:
protoconf agent -dev .
The protoconf agent implements the following gRPC service:
syntax = "proto3";
package protoconf.v1;
option java_package = "com.protoconf.datatypes.v1";
import "google/protobuf/any.proto";
import "google/api/httpbody.proto";
message ConfigSubscriptionRequest {
string path = 1;
string channel = 2;
}
message ConfigRequest {
string path = 1;
string channel = 2;
}
message ConfigUpdate {
google.protobuf.Any value = 1;
string error = 2;
google.api.HttpBody raw = 3;
}
service ProtoconfService{
rpc SubscribeForConfig(ConfigSubscriptionRequest) returns (stream ConfigUpdate);
rpc GetConfig(ConfigRequest) returns (ConfigUpdate);
}
Two fields are new in v0.2.0. channel on the request selects a
rollout channel for this subscriber, overriding the
agent's own channel. raw on the response carries the config's rendered JSON
alongside the typed value, which is what makes the HTTP endpoint below
possible.
You can use it as a dependency from buf: buf.build/protoconf/protoconf.
Code Generation
Before consuming the configuration updates, you need to generate code from the proto file. A simple solution for this is using buf. Here is the content for buf.yaml and buf.gen.yaml:
buf.yaml:
version: v1
name: buf.build/myusername/myrepository
deps:
- buf.build/protoconf/protoconf
buf.gen.yaml:
version: v1
plugins:
- name: go
out: gen/go
- name: java
out: gen/java
- name: node
out: gen/js
- name: python
out: gen/python
With the above configurations, run buf generate to generate the code for your protobuf files. Now you can consume the configuration updates in your preferred language.
- Go
- Python
- Node.js
- Rust
- Java
In Go, use the grpc package to create a client and subscribe to configuration updates:
package main
import (
"context"
"log"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/golang/protobuf/ptypes"
pb "gen/go/protoconf/v1"
mypb "gen/go/myproject/v1"
)
func main() {
conn, err := grpc.Dial("localhost:4300", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewProtoconfServiceClient(conn)
stream, err := client.SubscribeForConfig(context.Background(), &pb.ConfigSubscriptionRequest{
Path: "myproject/server_config",
})
if err != nil {
log.Fatalf("Failed to subscribe for config: %v", err)
}
for {
configUpdate, err := stream.Recv()
if err != nil {
log.Fatalf("Error receiving config update: %v", err)
}
var config mypb.ServerConfiguration
if err := ptypes.UnmarshalAny(configUpdate.Value, &config); err != nil {
log.Fatalf("Failed to unmarshal config update: %v", err)
}
log.Printf("Received config update: %+v", config)
}
}
In Python, use the grpc package to create a client and subscribe to configuration updates:
from google.protobuf import any_pb2
from grpc import insecure_channel
import protoconf.v1.protoconf_pb2 as pb
import myproject.v1.server_config_pb2 as mypb
with insecure_channel('localhost:4300') as channel:
client = pb.ProtoconfServiceStub(channel)
request = pb.ConfigSubscriptionRequest(path="myproject/server_config")
for config_update in client.SubscribeForConfig(request):
config = mypb.ServerConfiguration()
if config_update.value.Is(config.DESCRIPTOR):
config_update.value.Unpack(config)
print(f"Received config update: {config}")
In Node.js, use the grpc-js package to create a client and subscribe to configuration updates:
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const mypb = require("./gen/js/myproject/v1/server_config_pb.js");
const PROTO_PATH = __dirname + "/gen/js/protoconf/v1/protoconf.proto";
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const client = new protoDescriptor.v1.ProtoconfService(
"localhost:4300",
grpc.credentials.createInsecure()
);
const call = client.SubscribeForConfig({ path: "myproject/server_config" });
call.on("data", (configUpdate) => {
const config = mypb.ServerConfiguration.deserializeBinary(
configUpdate.value.value
);
console.log(`Received config update: ${JSON.stringify(config.toObject())}`);
});
call.on("error", (error) => {
console.error(`Error receiving config update: ${error}`);
});
call.on("end", () => {
console.log("End of config updates");
});
In Rust, use the tonic crate to create a client and subscribe to configuration updates:
use tonic::transport::Channel;
use futures::StreamExt;
use prost::Message;
use protoconf::v1::protoconf_service_client::ProtoconfServiceClient;
use protoconf::v1::ConfigSubscriptionRequest;
use myproject::v1::ServerConfiguration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let channel = Channel::from_static("http://localhost:4300").connect().await?;
let mut client = ProtoconfServiceClient::new(channel);
let request = tonic::Request::new(ConfigSubscriptionRequest {
path: "myproject/server_config".to_string(),
});
let mut stream = client.subscribe_for_config(request).await?.into_inner();
while let Some(config_update) = stream.message().await? {
let config = ServerConfiguration::decode(config_update.value.value.as_slice())?;
println!("Received config update: {:?}", config);
}
Ok(())
}
In Java, use the grpc package to create a client and subscribe to configuration updates:
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.protoconf.agent.api.v1.ProtoconfServiceGrpc;
import com.protoconf.agent.api.v1.ConfigUpdate;
import com.protoconf.agent.api.v1.ConfigSubscriptionRequest;
import myproject.v1.ServerConfiguration;
public class Main {
public static void main(String[] args) throws InterruptedException {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 4300)
.usePlaintext()
.build();
ProtoconfServiceGrpc.ProtoconfServiceBlockingStub stub = ProtoconfServiceGrpc.newBlockingStub(channel);
stub.subscribeForConfig(ConfigSubscriptionRequest.newBuilder().setPath("myproject/server_config").build())
.forEachRemaining(configUpdate -> {
try {
ServerConfiguration config = configUpdate.getValue().unpack(ServerConfiguration.class);
System.out.println("Received config update: " + config);
} catch (InvalidProtocolBufferException e) {
System.err.println("Failed to unpack config update: " + e);
}
});
channel.shutdown();
}
}
In this example, the SubscribeForConfig RPC is used to subscribe for updates to the myproject/server_config configuration. The received ConfigUpdate messages are unpacked into ServerConfiguration objects which can then be used by your application.
These examples provide a starting point for integrating protoconf into your applications. As you adapt these examples to your specific needs, you may find additional resources on the gRPC, protobuf, and protoconf libraries helpful.
One-shot reads
Not every consumer is a long-running service. A migration job, a shell script
or an init container wants the current value and nothing more. GetConfig is
the unary counterpart to SubscribeForConfig:
update, err := client.GetConfig(ctx, &pb.ConfigRequest{
Path: "myproject/server_config",
})
if err != nil {
log.Fatalf("Failed to get config: %v", err)
}
var config mypb.ServerConfiguration
if err := update.Value.UnmarshalTo(&config); err != nil {
log.Fatalf("Failed to unmarshal config: %v", err)
}
A path that does not exist in the store returns NOT_FOUND rather than an
empty value.
Over plain HTTP
The agent transcodes GetConfig onto its admin HTTP listener, so anything that
can make an HTTP request can read a config — no generated stubs, no gRPC
client:
curl http://localhost:4380/v1/config/myproject/server_config
The response body is the config's rendered JSON, served verbatim with
Content-Type: application/json. A missing config returns 404.
initContainers:
- name: fetch-config
image: curlimages/curl
command:
- sh
- -c
- curl -fsS http://protoconf-agent:4380/v1/config/myproject/server_config > /config/server.json
The HTTP endpoint is served by the admin listener, which is unauthenticated and unencrypted. Keep it on an internal network. Anything crossing a trust boundary should use the gRPC API with TLS.
GetConfig always returns the stable config: it does not resolve
rollout stages. Subscribers that need to observe a
rollout should use SubscribeForConfig.