mirror of
https://github.com/netbirdio/netbird.git
synced 2026-07-18 04:29:54 +00:00
[client] Make netbird up wait for the daemon to become ready
The CLI up path only tolerated a not-yet-ready daemon via a 10s blocking dial, so "netbird service start" immediately followed by "netbird up" (e.g. a container entrypoint) failed with a generic "daemon not running" error. The container entrypoint worked around this with a shell poll loop (status --check live) before running up. Move the readiness wait into the CLI, mirroring how the GUI already dials: - DialClientGRPCServer now uses grpc.NewClient with a tuned reconnect backoff and waits for the connection to reach READY (retrying on TRANSIENT_FAILURE) up to a 30s deadline, instead of grpc.DialContext + WithBlock with a hard 10s timeout. - up now polls Status via waitForDaemonStatus, retrying while the RPC is Unavailable (socket up but service not yet registered). Add an explicit daemon-ready signal so clients can wait deterministically instead of heuristically: - New optional StatusResponse.daemonReady field (field 5, wire-compatible with older GUIs/daemons which leave it unset). Regenerated with the pinned protoc v33.1 toolchain so no version churn leaks into the diff. - The server sets ready once Start succeeds and the DaemonService is registered (SetReady, called from the service controller). - waitForDaemonStatus waits for daemonReady=true (or an already-Connected status), with a bounded grace window so older daemons that never set the field are not blocked. Simplify the container entrypoint accordingly: drop the readiness poll loop (up now waits) and the now-dead NB_ENTRYPOINT_SERVICE_TIMEOUT env, keeping only the daemon+up process glue and SIGTERM forwarding for clean shutdown.
This commit is contained in:
@@ -17,8 +17,7 @@ ENV \
|
||||
NETBIRD_BIN="/usr/local/bin/netbird" \
|
||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||
NB_ENABLE_CAPTURE="false" \
|
||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||
NB_ENABLE_CAPTURE="false"
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
@@ -23,8 +23,7 @@ ENV \
|
||||
NB_DAEMON_ADDR="unix:///var/lib/netbird/netbird.sock" \
|
||||
NB_LOG_FILE="console,/var/lib/netbird/client.log" \
|
||||
NB_DISABLE_DNS="true" \
|
||||
NB_ENABLE_CAPTURE="false" \
|
||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||
NB_ENABLE_CAPTURE="false"
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
176
client/cmd/dial_test.go
Normal file
176
client/cmd/dial_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal"
|
||||
"github.com/netbirdio/netbird/client/proto"
|
||||
)
|
||||
|
||||
// startUnixGRPCServer starts a bare gRPC server listening on a unix socket at path
|
||||
// and returns a stop function. No services are registered; the connectivity-state
|
||||
// wait only cares about the transport becoming READY.
|
||||
func startUnixGRPCServer(t *testing.T, path string) func() {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
t.Fatalf("listen unix %s: %v", path, err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return srv.Stop
|
||||
}
|
||||
|
||||
func TestDialClientGRPCServer_ConnectsWhenServing(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "nb.sock")
|
||||
stop := startUnixGRPCServer(t, sock)
|
||||
defer stop()
|
||||
|
||||
conn, err := dialClientGRPCServer(context.Background(), "unix://"+sock, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("expected connection, got error: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if state := conn.GetState(); state != connectivity.Ready {
|
||||
t.Fatalf("expected READY, got %s", state)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDialClientGRPCServer_WaitsForLateServer is the core regression test: the
|
||||
// daemon socket appears only after the dial has already started, mirroring
|
||||
// "netbird service start" immediately followed by "netbird up".
|
||||
func TestDialClientGRPCServer_WaitsForLateServer(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "nb.sock")
|
||||
|
||||
var stop func()
|
||||
timer := time.AfterFunc(1*time.Second, func() {
|
||||
stop = startUnixGRPCServer(t, sock)
|
||||
})
|
||||
defer timer.Stop()
|
||||
defer func() {
|
||||
if stop != nil {
|
||||
stop()
|
||||
}
|
||||
}()
|
||||
|
||||
start := time.Now()
|
||||
conn, err := dialClientGRPCServer(context.Background(), "unix://"+sock, 10*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("expected connection after late server start, got error: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if elapsed := time.Since(start); elapsed < 500*time.Millisecond {
|
||||
t.Fatalf("connected too fast (%s); server should not have been up yet", elapsed)
|
||||
}
|
||||
if state := conn.GetState(); state != connectivity.Ready {
|
||||
t.Fatalf("expected READY, got %s", state)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeStatusServer serves the Status RPC with a programmable response so we can
|
||||
// exercise waitForDaemonStatus without spinning up a real engine.
|
||||
type fakeStatusServer struct {
|
||||
proto.UnimplementedDaemonServiceServer
|
||||
resp func() *proto.StatusResponse
|
||||
}
|
||||
|
||||
func (f *fakeStatusServer) Status(context.Context, *proto.StatusRequest) (*proto.StatusResponse, error) {
|
||||
return f.resp(), nil
|
||||
}
|
||||
|
||||
func startFakeStatusServer(t *testing.T, sock string, resp func() *proto.StatusResponse) func() {
|
||||
t.Helper()
|
||||
lis, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatalf("listen unix %s: %v", sock, err)
|
||||
}
|
||||
srv := grpc.NewServer()
|
||||
proto.RegisterDaemonServiceServer(srv, &fakeStatusServer{resp: resp})
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
return srv.Stop
|
||||
}
|
||||
|
||||
func dialFake(t *testing.T, sock string) proto.DaemonServiceClient {
|
||||
t.Helper()
|
||||
conn, err := dialClientGRPCServer(context.Background(), "unix://"+sock, 5*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("dial fake daemon: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
return proto.NewDaemonServiceClient(conn)
|
||||
}
|
||||
|
||||
// New daemon that flips DaemonReady=true after a couple of polls: waitForDaemonStatus
|
||||
// must block until the flag is set, then return.
|
||||
func TestWaitForDaemonStatus_WaitsForDaemonReady(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "nb.sock")
|
||||
var polls int32
|
||||
stop := startFakeStatusServer(t, sock, func() *proto.StatusResponse {
|
||||
n := atomic.AddInt32(&polls, 1)
|
||||
return &proto.StatusResponse{
|
||||
Status: string(internal.StatusConnecting),
|
||||
DaemonReady: n >= 3, // ready only from the 3rd poll on
|
||||
}
|
||||
})
|
||||
defer stop()
|
||||
|
||||
client := dialFake(t, sock)
|
||||
status, err := waitForDaemonStatus(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !status.GetDaemonReady() {
|
||||
t.Fatalf("expected DaemonReady=true, got false")
|
||||
}
|
||||
if got := atomic.LoadInt32(&polls); got < 3 {
|
||||
t.Fatalf("expected at least 3 polls before ready, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Older daemon that never sets DaemonReady but reports a healthy (Connected)
|
||||
// status: waitForDaemonStatus must return promptly via the readiness fallback,
|
||||
// not block for the whole grace window.
|
||||
func TestWaitForDaemonStatus_OlderDaemonHealthyStatus(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "nb.sock")
|
||||
stop := startFakeStatusServer(t, sock, func() *proto.StatusResponse {
|
||||
return &proto.StatusResponse{Status: string(internal.StatusConnected)} // DaemonReady unset
|
||||
})
|
||||
defer stop()
|
||||
|
||||
client := dialFake(t, sock)
|
||||
start := time.Now()
|
||||
status, err := waitForDaemonStatus(context.Background(), client)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if status.GetDaemonReady() {
|
||||
t.Fatalf("expected DaemonReady=false from older daemon")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 2*time.Second {
|
||||
t.Fatalf("returned too slowly (%s); healthy status should short-circuit the grace", elapsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDialClientGRPCServer_TimesOutWhenAbsent(t *testing.T) {
|
||||
sock := filepath.Join(t.TempDir(), "never.sock")
|
||||
|
||||
start := time.Now()
|
||||
conn, err := dialClientGRPCServer(context.Background(), "unix://"+sock, 1*time.Second)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
t.Fatal("expected timeout error, got nil")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed < 900*time.Millisecond {
|
||||
t.Fatalf("returned too early (%s); should have waited ~timeout", elapsed)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"google.golang.org/grpc"
|
||||
gbackoff "google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
daddr "github.com/netbirdio/netbird/client/internal/daemonaddr"
|
||||
@@ -264,17 +266,70 @@ func FlagNameToEnvVar(cmdFlag string, prefix string) string {
|
||||
return prefix + upper
|
||||
}
|
||||
|
||||
// DialClientGRPCServer returns client connection to the daemon server.
|
||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
// defaultDaemonDialTimeout is how long DialClientGRPCServer waits for the daemon
|
||||
// to become reachable. It is intentionally generous so that invoking the CLI
|
||||
// right after "netbird service start" (e.g. from a container entrypoint) tolerates
|
||||
// the window where the daemon has created its socket but is not yet serving.
|
||||
const defaultDaemonDialTimeout = 30 * time.Second
|
||||
|
||||
return grpc.DialContext(
|
||||
ctx,
|
||||
// DialClientGRPCServer returns a client connection to the daemon server. It waits
|
||||
// for the daemon to become reachable, retrying with backoff until the connection
|
||||
// reports READY or defaultDaemonDialTimeout elapses. This handles the startup race
|
||||
// where the daemon socket exists (or is about to) before the gRPC server is serving.
|
||||
func DialClientGRPCServer(ctx context.Context, addr string) (*grpc.ClientConn, error) {
|
||||
return dialClientGRPCServer(ctx, addr, defaultDaemonDialTimeout)
|
||||
}
|
||||
|
||||
func dialClientGRPCServer(ctx context.Context, addr string, timeout time.Duration) (*grpc.ClientConn, error) {
|
||||
conn, err := grpc.NewClient(
|
||||
strings.TrimPrefix(addr, "tcp://"),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithBlock(),
|
||||
// Cap reconnect backoff at 5s; gRPC's default 120s MaxDelay would leave the
|
||||
// CLI waiting far too long to notice a freshly-started daemon. Mirrors the GUI.
|
||||
grpc.WithConnectParams(grpc.ConnectParams{
|
||||
Backoff: gbackoff.Config{
|
||||
BaseDelay: 1 * time.Second,
|
||||
Multiplier: 1.6,
|
||||
Jitter: 0.2,
|
||||
MaxDelay: 5 * time.Second,
|
||||
},
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create daemon gRPC client: %w", err)
|
||||
}
|
||||
|
||||
// grpc.NewClient is lazy: it does not connect until the first RPC or until we
|
||||
// nudge it. Trigger connection attempts and wait until the channel reaches READY.
|
||||
if err := waitForConnReady(ctx, conn, timeout); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// waitForConnReady drives the gRPC channel out of IDLE and blocks until it becomes
|
||||
// READY, or until timeout/ctx expires. TRANSIENT_FAILURE (daemon not yet serving)
|
||||
// is treated as retryable so the caller keeps waiting within the deadline.
|
||||
func waitForConnReady(ctx context.Context, conn *grpc.ClientConn, timeout time.Duration) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
state := conn.GetState()
|
||||
switch state {
|
||||
case connectivity.Ready:
|
||||
return nil
|
||||
case connectivity.Idle:
|
||||
// Kick the lazy channel into connecting.
|
||||
conn.Connect()
|
||||
}
|
||||
|
||||
if !conn.WaitForStateChange(ctx, state) {
|
||||
// ctx expired while in `state`.
|
||||
return fmt.Errorf("timed out after %s waiting for daemon to become ready (last state: %s)", timeout, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackOff execute function in backoff cycle.
|
||||
|
||||
@@ -78,6 +78,10 @@ func (p *program) Start(svc service.Service) error {
|
||||
log.Fatalf("failed to start daemon: %v", err)
|
||||
}
|
||||
proto.RegisterDaemonServiceServer(p.serv, serverInstance)
|
||||
// The engine is started and the service is registered: from here on the
|
||||
// daemon serves RPCs backed by a running engine. Report readiness so
|
||||
// clients (e.g. netbird up) can wait deterministically.
|
||||
serverInstance.SetReady()
|
||||
|
||||
p.serverInstanceMu.Lock()
|
||||
p.serverInstance = serverInstance
|
||||
|
||||
@@ -295,9 +295,7 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
||||
|
||||
client := proto.NewDaemonServiceClient(conn)
|
||||
|
||||
status, err := client.Status(ctx, &proto.StatusRequest{
|
||||
WaitForReady: func() *bool { b := true; return &b }(),
|
||||
})
|
||||
status, err := waitForDaemonStatus(ctx, client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get daemon status: %v", err)
|
||||
}
|
||||
@@ -336,6 +334,79 @@ func runInDaemonMode(ctx context.Context, cmd *cobra.Command, pm *profilemanager
|
||||
return nil
|
||||
}
|
||||
|
||||
// daemonStatusPollTimeout bounds how long we poll the Status RPC waiting for the
|
||||
// daemon to answer coherently. The transport is already READY at this point (see
|
||||
// DialClientGRPCServer), so this only covers the brief window where the gRPC server
|
||||
// is serving but the daemon engine is still starting up and the Status RPC races
|
||||
// against server.Start().
|
||||
const daemonStatusPollTimeout = 15 * time.Second
|
||||
|
||||
// daemonReadyGrace bounds how long we keep polling once the daemon answers but
|
||||
// still reports DaemonReady=false. A freshly-started daemon flips it to true
|
||||
// within this window; an older daemon that never sets the field simply falls
|
||||
// through after the grace elapses, preserving backward compatibility.
|
||||
const daemonReadyGrace = 10 * time.Second
|
||||
|
||||
// waitForDaemonStatus fetches the daemon status, waiting for the daemon to become
|
||||
// ready. It handles two startup races:
|
||||
//
|
||||
// 1. The gRPC server is not yet serving: Status fails with Unavailable — retry.
|
||||
// 2. The server serves but the engine is still starting: a DaemonReady-aware
|
||||
// daemon reports DaemonReady=false until Start finishes; poll until it flips
|
||||
// true (bounded by daemonReadyGrace). Older daemons never set DaemonReady, so
|
||||
// we stop waiting on it after the grace and use the status as-is.
|
||||
//
|
||||
// It gives up after daemonStatusPollTimeout.
|
||||
func waitForDaemonStatus(ctx context.Context, client proto.DaemonServiceClient) (*proto.StatusResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, daemonStatusPollTimeout)
|
||||
defer cancel()
|
||||
|
||||
waitForReady := true
|
||||
req := &proto.StatusRequest{WaitForReady: &waitForReady}
|
||||
|
||||
var lastErr error
|
||||
var firstAnswer time.Time
|
||||
for {
|
||||
status, err := client.Status(ctx, req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
// Only retry while the daemon is not yet answering; surface real errors.
|
||||
if s, ok := gstatus.FromError(err); !ok || s.Code() != codes.Unavailable {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Daemon answered. Explicitly ready (DaemonReady-aware daemon), or
|
||||
// already fully connected — either way, done. Connected is the only
|
||||
// status unambiguous enough to short-circuit on: a DaemonReady-aware
|
||||
// daemon sets the flag at startup, so trusting Connected here can only
|
||||
// help an older daemon that never sets the flag, without overriding a
|
||||
// new daemon that legitimately reports DaemonReady=false while starting.
|
||||
if status.GetDaemonReady() || internal.StatusType(status.GetStatus()) == internal.StatusConnected {
|
||||
return status, nil
|
||||
}
|
||||
// Answered but neither ready-flagged nor connected yet: give a
|
||||
// DaemonReady-aware daemon a bounded window to finish starting, then
|
||||
// fall through so an older daemon that never sets the flag isn't
|
||||
// blocked here.
|
||||
if firstAnswer.IsZero() {
|
||||
firstAnswer = time.Now()
|
||||
} else if time.Since(firstAnswer) >= daemonReadyGrace {
|
||||
return status, nil
|
||||
}
|
||||
lastErr = nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if lastErr != nil {
|
||||
return nil, fmt.Errorf("daemon did not become ready: %w", lastErr)
|
||||
}
|
||||
return nil, fmt.Errorf("daemon did not become ready within %s", daemonStatusPollTimeout)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doDaemonUp(ctx context.Context, cmd *cobra.Command, client proto.DaemonServiceClient, pm *profilemanager.ProfileManager, activeProf *profilemanager.Profile, customDNSAddressConverted []byte, username string) error {
|
||||
|
||||
providedSetupKey, err := getSetupKey()
|
||||
|
||||
@@ -1,71 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the NetBird daemon and brings the connection up in one container process.
|
||||
#
|
||||
# A thin wrapper is needed (rather than a one-line ENTRYPOINT) for two reasons:
|
||||
# 1. Two processes must run: the daemon (`service run`, long-lived) and a
|
||||
# one-shot `up` that brings the connection up.
|
||||
# 2. Signal handling: as PID 1 the wrapper must forward SIGTERM/SIGINT to the
|
||||
# daemon so it tears down WireGuard and deregisters ephemeral peers on
|
||||
# `docker stop`. Without this the daemon would be killed uncleanly.
|
||||
#
|
||||
# `netbird up` waits for the daemon to become ready on its own, so no readiness
|
||||
# poll is needed here.
|
||||
set -eEuo pipefail
|
||||
|
||||
: ${NB_ENTRYPOINT_SERVICE_TIMEOUT:="30"}
|
||||
NETBIRD_BIN="${NETBIRD_BIN:-"netbird"}"
|
||||
export NB_LOG_FILE="${NB_LOG_FILE:-"console,/var/log/netbird/client.log"}"
|
||||
service_pids=()
|
||||
|
||||
_log() {
|
||||
# mimic Go logger's output for easier parsing
|
||||
# 2025-04-15T21:32:00+08:00 INFO client/internal/config.go:495: setting notifications to disabled by default
|
||||
printf "$(date -Isec) ${1} ${BASH_SOURCE[1]}:${BASH_LINENO[1]}: ${2}\n" "${@:3}" >&2
|
||||
}
|
||||
daemon=""
|
||||
cleanup() { [[ -n "${daemon}" ]] && kill -TERM "${daemon}" 2>/dev/null || true; }
|
||||
trap cleanup SIGTERM SIGINT EXIT
|
||||
|
||||
info() {
|
||||
_log INFO "$@"
|
||||
}
|
||||
"${NETBIRD_BIN}" service run &
|
||||
daemon=$!
|
||||
|
||||
warn() {
|
||||
_log WARN "$@"
|
||||
}
|
||||
"${NETBIRD_BIN}" up
|
||||
|
||||
on_exit() {
|
||||
info "Shutting down NetBird daemon..."
|
||||
if test "${#service_pids[@]}" -gt 0; then
|
||||
info "terminating service process IDs: ${service_pids[@]@Q}"
|
||||
kill -TERM "${service_pids[@]}" 2>/dev/null || true
|
||||
wait "${service_pids[@]}" 2>/dev/null || true
|
||||
else
|
||||
info "there are no service processes to terminate"
|
||||
fi
|
||||
}
|
||||
|
||||
wait_for_daemon_startup() {
|
||||
local timeout="${1}"
|
||||
if [[ "${timeout}" -eq 0 ]]; then
|
||||
info "not waiting for daemon startup due to zero timeout."
|
||||
return
|
||||
fi
|
||||
|
||||
local deadline=$((SECONDS + timeout))
|
||||
while [[ "${SECONDS}" -lt "${deadline}" ]]; do
|
||||
if "${NETBIRD_BIN}" status --check live 2>/dev/null; then
|
||||
return
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
warn "daemon did not become responsive after ${timeout} seconds, exiting..."
|
||||
exit 1
|
||||
}
|
||||
|
||||
connect() {
|
||||
info "running 'netbird up'..."
|
||||
"${NETBIRD_BIN}" up
|
||||
return $?
|
||||
}
|
||||
|
||||
main() {
|
||||
trap 'on_exit' SIGTERM SIGINT EXIT
|
||||
"${NETBIRD_BIN}" service run &
|
||||
service_pids+=("$!")
|
||||
info "registered new service process 'netbird service run', currently running: ${service_pids[@]@Q}"
|
||||
|
||||
wait_for_daemon_startup "${NB_ENTRYPOINT_SERVICE_TIMEOUT}"
|
||||
connect
|
||||
|
||||
wait "${service_pids[@]}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
wait "${daemon}"
|
||||
|
||||
@@ -995,8 +995,13 @@ type StatusResponse struct {
|
||||
// Unset when the peer is not SSO-registered or login expiration is disabled.
|
||||
// The UI derives "warning active" from this value and its own clock.
|
||||
SessionExpiresAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=sessionExpiresAt,proto3" json:"sessionExpiresAt,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
// daemonReady reports whether the daemon has finished starting up and is
|
||||
// serving RPCs backed by a running engine. Older daemons never set this
|
||||
// (it defaults to false); clients must treat an unset value as "unknown"
|
||||
// and fall back to their previous readiness heuristics for compatibility.
|
||||
DaemonReady bool `protobuf:"varint,5,opt,name=daemonReady,proto3" json:"daemonReady,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *StatusResponse) Reset() {
|
||||
@@ -1057,6 +1062,13 @@ func (x *StatusResponse) GetSessionExpiresAt() *timestamppb.Timestamp {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *StatusResponse) GetDaemonReady() bool {
|
||||
if x != nil {
|
||||
return x.DaemonReady
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type DownRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@@ -7083,14 +7095,15 @@ const file_daemon_proto_rawDesc = "" +
|
||||
"\x11getFullPeerStatus\x18\x01 \x01(\bR\x11getFullPeerStatus\x12(\n" +
|
||||
"\x0fshouldRunProbes\x18\x02 \x01(\bR\x0fshouldRunProbes\x12'\n" +
|
||||
"\fwaitForReady\x18\x03 \x01(\bH\x00R\fwaitForReady\x88\x01\x01B\x0f\n" +
|
||||
"\r_waitForReady\"\xca\x01\n" +
|
||||
"\r_waitForReady\"\xec\x01\n" +
|
||||
"\x0eStatusResponse\x12\x16\n" +
|
||||
"\x06status\x18\x01 \x01(\tR\x06status\x122\n" +
|
||||
"\n" +
|
||||
"fullStatus\x18\x02 \x01(\v2\x12.daemon.FullStatusR\n" +
|
||||
"fullStatus\x12$\n" +
|
||||
"\rdaemonVersion\x18\x03 \x01(\tR\rdaemonVersion\x12F\n" +
|
||||
"\x10sessionExpiresAt\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x10sessionExpiresAt\"\r\n" +
|
||||
"\x10sessionExpiresAt\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampR\x10sessionExpiresAt\x12 \n" +
|
||||
"\vdaemonReady\x18\x05 \x01(\bR\vdaemonReady\"\r\n" +
|
||||
"\vDownRequest\"\x0e\n" +
|
||||
"\fDownResponse\"P\n" +
|
||||
"\x10GetConfigRequest\x12 \n" +
|
||||
|
||||
@@ -291,6 +291,11 @@ message StatusResponse{
|
||||
// Unset when the peer is not SSO-registered or login expiration is disabled.
|
||||
// The UI derives "warning active" from this value and its own clock.
|
||||
google.protobuf.Timestamp sessionExpiresAt = 4;
|
||||
// daemonReady reports whether the daemon has finished starting up and is
|
||||
// serving RPCs backed by a running engine. Older daemons never set this
|
||||
// (it defaults to false); clients must treat an unset value as "unknown"
|
||||
// and fall back to their previous readiness heuristics for compatibility.
|
||||
bool daemonReady = 5;
|
||||
}
|
||||
|
||||
message DownRequest {}
|
||||
|
||||
@@ -104,6 +104,12 @@ type Server struct {
|
||||
persistSyncResponse bool
|
||||
isSessionActive atomic.Bool
|
||||
|
||||
// ready is set once the daemon has finished startup and is serving RPCs
|
||||
// backed by a running engine (see SetReady, called after Start succeeds and
|
||||
// the service is registered). Reported via StatusResponse.DaemonReady so
|
||||
// clients can wait deterministically instead of polling heuristically.
|
||||
ready atomic.Bool
|
||||
|
||||
cpuProfileBuf *bytes.Buffer
|
||||
cpuProfiling bool
|
||||
|
||||
@@ -158,6 +164,14 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
|
||||
return s
|
||||
}
|
||||
|
||||
// SetReady marks the daemon as fully started and serving RPCs. It is called by
|
||||
// the service controller once Start has succeeded and the DaemonService is
|
||||
// registered, so a subsequent Status RPC reports DaemonReady=true. Safe for
|
||||
// concurrent use.
|
||||
func (s *Server) SetReady() {
|
||||
s.ready.Store(true)
|
||||
}
|
||||
|
||||
func (s *Server) Start() error {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
@@ -1422,7 +1436,7 @@ func (s *Server) buildStatusResponse(ctx context.Context, msg *proto.StatusReque
|
||||
s.isSessionActive.Store(false)
|
||||
}
|
||||
|
||||
statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: version.NetbirdVersion()}
|
||||
statusResponse := proto.StatusResponse{Status: string(status), DaemonVersion: version.NetbirdVersion(), DaemonReady: s.ready.Load()}
|
||||
|
||||
if deadline := s.statusRecorder.GetSessionExpiresAt(); !deadline.IsZero() {
|
||||
statusResponse.SessionExpiresAt = timestamppb.New(deadline)
|
||||
|
||||
@@ -17,8 +17,7 @@ RUN apk add --no-cache bash ca-certificates ip6tables iproute2 iptables
|
||||
ENV NETBIRD_BIN="/usr/local/bin/netbird" \
|
||||
NB_LOG_FILE="console,/var/log/netbird/client.log" \
|
||||
NB_DAEMON_ADDR="unix:///var/run/netbird.sock" \
|
||||
NB_ENABLE_CAPTURE="false" \
|
||||
NB_ENTRYPOINT_SERVICE_TIMEOUT="30"
|
||||
NB_ENABLE_CAPTURE="false"
|
||||
ENTRYPOINT [ "/usr/local/bin/netbird-entrypoint.sh" ]
|
||||
COPY client/netbird-entrypoint.sh /usr/local/bin/netbird-entrypoint.sh
|
||||
COPY --from=builder /out/netbird /usr/local/bin/netbird
|
||||
|
||||
Reference in New Issue
Block a user