Skip to content

Connector join

Common global behavior is documented in the CLI hub. The complete join contract follows; its accepted resource set is source-derived.

ssh postgres mysql tcp mcp

gdsgate join <domain>

Configure and register a node in one command: the configuration file and the transport identity a connector needs before gdsgate connector can run.

TOKEN_FILE="<provision-token-file>"
sudo cat -- "$TOKEN_FILE" | sudo gdsgate join acme.example.com \
    --serve prod-db=postgres@10.0.0.5:5432 \
    --serve jump-host=ssh

It takes the deployment's domain and a one-time join token (gdsgate authority create-token --role connector) and asks for nothing else. What it writes:

Path Contents
/etc/gdsgate/connector.toml [endpoints], [enroll], [connector], one [[connector.backends]] per --serve, and the deployment label and health address where the flags below name them
/var/lib/gdsgate/connector/ transport.key (mode 0600), transport.crt, transport-ca.pem

Both defaults are the paths the shipped gdsgate-connector.service reads.

The token is read from the standard input and is refused as an argument, since arguments are readable by every process on the host and are kept by the shell's history. join spends it, so it reaches no file on this host and the role starts afterwards with nothing in its environment. The standard input is read to the end whether the token was needed or not, so a loop that pipes one on every pass is not handed a broken pipe.

One address to begin with, and the rest from the cluster. The domain settles where to register: the gateway, proxy. under it. Everything else arrives on the answer to the registration: the addresses the gateway serves a connector on, which of the three routes to Authority this deployment runs, and where the identity is renewed, scheme included. See What the cluster tells a registering node. A node that is told nothing usable says what was missing rather than deriving an address.

Resources come from the command and only from the command. A backend address is inside the protected zone and a service-account token is a path on this host, while a catalogue row is what every client reads through gdsgate ls. Each --serve takes <resource>=<kind>[@<addr>], with ssh the one kind that may omit the address, since the connector terminates it itself. Each id needs a matching catalogue entry, or the gateway closes the connector's tunnel; see Resource catalog lifecycle.

Flags:

Flag Effect
--role <connector> Role the token was minted for. Only the connector: a workload has machine-id, a provisioning control plane has authority bootstrap-provisioner, and a Proxy binds listeners, which no domain implies.
--name <NAME> What this node calls itself in the registry ([enroll].node_name) and at the gateway ([connector].id), one string in both. Default: this host's fully qualified name, and the run says so.
--serve <SPEC> A resource this node serves. Repeatable. Kinds: ssh, postgres, mysql, tcp, mcp. cockroach is intentionally rejected in this compact parser; put a CockroachDB backend in --backends-file.
--backends-file <PATH> A TOML fragment of [[connector.backends]] entries, for what --serve cannot spell: a Kubernetes API with its bundle and service-account token, a CockroachDB backend, a model service whose address is a base URL instead of a host:port, or a jump host with pinned host keys. It may declare [[connector.backends]] and nothing else.
--enroll-ca <PATH> PEM anchor verifying the certificate the registration listener presents, for a deployment issuing from its own authority. Without it, that certificate is verified against the authorities this host already trusts.
--insecure Register over cleartext. Nothing is encrypted, nothing is verified, and the token crosses the network as it is written.
--enroll-endpoint <URL> Registration URL, overriding the one derived from the domain.
--profile <LABEL> Deployment label on every metric series, log line and exported trace this node publishes (profile). Left out, there is no such label anywhere.
--metrics-listen <ADDR> Where /healthz, /readyz and Prometheus /metrics answer ([telemetry].metrics_listen). Plain HTTP, so keep it on loopback unless something off this host reads it. Left out, that surface is off.
--output <PATH>, -o Write the configuration elsewhere.
--state-dir <DIR> Keep the identity elsewhere. It has to survive restarts.
--print Render the configuration, change nothing, read no token, dial nothing.
--if-missing Reconcile instead of installing over what is here. Exit 3 when everything asked for was already so.
--force Replace a configuration that already exists, spending a fresh token. Without it, an existing file is an error and nothing is written.

Exit codes: 0 done, 3 everything asked for was already so (--if-missing), 1 it did not work, 2 the arguments do not describe an installation.

--if-missing turns the command from "install this" into "make this so", which is the shape a loop on a timer needs: an identity that is still current is left as it is and no token is spent, one that has run down is renewed over mutual TLS, and the configuration is rewritten only if these arguments render a different one. A run that finds all of it already so writes nothing and exits 3. A token piped in and not needed is read to the end and discarded, so the caller is not handed a broken pipe. A rewritten configuration is reported with the restart it needs, and the restart is named rather than performed: a service holds the file it was started with, while the identity it renews itself. --if-missing and --force cannot both be meant, and asking for both is refused.

--print reads no token. It renders everything the arguments settle and names the values only the cluster can supply rather than inventing them, so what it prints is safe to review, paste and keep, and it parses as a configuration.

What it deliberately does not do:

  • It starts nothing. No unit is written, no service is enabled, nothing is restarted. Run the role next (gdsgate --config /etc/gdsgate/connector.toml connector), or let install-connector.sh do the systemd half.
  • It sets no group or ownership beyond its own files. The configuration is owner-only; the group that lets a service's own account read it is the installing script's business.
  • It has no --fingerprint and no trust-on-first-use. A registration listener publishes no anchor to fingerprint: what comes from outside the exchange is the anchor itself, as a file (--enroll-ca), or the authorities this host already trusts. There is no third way, and none of them can be arrived at from inside the exchange.
  • It writes nothing on failure. An unreachable listener, a certificate nothing vouches for, a refused token, a cluster with nothing to advertise: each leaves the filesystem as it was.

A node that loses its state_dir comes back needing a fresh token, as a second registry row, since nothing verified connects the two. Persist that directory.

Source and negative contract

The accepted list is SERVEABLE_KINDS in crates/gdsgate/src/join.rs, not the larger ResourceKindArg catalog list in main.rs. Focused join tests cover the accepted list and reject unsupported kinds before configuration generation.