mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
teleport reconfigure (#250)
Export-Source-Commit: e06a63c176c6e97c5f87ed832a6d4aae6d1b22e6
This commit is contained in:
@@ -851,6 +851,11 @@ const (
|
||||
|
||||
// CreatedByIaCLabel is a resource metadata label name used to identify if resource was created by IaC tooling.
|
||||
CreatedByIaCLabel = TeleportNamespace + "/iac-tool"
|
||||
|
||||
// ReconfigureSourceUUIDLabel is used to identify original source uuid
|
||||
// of node where `teleport reconfigure` was used.
|
||||
ReconfigureSourceUUIDLabel = TeleportNamespace + "/reconfigure-source-uuid"
|
||||
|
||||
// OriginDefaults is an origin value indicating that the resource was
|
||||
// constructed as a default value.
|
||||
OriginDefaults = common.OriginDefaults
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/utils"
|
||||
)
|
||||
|
||||
// ReconfigureRequest describes the changes Reconfigure applies to a parsed
|
||||
// FileConfig. A zero-valued field leaves the corresponding setting untouched;
|
||||
// none of the fields has a meaningful empty value.
|
||||
type ReconfigureRequest struct {
|
||||
// ProxyServer sets proxy_server. Valid only for v3 configs.
|
||||
ProxyServer string
|
||||
// AuthServer sets auth_server (v3) or replaces auth_servers with the
|
||||
// single given address (v1/v2). Mutually exclusive with ProxyServer.
|
||||
AuthServer string
|
||||
// CAPins replaces ca_pin with the target cluster's pins. Required when
|
||||
// ProxyServer or AuthServer retargets a config that already pins a CA.
|
||||
CAPins []string
|
||||
// TokenName sets join_params.token_name and clears the legacy auth_token.
|
||||
TokenName string
|
||||
// JoinMethod sets join_params.method. It is never inferred from the
|
||||
// other join fields.
|
||||
JoinMethod string
|
||||
// BoundKeypairRegistrationSecret sets the bound_keypair registration secret
|
||||
// value and clears the incompatible registration_secret_path.
|
||||
BoundKeypairRegistrationSecret string
|
||||
// BoundKeypairRegistrationSecretPath sets the bound_keypair registration
|
||||
// secret path and clears the incompatible registration_secret_value.
|
||||
// Mutually exclusive with BoundKeypairRegistrationSecret.
|
||||
BoundKeypairRegistrationSecretPath string
|
||||
// NodeLabels is a comma-separated k=v list merged additively into
|
||||
// ssh_service labels; on a key conflict the last value wins.
|
||||
NodeLabels string
|
||||
// SourceHostUUID sets the node label types.ReconfigureSourceUUIDLabel,
|
||||
// recording the host the input config was read on.
|
||||
SourceHostUUID string
|
||||
// DataDir sets teleport.data_dir.
|
||||
DataDir string
|
||||
// PIDFile sets teleport.pid_file.
|
||||
PIDFile string
|
||||
// DiagAddr sets teleport.diag_addr.
|
||||
DiagAddr string
|
||||
// SSHListenAddr sets ssh_service.listen_addr.
|
||||
SSHListenAddr string
|
||||
// KubeListenAddr sets kubernetes_service.listen_addr.
|
||||
KubeListenAddr string
|
||||
// MetricsListenAddr sets metrics_service.listen_addr.
|
||||
MetricsListenAddr string
|
||||
}
|
||||
|
||||
// Reconfigure applies req to fc in place, leaving everything not named by a
|
||||
// request field untouched. It rejects cases where an unspecified request
|
||||
// field would cause a collision with a second agent on the same host.
|
||||
func Reconfigure(fc *FileConfig, req ReconfigureRequest) error {
|
||||
if req.ProxyServer != "" && req.AuthServer != "" {
|
||||
return trace.BadParameter("--proxy-server and --auth-server are mutually exclusive")
|
||||
}
|
||||
if req.BoundKeypairRegistrationSecret != "" && req.BoundKeypairRegistrationSecretPath != "" {
|
||||
return trace.BadParameter("--registration-secret and --registration-secret-path are mutually exclusive")
|
||||
}
|
||||
if req.ProxyServer != "" && fc.Version != defaults.TeleportConfigVersionV3 {
|
||||
return trace.BadParameter("--proxy-server requires a v3 config; use --auth-server for v1/v2 configs")
|
||||
}
|
||||
if req.ProxyServer != "" && fc.Proxy.Enabled() {
|
||||
return trace.BadParameter("--proxy-server cannot be specified if the proxy_service is enabled")
|
||||
}
|
||||
// Mirror the parsers the agent runs at start (applyConfig and the
|
||||
// per-service apply functions), or the tool writes a config the agent rejects.
|
||||
if req.ProxyServer != "" {
|
||||
if _, err := utils.ParseHostPortAddr(req.ProxyServer, defaults.HTTPListenPort); err != nil {
|
||||
return trace.Wrap(err, "parsing --proxy-server")
|
||||
}
|
||||
}
|
||||
if req.AuthServer != "" {
|
||||
if _, err := utils.ParseHostPortAddr(req.AuthServer, defaults.AuthListenPort); err != nil {
|
||||
return trace.Wrap(err, "parsing --auth-server")
|
||||
}
|
||||
}
|
||||
if len(req.CAPins) > 0 {
|
||||
// Nil certs: format check only; the pins are verified against the
|
||||
// target cluster's CA when the agent joins.
|
||||
if err := utils.CheckSPKI(req.CAPins, nil); err != nil {
|
||||
return trace.Wrap(err, "parsing --ca-pin")
|
||||
}
|
||||
}
|
||||
if req.JoinMethod != "" {
|
||||
if err := types.ValidateJoinMethod(types.JoinMethod(req.JoinMethod)); err != nil {
|
||||
return trace.Wrap(err, "parsing --join-method")
|
||||
}
|
||||
}
|
||||
if req.DiagAddr != "" {
|
||||
if _, err := utils.ParseAddr(req.DiagAddr); err != nil {
|
||||
return trace.Wrap(err, "parsing --diag-addr")
|
||||
}
|
||||
}
|
||||
if req.SSHListenAddr != "" {
|
||||
if _, err := utils.ParseHostPortAddr(req.SSHListenAddr, defaults.SSHServerListenPort); err != nil {
|
||||
return trace.Wrap(err, "parsing --ssh-listen-addr")
|
||||
}
|
||||
}
|
||||
if req.KubeListenAddr != "" {
|
||||
// Not a typo: applyKubeConfig uses the SSH proxy port as kube's default.
|
||||
if _, err := utils.ParseHostPortAddr(req.KubeListenAddr, defaults.SSHProxyListenPort); err != nil {
|
||||
return trace.Wrap(err, "parsing --kube-listen-addr")
|
||||
}
|
||||
}
|
||||
if req.MetricsListenAddr != "" {
|
||||
if _, err := utils.ParseHostPortAddr(req.MetricsListenAddr, defaults.MetricsListenPort); err != nil {
|
||||
return trace.Wrap(err, "parsing --metrics-listen-addr")
|
||||
}
|
||||
}
|
||||
|
||||
// The join method comes from the request or the config, never inference.
|
||||
// Reject what the agent would reject at startup (join_params without a
|
||||
// method) or silently ignore (a registration secret without bound_keypair).
|
||||
joinMethod := fc.JoinParams.Method
|
||||
if req.JoinMethod != "" {
|
||||
joinMethod = types.JoinMethod(req.JoinMethod)
|
||||
}
|
||||
if (req.BoundKeypairRegistrationSecret != "" || req.BoundKeypairRegistrationSecretPath != "") && joinMethod != types.JoinMethodBoundKeypair {
|
||||
flag := "--registration-secret"
|
||||
if req.BoundKeypairRegistrationSecretPath != "" {
|
||||
flag = "--registration-secret-path"
|
||||
}
|
||||
return trace.BadParameter("%s requires the %s join method; pass --join-method %s", flag, types.JoinMethodBoundKeypair, types.JoinMethodBoundKeypair)
|
||||
}
|
||||
joinParamsInUse := !fc.JoinParams.IsEqual(&JoinParams{}) ||
|
||||
req.TokenName != "" || req.BoundKeypairRegistrationSecret != "" || req.BoundKeypairRegistrationSecretPath != ""
|
||||
if joinParamsInUse && joinMethod == "" {
|
||||
return trace.BadParameter("the output config would set join_params without a join method, which the agent rejects at startup; pass --join-method")
|
||||
}
|
||||
if req.JoinMethod != "" && !joinParamsInUse && fc.AuthToken == "" {
|
||||
return trace.BadParameter("--join-method would create join_params without a token name; pass --token as well")
|
||||
}
|
||||
// ca_pin validates the CA of the cluster the agent joins, so pins carried
|
||||
// over from the old cluster cannot be reused after a retarget. Requiring
|
||||
// new ones keeps a pinned config from silently becoming an unpinned one.
|
||||
if (req.ProxyServer != "" || req.AuthServer != "") && len(fc.CAPin) > 0 && len(req.CAPins) == 0 {
|
||||
return trace.BadParameter("the input config sets ca_pin, and retargeting would leave the next join unpinned; pass --ca-pin with the target cluster's pins, which `tctl status` prints")
|
||||
}
|
||||
|
||||
if req.ProxyServer != "" {
|
||||
fc.ProxyServer = req.ProxyServer
|
||||
fc.AuthServer = ""
|
||||
fc.AuthServers = nil
|
||||
}
|
||||
if req.AuthServer != "" {
|
||||
if fc.Version == defaults.TeleportConfigVersionV3 {
|
||||
fc.AuthServer = req.AuthServer
|
||||
fc.ProxyServer = ""
|
||||
fc.AuthServers = nil
|
||||
} else {
|
||||
fc.AuthServers = []string{req.AuthServer}
|
||||
}
|
||||
}
|
||||
if len(req.CAPins) > 0 {
|
||||
fc.CAPin = apiutils.Strings(slices.Clone(req.CAPins))
|
||||
}
|
||||
|
||||
if req.TokenName != "" {
|
||||
fc.JoinParams.TokenName = req.TokenName
|
||||
fc.AuthToken = ""
|
||||
}
|
||||
if req.BoundKeypairRegistrationSecret != "" {
|
||||
fc.JoinParams.BoundKeypair.RegistrationSecretValue = req.BoundKeypairRegistrationSecret
|
||||
fc.JoinParams.BoundKeypair.RegistrationSecretPath = ""
|
||||
}
|
||||
if req.BoundKeypairRegistrationSecretPath != "" {
|
||||
fc.JoinParams.BoundKeypair.RegistrationSecretPath = req.BoundKeypairRegistrationSecretPath
|
||||
fc.JoinParams.BoundKeypair.RegistrationSecretValue = ""
|
||||
}
|
||||
if req.JoinMethod != "" {
|
||||
fc.JoinParams.Method = types.JoinMethod(req.JoinMethod)
|
||||
// Leaving the bound_keypair block under another method would carry a
|
||||
// possibly live registration secret into the output.
|
||||
if fc.JoinParams.Method != types.JoinMethodBoundKeypair {
|
||||
fc.JoinParams.BoundKeypair = BoundKeypairParams{}
|
||||
}
|
||||
}
|
||||
// The agent rejects a config that sets both join_params and the legacy
|
||||
// auth_token.
|
||||
if !fc.JoinParams.IsEqual(&JoinParams{}) && fc.AuthToken != "" {
|
||||
if fc.JoinParams.TokenName == "" {
|
||||
fc.JoinParams.TokenName = fc.AuthToken
|
||||
}
|
||||
fc.AuthToken = ""
|
||||
}
|
||||
|
||||
if req.NodeLabels != "" {
|
||||
static, dynamic, err := parseLabels(req.NodeLabels)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "parsing --node-labels")
|
||||
}
|
||||
if len(dynamic) > 0 {
|
||||
return trace.BadParameter("--node-labels only accepts static labels")
|
||||
}
|
||||
if fc.SSH.Labels == nil {
|
||||
fc.SSH.Labels = make(map[string]string, len(static))
|
||||
}
|
||||
maps.Copy(fc.SSH.Labels, static)
|
||||
}
|
||||
// After the node-labels merge, so the recorded source wins over both a
|
||||
// stale label carried in the input and a --node-labels value.
|
||||
if req.SourceHostUUID != "" {
|
||||
if fc.SSH.Labels == nil {
|
||||
fc.SSH.Labels = make(map[string]string, 1)
|
||||
}
|
||||
fc.SSH.Labels[types.ReconfigureSourceUUIDLabel] = req.SourceHostUUID
|
||||
}
|
||||
|
||||
if req.DataDir != "" {
|
||||
fc.DataDir = req.DataDir
|
||||
}
|
||||
if req.PIDFile != "" {
|
||||
fc.PIDFile = req.PIDFile
|
||||
}
|
||||
if req.DiagAddr != "" {
|
||||
fc.DiagAddr = req.DiagAddr
|
||||
}
|
||||
if req.SSHListenAddr != "" {
|
||||
fc.SSH.ListenAddress = req.SSHListenAddr
|
||||
}
|
||||
if req.KubeListenAddr != "" {
|
||||
fc.Kube.ListenAddress = req.KubeListenAddr
|
||||
}
|
||||
if req.MetricsListenAddr != "" {
|
||||
fc.Metrics.ListenAddress = req.MetricsListenAddr
|
||||
}
|
||||
|
||||
return trace.Wrap(collisionError(fc, req))
|
||||
}
|
||||
|
||||
// collisionError rejects values kept from the input that a second agent on
|
||||
// the same host would contend for; any explicitly requested value passes.
|
||||
// Only values the input sets are checked; implicit defaults (like SSH's
|
||||
// 3022) cannot be detected here.
|
||||
func collisionError(fc *FileConfig, req ReconfigureRequest) error {
|
||||
var collisions []string
|
||||
add := func(field, value, flag string) {
|
||||
collisions = append(collisions, fmt.Sprintf("%s %q (%s)", field, value, flag))
|
||||
}
|
||||
if req.PIDFile == "" && fc.PIDFile != "" {
|
||||
add("pid_file", fc.PIDFile, "--pid-file")
|
||||
}
|
||||
if req.DiagAddr == "" && fc.DiagAddr != "" {
|
||||
add("diag_addr", fc.DiagAddr, "--diag-addr")
|
||||
}
|
||||
if req.SSHListenAddr == "" && listenAddrCollides(fc.SSH.Service) {
|
||||
add("ssh_service.listen_addr", fc.SSH.ListenAddress, "--ssh-listen-addr")
|
||||
}
|
||||
if req.KubeListenAddr == "" && listenAddrCollides(fc.Kube.Service) {
|
||||
add("kubernetes_service.listen_addr", fc.Kube.ListenAddress, "--kube-listen-addr")
|
||||
}
|
||||
if req.MetricsListenAddr == "" && listenAddrCollides(fc.Metrics.Service) {
|
||||
add("metrics_service.listen_addr", fc.Metrics.ListenAddress, "--metrics-listen-addr")
|
||||
}
|
||||
if len(collisions) == 0 {
|
||||
return nil
|
||||
}
|
||||
return trace.BadParameter(
|
||||
"the output would keep these values from the input, and two agents on one host would collide on them:\n\t%s\npass each listed flag with a new value, or repeat the current value to keep it",
|
||||
strings.Join(collisions, "\n\t"))
|
||||
}
|
||||
|
||||
// listenAddrCollides reports whether a service pins a listen address a second
|
||||
// agent would contend for: explicitly set, on an enabled service.
|
||||
func listenAddrCollides(s Service) bool {
|
||||
return s.ListenAddress != "" && s.Enabled()
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
version: v3
|
||||
teleport:
|
||||
data_dir: /var/lib/teleport
|
||||
join_params:
|
||||
token_name: bk-token
|
||||
method: token
|
||||
proxy_server: old.example.com:443
|
||||
ca_pin: []
|
||||
diag_addr: ""
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
version: v2
|
||||
teleport:
|
||||
nodename: node-04
|
||||
data_dir: /var/lib/teleport
|
||||
join_params:
|
||||
token_name: new-token
|
||||
method: token
|
||||
auth_servers:
|
||||
- new.example.com:3025
|
||||
ca_pin: sha256:bbb
|
||||
diag_addr: ""
|
||||
@@ -0,0 +1,21 @@
|
||||
version: v3
|
||||
teleport:
|
||||
nodename: node-04
|
||||
data_dir: /var/lib/teleport_new
|
||||
join_params:
|
||||
token_name: new-token
|
||||
method: token
|
||||
proxy_server: new.example.com:443
|
||||
ca_pin: sha256:bbb
|
||||
diag_addr: ""
|
||||
ssh_service:
|
||||
enabled: "yes"
|
||||
labels:
|
||||
env: dev
|
||||
team: a
|
||||
teleport.dev/reconfigure-source-uuid: 8a2b5c70-97d8-4d97-b3c4-6e54b4a63cfe
|
||||
proxy_service:
|
||||
enabled: "no"
|
||||
https_keypairs: []
|
||||
https_keypairs_reload_interval: 0s
|
||||
acme: {}
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Teleport
|
||||
* Copyright (C) 2026 Gravitational, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/renameio/v2"
|
||||
"github.com/gravitational/trace"
|
||||
|
||||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/lib/config"
|
||||
"github.com/gravitational/teleport/lib/defaults"
|
||||
"github.com/gravitational/teleport/lib/utils/hostid"
|
||||
)
|
||||
|
||||
// reconfigureFlags holds the command line flags for `teleport reconfigure`.
|
||||
type reconfigureFlags struct {
|
||||
input string
|
||||
output string
|
||||
overwrite bool
|
||||
proxyServer string
|
||||
authServer string
|
||||
caPins []string
|
||||
tokenName string
|
||||
joinMethod string
|
||||
boundKeypairRegistrationSecret string
|
||||
boundKeypairRegistrationSecretPath string
|
||||
nodeLabels string
|
||||
dataDir string
|
||||
pidFile string
|
||||
diagAddr string
|
||||
sshListenAddr string
|
||||
kubeListenAddr string
|
||||
metricsListenAddr string
|
||||
}
|
||||
|
||||
// onReconfigure is the handler for the "reconfigure" CLI command.
|
||||
func onReconfigure(flags reconfigureFlags) error {
|
||||
return trace.Wrap(runReconfigure(flags, os.Stdout))
|
||||
}
|
||||
|
||||
func runReconfigure(flags reconfigureFlags, stdout io.Writer) error {
|
||||
if flags.overwrite && flags.output == "" {
|
||||
return trace.BadParameter("--overwrite requires --output")
|
||||
}
|
||||
fc, err := config.ReadFromFile(flags.input)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
// The uuid names the host the input config was read on, if present. Best effort: when the uuid is
|
||||
// unreadable (a machine that never ran an agent, an unprivileged user),
|
||||
// ReadFile returns "" and the label is not added.
|
||||
sourceDataDir := fc.DataDir
|
||||
if sourceDataDir == "" {
|
||||
sourceDataDir = defaults.DataDir
|
||||
}
|
||||
sourceHostUUID, _ := hostid.ReadFile(sourceDataDir)
|
||||
if err := config.Reconfigure(fc, config.ReconfigureRequest{
|
||||
ProxyServer: flags.proxyServer,
|
||||
AuthServer: flags.authServer,
|
||||
CAPins: flags.caPins,
|
||||
TokenName: flags.tokenName,
|
||||
JoinMethod: flags.joinMethod,
|
||||
BoundKeypairRegistrationSecret: flags.boundKeypairRegistrationSecret,
|
||||
BoundKeypairRegistrationSecretPath: flags.boundKeypairRegistrationSecretPath,
|
||||
NodeLabels: flags.nodeLabels,
|
||||
SourceHostUUID: sourceHostUUID,
|
||||
DataDir: flags.dataDir,
|
||||
PIDFile: flags.pidFile,
|
||||
DiagAddr: flags.diagAddr,
|
||||
SSHListenAddr: flags.sshListenAddr,
|
||||
KubeListenAddr: flags.kubeListenAddr,
|
||||
MetricsListenAddr: flags.metricsListenAddr,
|
||||
}); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
out, err := fc.YAMLString()
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
// Round-trip the output through the same reader the agent uses at
|
||||
// start, so an invalid file is never written.
|
||||
if _, err := config.ReadConfig(strings.NewReader(out)); err != nil {
|
||||
return trace.Wrap(err, "internal error: generated config failed validation, nothing written")
|
||||
}
|
||||
if flags.output == "" {
|
||||
if _, err := fmt.Fprint(stdout, out); err != nil {
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return trace.Wrap(writeReconfigureOutput(flags.input, flags.output, flags.overwrite, []byte(out)))
|
||||
}
|
||||
|
||||
// writeReconfigureOutput writes data to output with owner-only permissions.
|
||||
// It refuses to write over the input config and refuses an existing output
|
||||
// unless overwrite is set.
|
||||
func writeReconfigureOutput(input, output string, overwrite bool, data []byte) error {
|
||||
inInfo, err := os.Stat(input)
|
||||
if err != nil {
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
if outInfo, err := os.Stat(output); err == nil && os.SameFile(inInfo, outInfo) {
|
||||
return trace.BadParameter("refusing to write over the input config %q", input)
|
||||
}
|
||||
|
||||
if overwrite {
|
||||
// Temp file + atomic rename, forcing owner-only permissions:
|
||||
// renameio.WriteFile would keep an existing file's broader mode.
|
||||
pending, err := renameio.NewPendingFile(output, renameio.WithPermissions(teleport.FileMaskOwnerOnly))
|
||||
if err != nil {
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
defer pending.Cleanup()
|
||||
if _, err := pending.Write(data); err != nil {
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
return trace.ConvertSystemError(pending.CloseAtomicallyReplace())
|
||||
}
|
||||
|
||||
// O_EXCL atomically rejects anything already at the path, symlinks included.
|
||||
f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE|os.O_EXCL, teleport.FileMaskOwnerOnly)
|
||||
if err != nil {
|
||||
if os.IsExist(err) {
|
||||
return trace.AlreadyExists("output file %q already exists; use --overwrite to replace it", output)
|
||||
}
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
// On failure, remove the partial file so a retry is not blocked by the
|
||||
// already-exists guard.
|
||||
if _, err := f.Write(data); err != nil {
|
||||
f.Close()
|
||||
os.Remove(output)
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
// Close can carry the real write failure.
|
||||
if err := f.Close(); err != nil {
|
||||
os.Remove(output)
|
||||
return trace.ConvertSystemError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -97,6 +97,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
|
||||
ccf config.CommandLineFlags
|
||||
scpFlags scp.Flags
|
||||
dumpFlags dumpFlags
|
||||
reconfigureFlags reconfigureFlags
|
||||
configureDatabaseAWSPrintFlags configureDatabaseAWSPrintFlags
|
||||
configureDatabaseAWSCreateFlags configureDatabaseAWSCreateFlags
|
||||
configureDiscoveryBootstrapFlags configureDiscoveryBootstrapFlags
|
||||
@@ -457,6 +458,25 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
|
||||
dump.Flag("node-name", "Name for the Teleport node.").StringVar(&dumpFlags.NodeName)
|
||||
dump.Flag("node-labels", "Comma-separated list of labels to add to newly created nodes, for example env=staging,cloud=aws.").StringVar(&dumpFlags.NodeLabels)
|
||||
|
||||
reconf := app.Command("reconfigure", "Generate a new agent config file from an existing one, applying changes given by flags.").Hidden()
|
||||
reconf.Flag("input", "Path to the source config file.").Default(defaults.ConfigFilePath).StringVar(&reconfigureFlags.input)
|
||||
reconf.Flag("output", "Path to write the new config file to. Writes to stdout when omitted.").StringVar(&reconfigureFlags.output)
|
||||
reconf.Flag("overwrite", "Replace the output file if it already exists.").BoolVar(&reconfigureFlags.overwrite)
|
||||
reconf.Flag("proxy-server", "Proxy address of the target cluster; sets proxy_server (v3 configs only).").StringVar(&reconfigureFlags.proxyServer)
|
||||
reconf.Flag("auth-server", "Auth server address of the target cluster; sets auth_server (v3) or auth_servers (v1/v2).").StringVar(&reconfigureFlags.authServer)
|
||||
reconf.Flag("ca-pin", "CA pin of the target cluster (can be repeated for multiple pins).").StringsVar(&reconfigureFlags.caPins)
|
||||
reconf.Flag("token", "Name of the join token to register with the target cluster.").StringVar(&reconfigureFlags.tokenName)
|
||||
reconf.Flag("join-method", "Join method to set in join_params. Defaults to the method already in the config.").EnumVar(&reconfigureFlags.joinMethod, joinMethods...)
|
||||
reconf.Flag("registration-secret", "Bound keypair registration secret. Prefer --registration-secret-path, which keeps the secret off the command line.").StringVar(&reconfigureFlags.boundKeypairRegistrationSecret)
|
||||
reconf.Flag("registration-secret-path", "Path to a file containing the bound keypair registration secret; the agent reads it at join time.").StringVar(&reconfigureFlags.boundKeypairRegistrationSecretPath)
|
||||
reconf.Flag("node-labels", "Comma-separated labels to merge into ssh_service labels, for example env=dev,team=a; new values win on conflict.").StringVar(&reconfigureFlags.nodeLabels)
|
||||
reconf.Flag("data-dir", "Path to the new agent's data directory.").StringVar(&reconfigureFlags.dataDir)
|
||||
reconf.Flag("pid-file", "Full path to the new agent's PID file.").StringVar(&reconfigureFlags.pidFile)
|
||||
reconf.Flag("diag-addr", "Address for the new agent's diagnostic endpoint.").StringVar(&reconfigureFlags.diagAddr)
|
||||
reconf.Flag("ssh-listen-addr", "Listen address for the new agent's SSH service.").StringVar(&reconfigureFlags.sshListenAddr)
|
||||
reconf.Flag("kube-listen-addr", "Listen address for the new agent's Kubernetes service.").StringVar(&reconfigureFlags.kubeListenAddr)
|
||||
reconf.Flag("metrics-listen-addr", "Listen address for the new agent's metrics service.").StringVar(&reconfigureFlags.metricsListenAddr)
|
||||
|
||||
ver.Flag("raw", "Print the raw teleport version string.").BoolVar(&rawVersion)
|
||||
|
||||
dumpNode := app.Command("node", "SSH Node configuration commands")
|
||||
@@ -752,6 +772,8 @@ Examples:
|
||||
err = onStatus()
|
||||
case dump.FullCommand():
|
||||
err = onConfigDump(dumpFlags)
|
||||
case reconf.FullCommand():
|
||||
err = onReconfigure(reconfigureFlags)
|
||||
case dumpNodeConfigure.FullCommand():
|
||||
dumpFlags.Roles = defaults.RoleNode
|
||||
err = onConfigDump(dumpFlags)
|
||||
|
||||
Reference in New Issue
Block a user