mirror of
https://github.com/gravitational/teleport
synced 2024-10-20 01:03:40 +00:00
Machine ID: Configuration migration (#27468)
* Introduce new tbot output configuration * Remove code that will be included in future test refactor PR * Add config version header * Fix TestInitSymlink * Add support for `standard` database type * Set CA type * `make fix-imports` * More closely mimic original database output behaviour * Make output of additional TLS files for application output optional * Spell compatability properly * Add test for config marshalling * Fix cluster field yaml name * Fix YAML marshalling/unmarshalling * Fix sidecar invocation of tbot * Add WrapDestination helper function to protect wrappedDestination * Apply changes to sidecar * Spell Marshalling the way the linter insists * Fix some logging * Tidy mutex usage on outputRenewalCache * Fix ssh_host generation * TBot Config V2 migration support * Fix misspelling * Get rid of `destinationWrapper` * Single l in Unmarshaling * Fix operator sidecar tbot * Add UnmarshalYAML for SSHHostOutput * Fix migration for removed destinationWrapper * Use updated KubernetesCluster field name * Add real world test case for migration * Add additional migration tests * Use `KubernetesCluster` instead of `ClusterName` for clarity * Add additional "real-world" migrations from customer feedback * Rename `Subtype` -> `Format` for `DatabaseOutput` * Rename Subtype -> Format for DatabaseOutput * Remove a very british "u" from Behaviour * Add godoc for interfacemethods * Fix double dot in comment Co-authored-by: Michael Wilson <mike@mdwn.dev> * Use const for database formats * Reuse constant type string in Stringer * Add godoc comment explaining behaviour if no destination found * Inject executablePathGetter rather than using package level variable * Use correct case in error * Add warning for destination reuse * Try to improve confusing log message * Remove 'u' from behaviour * Emit error when v2 config possibly being migrated as v1 * Fix imports * Ensure they dont overwrite original config * Remove redundant check --------- Co-authored-by: Michael Wilson <mike@mdwn.dev>
This commit is contained in:
parent
bcf6e8fd30
commit
2e47bf740d
|
@ -238,6 +238,7 @@ func (conf *OnboardingConfig) Token() (string, error) {
|
|||
}
|
||||
|
||||
// BotConfig is the bot's root config object.
|
||||
// This is currently at version "v2".
|
||||
type BotConfig struct {
|
||||
Version Version `yaml:"version"`
|
||||
Onboarding OnboardingConfig `yaml:"onboarding,omitempty"`
|
||||
|
@ -507,7 +508,7 @@ func FromCLIConf(cf *CLIConf) (*BotConfig, error) {
|
|||
var err error
|
||||
|
||||
if cf.ConfigPath != "" {
|
||||
config, err = ReadConfigFromFile(cf.ConfigPath)
|
||||
config, err = ReadConfigFromFile(cf.ConfigPath, false)
|
||||
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "loading bot config from path %s", cf.ConfigPath)
|
||||
|
@ -619,14 +620,14 @@ func FromCLIConf(cf *CLIConf) (*BotConfig, error) {
|
|||
}
|
||||
|
||||
// ReadConfigFromFile reads and parses a YAML config from a file.
|
||||
func ReadConfigFromFile(filePath string) (*BotConfig, error) {
|
||||
func ReadConfigFromFile(filePath string, manualMigration bool) (*BotConfig, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, fmt.Sprintf("failed to open file: %v", filePath))
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
return ReadConfig(f)
|
||||
return ReadConfig(f, manualMigration)
|
||||
}
|
||||
|
||||
type Version string
|
||||
|
@ -637,7 +638,7 @@ var (
|
|||
)
|
||||
|
||||
// ReadConfig parses a YAML config file from a Reader.
|
||||
func ReadConfig(reader io.ReadSeeker) (*BotConfig, error) {
|
||||
func ReadConfig(reader io.ReadSeeker, manualMigration bool) (*BotConfig, error) {
|
||||
var version struct {
|
||||
Version Version `yaml:"version"`
|
||||
}
|
||||
|
@ -651,12 +652,29 @@ func ReadConfig(reader io.ReadSeeker) (*BotConfig, error) {
|
|||
return nil, trace.Wrap(err)
|
||||
}
|
||||
decoder = yaml.NewDecoder(reader)
|
||||
decoder.KnownFields(true)
|
||||
|
||||
switch version.Version {
|
||||
case V1, "":
|
||||
panic("migration code will be inserted here in follow up PR")
|
||||
if !manualMigration {
|
||||
log.Warn("Deprecated config version (V1) detected. Attempting to perform an on-the-fly in-memory migration to latest version. Please persist the config migration by following the guidance at https://goteleport.com/docs/machine-id/reference/v14-upgrade-guide/")
|
||||
}
|
||||
config := &configV1{}
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, trace.BadParameter("failed parsing config file: %s", strings.Replace(err.Error(), "\n", "", -1))
|
||||
}
|
||||
latestConfig, err := config.migrate()
|
||||
if err != nil {
|
||||
return nil, trace.WithUserMessage(
|
||||
trace.Wrap(err, "migrating v1 config"),
|
||||
"Failed to migrate. See https://goteleport.com/docs/machine-id/reference/v14-upgrade-guide/",
|
||||
)
|
||||
}
|
||||
return latestConfig, nil
|
||||
case V2:
|
||||
if manualMigration {
|
||||
return nil, trace.BadParameter("configuration already the latest version. nothing to migrate.")
|
||||
}
|
||||
decoder.KnownFields(true)
|
||||
config := &BotConfig{}
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, trace.BadParameter("failed parsing config file: %s", strings.Replace(err.Error(), "\n", "", -1))
|
||||
|
|
|
@ -42,7 +42,7 @@ func (sc *StorageConfig) CheckAndSetDefaults() error {
|
|||
}
|
||||
}
|
||||
|
||||
return trace.Wrap(sc.Destination.CheckAndSetDefaults())
|
||||
return trace.Wrap(sc.Destination.CheckAndSetDefaults(), "validating storage")
|
||||
}
|
||||
|
||||
func (sc *StorageConfig) MarshalYAML() (interface{}, error) {
|
||||
|
|
|
@ -78,7 +78,7 @@ func TestConfigCLIOnlySample(t *testing.T) {
|
|||
|
||||
func TestConfigFile(t *testing.T) {
|
||||
configData := fmt.Sprintf(exampleConfigFile, "foo")
|
||||
cfg, err := ReadConfig(strings.NewReader(configData))
|
||||
cfg, err := ReadConfig(strings.NewReader(configData), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, "auth.example.com", cfg.AuthServer)
|
||||
|
@ -114,7 +114,7 @@ func TestLoadTokenFromFile(t *testing.T) {
|
|||
require.NoError(t, os.WriteFile(tokenFile, []byte("xxxyyy"), 0660))
|
||||
|
||||
configData := fmt.Sprintf(exampleConfigFile, tokenFile)
|
||||
cfg, err := ReadConfig(strings.NewReader(configData))
|
||||
cfg, err := ReadConfig(strings.NewReader(configData), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := cfg.Onboarding.Token()
|
||||
|
|
415
lib/tbot/config/migrate.go
Normal file
415
lib/tbot/config/migrate.go
Normal file
|
@ -0,0 +1,415 @@
|
|||
/*
|
||||
Copyright 2023 Gravitational, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"golang.org/x/exp/slices"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gravitational/teleport/lib/tbot/bot"
|
||||
)
|
||||
|
||||
type destinationMixinV1 struct {
|
||||
Directory *DestinationDirectory `yaml:"directory"`
|
||||
Memory *DestinationMemory `yaml:"memory"`
|
||||
}
|
||||
|
||||
func (c *destinationMixinV1) migrate() (bot.Destination, error) {
|
||||
switch {
|
||||
case c.Memory != nil && c.Directory != nil:
|
||||
return nil, trace.BadParameter("both 'memory' and 'directory' cannot be specified")
|
||||
case c.Memory != nil:
|
||||
return c.Memory, nil
|
||||
case c.Directory != nil:
|
||||
return c.Directory, nil
|
||||
default:
|
||||
return nil, trace.BadParameter("at least one of `memory' and 'directory' must be specified")
|
||||
}
|
||||
}
|
||||
|
||||
type storageConfigV1 struct {
|
||||
Mixin destinationMixinV1 `yaml:",inline"`
|
||||
}
|
||||
|
||||
func (c *storageConfigV1) migrate() (*StorageConfig, error) {
|
||||
dest, err := c.Mixin.migrate()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "migrating destination mixin")
|
||||
}
|
||||
|
||||
return &StorageConfig{
|
||||
Destination: dest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type configV1Database struct {
|
||||
Service string `yaml:"service"`
|
||||
Database string `yaml:"database"`
|
||||
Username string `yaml:"username"`
|
||||
}
|
||||
|
||||
type configV1DestinationConfigHostCert struct {
|
||||
Principals []string `yaml:"principals"`
|
||||
}
|
||||
|
||||
type configV1DestinationConfig struct {
|
||||
SSHClient map[string]any `yaml:"ssh_client"`
|
||||
Identity map[string]any `yaml:"identity"`
|
||||
TLS map[string]any `yaml:"tls"`
|
||||
TLSCAs map[string]any `yaml:"tls_cas"`
|
||||
Mongo map[string]any `yaml:"mongo"`
|
||||
Cockroach map[string]any `yaml:"cockroach"`
|
||||
Kubernetes map[string]any `yaml:"kubernetes"`
|
||||
SSHHostCert *configV1DestinationConfigHostCert `yaml:"ssh_host_cert"`
|
||||
}
|
||||
|
||||
func (c *configV1DestinationConfig) UnmarshalYAML(node *yaml.Node) error {
|
||||
var simpleTemplate string
|
||||
if err := node.Decode(&simpleTemplate); err == nil {
|
||||
switch simpleTemplate {
|
||||
case TemplateSSHClientName:
|
||||
c.SSHClient = map[string]any{}
|
||||
case TemplateIdentityName:
|
||||
c.Identity = map[string]any{}
|
||||
case TemplateTLSName:
|
||||
c.TLS = map[string]any{}
|
||||
case TemplateTLSCAsName:
|
||||
c.TLSCAs = map[string]any{}
|
||||
case TemplateMongoName:
|
||||
c.Mongo = map[string]any{}
|
||||
case TemplateCockroachName:
|
||||
c.Cockroach = map[string]any{}
|
||||
case TemplateKubernetesName:
|
||||
c.Kubernetes = map[string]any{}
|
||||
case TemplateSSHHostCertName:
|
||||
c.SSHHostCert = &configV1DestinationConfigHostCert{}
|
||||
default:
|
||||
return trace.BadParameter("unrecognized config template %q", simpleTemplate)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Fall back to the full struct; alias it to get standard unmarshal
|
||||
// behavior and avoid recursion
|
||||
type rawTemplate configV1DestinationConfig
|
||||
return trace.Wrap(node.Decode((*rawTemplate)(c)))
|
||||
}
|
||||
|
||||
type configV1Destination struct {
|
||||
Mixin destinationMixinV1 `yaml:",inline"`
|
||||
|
||||
Roles []string `yaml:"roles"`
|
||||
Configs []configV1DestinationConfig `yaml:"configs"`
|
||||
|
||||
Database *configV1Database `yaml:"database"`
|
||||
KubernetesCluster string `yaml:"kubernetes_cluster"`
|
||||
App string `yaml:"app"`
|
||||
Cluster string `yaml:"cluster"`
|
||||
}
|
||||
|
||||
func validateTemplates(configs []configV1DestinationConfig, allowedTypes []string, requiredTypes []string) error {
|
||||
var allConfiguredTypes []string
|
||||
|
||||
configUnsupportedErr := func(typeName string) error {
|
||||
return trace.BadParameter("configuration options are not supported by migration for %s config template", typeName)
|
||||
}
|
||||
|
||||
for _, templateConfig := range configs {
|
||||
var configuredTypes []string
|
||||
if templateConfig.SSHClient != nil {
|
||||
if len(templateConfig.SSHClient) > 0 {
|
||||
return configUnsupportedErr(TemplateSSHClientName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateSSHClientName)
|
||||
}
|
||||
if templateConfig.Identity != nil {
|
||||
if len(templateConfig.Identity) > 0 {
|
||||
return configUnsupportedErr(TemplateIdentityName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateIdentityName)
|
||||
}
|
||||
if templateConfig.TLS != nil {
|
||||
if len(templateConfig.TLS) > 0 {
|
||||
return configUnsupportedErr(TemplateTLSName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateTLSName)
|
||||
}
|
||||
if templateConfig.TLSCAs != nil {
|
||||
if len(templateConfig.TLSCAs) > 0 {
|
||||
return configUnsupportedErr(TemplateTLSCAsName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateTLSCAsName)
|
||||
}
|
||||
if templateConfig.Mongo != nil {
|
||||
if len(templateConfig.Mongo) > 0 {
|
||||
return configUnsupportedErr(TemplateMongoName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateMongoName)
|
||||
}
|
||||
if templateConfig.Cockroach != nil {
|
||||
if len(templateConfig.Cockroach) > 0 {
|
||||
return configUnsupportedErr(TemplateCockroachName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateCockroachName)
|
||||
}
|
||||
if templateConfig.Kubernetes != nil {
|
||||
if len(templateConfig.Kubernetes) > 0 {
|
||||
return configUnsupportedErr(TemplateKubernetesName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateKubernetesName)
|
||||
}
|
||||
if templateConfig.SSHHostCert != nil {
|
||||
if len(templateConfig.SSHHostCert.Principals) == 0 {
|
||||
return trace.BadParameter("no principals specified for %s config template", TemplateSSHHostCertName)
|
||||
}
|
||||
configuredTypes = append(configuredTypes, TemplateSSHHostCertName)
|
||||
}
|
||||
|
||||
if len(configuredTypes) == 0 {
|
||||
return trace.BadParameter("config template must not be empty")
|
||||
}
|
||||
if len(configuredTypes) > 1 {
|
||||
return trace.BadParameter("config template must have exactly one configuration")
|
||||
}
|
||||
|
||||
allConfiguredTypes = append(allConfiguredTypes, configuredTypes...)
|
||||
}
|
||||
|
||||
// Ensure all types are allowed by the new output type
|
||||
for _, typeName := range allConfiguredTypes {
|
||||
if !slices.Contains(allowedTypes, typeName) {
|
||||
return trace.BadParameter("config template %q unsupported by new output type", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure the required types are specified for the new output type
|
||||
for _, typeName := range requiredTypes {
|
||||
if !slices.Contains(allConfiguredTypes, typeName) {
|
||||
return trace.BadParameter("old config templates missing required template %s", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any weird duplicates we can't handle correctly
|
||||
typeCounts := map[string]int{}
|
||||
for _, typeName := range allConfiguredTypes {
|
||||
typeCounts[typeName]++
|
||||
}
|
||||
for typeName, count := range typeCounts {
|
||||
if count > 1 {
|
||||
return trace.BadParameter("multiple config template entries found for %q", typeName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *configV1Destination) migrate() (Output, error) {
|
||||
dest, err := c.Mixin.migrate()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "migrating destination")
|
||||
}
|
||||
|
||||
appConfigured := c.App != ""
|
||||
databaseConfigured := c.Database != nil
|
||||
kubernetesConfigured := c.KubernetesCluster != ""
|
||||
hostCertConfigured := false
|
||||
for _, templateConfig := range c.Configs {
|
||||
if templateConfig.SSHHostCert != nil {
|
||||
hostCertConfigured = true
|
||||
}
|
||||
}
|
||||
outputTypesCount := 0
|
||||
for _, val := range []bool{appConfigured, databaseConfigured, kubernetesConfigured, hostCertConfigured} {
|
||||
if val {
|
||||
outputTypesCount++
|
||||
}
|
||||
}
|
||||
if outputTypesCount > 1 {
|
||||
return nil, trace.BadParameter("multiple potential output types detected, cannot determine correct type")
|
||||
}
|
||||
|
||||
switch {
|
||||
case appConfigured:
|
||||
if err := validateTemplates(
|
||||
c.Configs,
|
||||
[]string{TemplateTLSCAsName, TemplateTLSName, TemplateIdentityName},
|
||||
[]string{},
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err, "validating template configs")
|
||||
}
|
||||
specificTLSExtensions := false
|
||||
for _, templateConfig := range c.Configs {
|
||||
if templateConfig.TLS != nil {
|
||||
specificTLSExtensions = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return &ApplicationOutput{
|
||||
Destination: dest,
|
||||
Roles: c.Roles,
|
||||
AppName: c.App,
|
||||
SpecificTLSExtensions: specificTLSExtensions,
|
||||
}, nil
|
||||
case databaseConfigured:
|
||||
if err := validateTemplates(
|
||||
c.Configs,
|
||||
[]string{TemplateTLSCAsName, TemplateIdentityName, TemplateMongoName, TemplateCockroachName, TemplateTLSName},
|
||||
[]string{},
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err, "validating template configs")
|
||||
}
|
||||
format := UnspecifiedDatabaseFormat
|
||||
for _, templateConfig := range c.Configs {
|
||||
if templateConfig.Mongo != nil {
|
||||
if format != UnspecifiedDatabaseFormat {
|
||||
return nil, trace.BadParameter("multiple candidate formats for database output")
|
||||
}
|
||||
format = MongoDatabaseFormat
|
||||
}
|
||||
if templateConfig.Cockroach != nil {
|
||||
if format != UnspecifiedDatabaseFormat {
|
||||
return nil, trace.BadParameter("multiple candidate formats for database output")
|
||||
}
|
||||
format = CockroachDatabaseFormat
|
||||
}
|
||||
if templateConfig.TLS != nil {
|
||||
if format != UnspecifiedDatabaseFormat {
|
||||
return nil, trace.BadParameter("multiple candidate formats for database output")
|
||||
}
|
||||
format = TLSDatabaseFormat
|
||||
}
|
||||
}
|
||||
return &DatabaseOutput{
|
||||
Destination: dest,
|
||||
Roles: c.Roles,
|
||||
Format: format,
|
||||
Database: c.Database.Database,
|
||||
Service: c.Database.Service,
|
||||
Username: c.Database.Username,
|
||||
}, nil
|
||||
case kubernetesConfigured:
|
||||
if err := validateTemplates(
|
||||
c.Configs,
|
||||
[]string{TemplateTLSCAsName, TemplateIdentityName, TemplateKubernetesName},
|
||||
[]string{},
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err, "validating template configs")
|
||||
}
|
||||
return &KubernetesOutput{
|
||||
Destination: dest,
|
||||
Roles: c.Roles,
|
||||
KubernetesCluster: c.KubernetesCluster,
|
||||
}, nil
|
||||
case hostCertConfigured:
|
||||
if err := validateTemplates(
|
||||
c.Configs,
|
||||
[]string{TemplateSSHHostCertName},
|
||||
[]string{TemplateSSHHostCertName},
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err, "validating template configs")
|
||||
}
|
||||
|
||||
// Extract principals from template config
|
||||
principals := []string{}
|
||||
for _, c := range c.Configs {
|
||||
if c.SSHHostCert != nil {
|
||||
principals = c.SSHHostCert.Principals
|
||||
break
|
||||
}
|
||||
}
|
||||
return &SSHHostOutput{
|
||||
Destination: dest,
|
||||
Roles: c.Roles,
|
||||
Principals: principals,
|
||||
}, nil
|
||||
default:
|
||||
if err := validateTemplates(
|
||||
c.Configs,
|
||||
[]string{TemplateTLSCAsName, TemplateIdentityName, TemplateSSHClientName},
|
||||
[]string{},
|
||||
); err != nil {
|
||||
return nil, trace.Wrap(err, "validating template configs")
|
||||
}
|
||||
return &IdentityOutput{
|
||||
Destination: dest,
|
||||
Roles: c.Roles,
|
||||
Cluster: c.Cluster,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type configV1 struct {
|
||||
Onboarding OnboardingConfig `yaml:"onboarding"`
|
||||
Debug bool `yaml:"debug"`
|
||||
AuthServer string `yaml:"auth_server"`
|
||||
CertificateTTL time.Duration `yaml:"certificate_ttl"`
|
||||
RenewalInterval time.Duration `yaml:"renewal_interval"`
|
||||
Oneshot bool `yaml:"oneshot"`
|
||||
FIPS bool `yaml:"fips"`
|
||||
DiagAddr string `yaml:"diag_addr"`
|
||||
|
||||
Destinations []configV1Destination `yaml:"destinations"`
|
||||
StorageConfig *storageConfigV1 `yaml:"storage"`
|
||||
|
||||
// This field doesn't exist in V1, but, it exists here so we can detect
|
||||
// a scenario where for some reason we're trying to migrate a V2 config
|
||||
// that's missing the version header.
|
||||
Outputs []any `yaml:"outputs"`
|
||||
}
|
||||
|
||||
func (c *configV1) migrate() (*BotConfig, error) {
|
||||
if len(c.Outputs) > 0 {
|
||||
return nil, trace.BadParameter("config has been detected as potentially v1, but includes the v2 outputs field")
|
||||
}
|
||||
|
||||
var storage *StorageConfig
|
||||
var err error
|
||||
if c.StorageConfig != nil {
|
||||
storage, err = c.StorageConfig.migrate()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "migrating storage config")
|
||||
}
|
||||
}
|
||||
|
||||
var outputs []Output
|
||||
for _, d := range c.Destinations {
|
||||
o, err := d.migrate()
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "migrating output")
|
||||
}
|
||||
outputs = append(outputs, o)
|
||||
}
|
||||
|
||||
return &BotConfig{
|
||||
Version: V2,
|
||||
|
||||
Onboarding: c.Onboarding,
|
||||
Debug: c.Debug,
|
||||
AuthServer: c.AuthServer,
|
||||
CertificateTTL: c.CertificateTTL,
|
||||
RenewalInterval: c.RenewalInterval,
|
||||
Oneshot: c.Oneshot,
|
||||
FIPS: c.FIPS,
|
||||
DiagAddr: c.DiagAddr,
|
||||
|
||||
Storage: storage,
|
||||
Outputs: outputs,
|
||||
}, nil
|
||||
}
|
1145
lib/tbot/config/migrate_test.go
Normal file
1145
lib/tbot/config/migrate_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -112,7 +112,7 @@ func testConfigFromCLI(t *testing.T, cf *config.CLIConf) *config.BotConfig {
|
|||
|
||||
// testConfigFromString parses a YAML config file from a string.
|
||||
func testConfigFromString(t *testing.T, yaml string) *config.BotConfig {
|
||||
cfg, err := config.ReadConfig(strings.NewReader(yaml))
|
||||
cfg, err := config.ReadConfig(strings.NewReader(yaml), false)
|
||||
require.NoError(t, err)
|
||||
|
||||
return cfg
|
||||
|
|
|
@ -108,6 +108,9 @@ func Run(args []string, stdout io.Writer) error {
|
|||
configureCmd.Flag("token", "A bot join token, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
|
||||
configureCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)
|
||||
|
||||
migrateCmd := app.Command("migrate", "Migrates a config file from an older version to the newest version. Outputs to stdout by default.")
|
||||
migrateCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)
|
||||
|
||||
dbCmd := app.Command("db", "Execute database commands through tsh.")
|
||||
dbCmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Required().StringVar(&cf.Proxy)
|
||||
dbCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
|
||||
|
@ -151,6 +154,12 @@ func Run(args []string, stdout io.Writer) error {
|
|||
utils.InitLogger(utils.LoggingForDaemon, logrus.DebugLevel)
|
||||
}
|
||||
|
||||
// If migration is specified, we want to run this before the config is
|
||||
// loaded normally.
|
||||
if migrateCmd.FullCommand() == command {
|
||||
return onMigrate(cf, stdout)
|
||||
}
|
||||
|
||||
botConfig, err := config.FromCLIConf(&cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
|
@ -232,6 +241,60 @@ func onConfigure(
|
|||
return nil
|
||||
}
|
||||
|
||||
func onMigrate(
|
||||
cf config.CLIConf,
|
||||
stdout io.Writer,
|
||||
) error {
|
||||
if cf.ConfigPath == "" {
|
||||
return trace.BadParameter("source config file must be provided with -c")
|
||||
}
|
||||
|
||||
out := stdout
|
||||
outPath := cf.ConfigureOutput
|
||||
if outPath != "" {
|
||||
if outPath == cf.ConfigPath {
|
||||
return trace.BadParameter("migrated config output path should not be the same as the source config path")
|
||||
}
|
||||
|
||||
f, err := os.Create(outPath)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
defer f.Close()
|
||||
out = f
|
||||
}
|
||||
|
||||
// We do not want to load an existing configuration file as this will cause
|
||||
// it to be merged with the provided flags and defaults.
|
||||
cfg, err := config.ReadConfigFromFile(cf.ConfigPath, true)
|
||||
if err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
if err := cfg.CheckAndSetDefaults(); err != nil {
|
||||
return trace.Wrap(err, "validating new config")
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, "# tbot config file generated by `migrate` command")
|
||||
|
||||
enc := yaml.NewEncoder(out)
|
||||
enc.SetIndent(2)
|
||||
if err := enc.Encode(cfg); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if err := enc.Close(); err != nil {
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
if outPath != "" {
|
||||
log.Infof(
|
||||
"Generated config file written to file: %s", outPath,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func onStart(botConfig *config.BotConfig) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
|
Loading…
Reference in a new issue