Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions lib/client/identityfile/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ type ConfigWriter interface {
// permissions if the file is new.
WriteFile(name string, data []byte, perm os.FileMode) error

// ReadFile reads the file at tpath `name`
ReadFile(name string) ([]byte, error)

// Remove removes a file.
Remove(name string) error

Expand All @@ -152,6 +155,11 @@ func (s *StandardConfigWriter) WriteFile(name string, data []byte, perm os.FileM
return os.WriteFile(name, data, perm)
}

// ReadFile reads the file at tpath `name`, returning
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfinished comment.

func (s *StandardConfigWriter) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}

// Remove removes the named file or (empty) directory.
// If there is an error, it will be of type *PathError.
func (s *StandardConfigWriter) Remove(name string) error {
Expand Down Expand Up @@ -389,7 +397,7 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err

case FormatKubernetes:
filesWritten = append(filesWritten, cfg.OutputPath)
// If the user does not want to override, it will merge the previous kubeconfig
// If the user does not want to override, it will merge the previous kubeconfig
// with the new entry.
if err := checkOverwrite(ctx, writer, cfg.OverwriteDestination, filesWritten...); err != nil && !trace.IsAlreadyExists(err) {
return nil, trace.Wrap(err)
Expand All @@ -408,13 +416,13 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err
kubeCluster = []string{cfg.KubeClusterName}
}

if err := kubeconfig.Update(cfg.OutputPath, kubeconfig.Values{
if err := kubeconfig.UpdateConfig(cfg.OutputPath, kubeconfig.Values{
TeleportClusterName: cfg.Key.ClusterName,
ClusterAddr: cfg.KubeProxyAddr,
Credentials: cfg.Key,
TLSServerName: cfg.KubeTLSServerName,
KubeClusters: kubeCluster,
}, cfg.KubeStoreAllCAs); err != nil {
}, cfg.KubeStoreAllCAs, writer); err != nil {
return nil, trace.Wrap(err)
}

Expand Down
36 changes: 36 additions & 0 deletions lib/client/identityfile/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,42 @@ func TestWrite(t *testing.T) {
assertKubeconfigContents(t, cfg.OutputPath, key.ClusterName, "far.away.cluster", cfg.KubeTLSServerName)
}

// Assert that the kubeconfig writer only writes to the supplied filesystem
// abstraction, and not to the system
func TestWriteKubeOnlyWritesToWriter(t *testing.T) {
key := newClientKey(t)
outputDir := t.TempDir()

fs := NewInMemoryConfigWriter()
cfg := WriteConfig{
Key: key,
Writer: fs,
}

cfg.OutputPath = filepath.Join(outputDir, "kubeconfig")
cfg.Format = FormatOpenSSH
cfg.KubeProxyAddr = "far.away.cluster"
cfg.KubeTLSServerName = constants.KubeTeleportProxyALPNPrefix + "far.away.cluster"
files, err := Write(context.Background(), cfg)
require.NoError(t, err)

// Assert that none of the listed files
for _, fn := range files {
// assert that no such file exists on the system filesystem
_, err := os.Stat(fn)
require.Error(t, err)

// assert that the file exists is in the filesystem abstraction
require.Contains(t, fs.files, fn)
}

// Assert that nothing has written to the temp dir without it being added to
// the returned file list
actualFiles, err := os.ReadDir(outputDir)
require.NoError(t, err)
require.Empty(t, actualFiles)
}

func TestWriteAllFormats(t *testing.T) {
for _, format := range KnownFileFormats {
t.Run(string(format), func(t *testing.T) {
Expand Down
36 changes: 29 additions & 7 deletions lib/client/identityfile/inmemory_config_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,52 @@ import (
"io/fs"
"os"
"sync"
"time"

"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"

"github.com/gravitational/teleport/lib/utils"
)

type InMemoryFS map[string]*utils.InMemoryFile

type InMemoryConfigWriterOption func(*InMemoryConfigWriter)

func WithClock(clock clockwork.Clock) InMemoryConfigWriterOption {
return func(w *InMemoryConfigWriter) {
w.clock = clock
}
}

// NewInMemoryConfigWriter creates a new virtual file system
// It stores the files contents and their properties in memory
func NewInMemoryConfigWriter() *InMemoryConfigWriter {
return &InMemoryConfigWriter{
func NewInMemoryConfigWriter(options ...InMemoryConfigWriterOption) *InMemoryConfigWriter {
w := &InMemoryConfigWriter{
mux: &sync.RWMutex{},
files: make(map[string]*utils.InMemoryFile),
clock: clockwork.NewRealClock(),
files: InMemoryFS{},
}
for _, option := range options {
option(w)
}
return w
}

// InMemoryConfigWriter is a basic virtual file system abstraction that writes into memory
//
// instead of writing to a more persistent storage.
type InMemoryConfigWriter struct {
mux *sync.RWMutex
files map[string]*utils.InMemoryFile
clock clockwork.Clock
files InMemoryFS
}

// WriteFile writes the given data to path `name`
// It replaces the file if it already exists
func (m *InMemoryConfigWriter) WriteFile(name string, data []byte, perm os.FileMode) error {
m.mux.Lock()
defer m.mux.Unlock()
m.files[name] = utils.NewInMemoryFile(name, perm, time.Now(), data)
m.files[name] = utils.NewInMemoryFile(name, perm, m.clock.Now(), data)

return nil
}
Expand Down Expand Up @@ -92,7 +108,13 @@ func (m *InMemoryConfigWriter) ReadFile(name string) ([]byte, error) {
return f.Content(), nil
}

// Open is not implemented but exists here to satisfy the io/fs.ReadFileFS interface.
// Open is not implemented but exists here to satisfy the io/fs. interface.
func (m *InMemoryConfigWriter) Open(name string) (fs.File, error) {
return nil, trace.NotImplemented("Open is not implemented for InMemoryConfigWriter")
}

func (m *InMemoryConfigWriter) WithReadonlyFiles(fn func(InMemoryFS) error) error {
m.mux.RLock()
defer m.mux.RUnlock()
return fn(m.files)
}
103 changes: 97 additions & 6 deletions lib/kube/kubeconfig/kubeconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,17 +101,47 @@ type ExecValues struct {
Env map[string]string
}

// ConfigFS is a simple filesystem abstraction to allow alternative file
// writing options when generating kube config files.
type ConfigFS interface {
// WriteFile writes the given data to path `name`, using the specified
// permissions if the file is new.
WriteFile(name string, data []byte, perm os.FileMode) error

ReadFile(name string) ([]byte, error)
}

// defaultConfigFS is a ConfigFS that is backed by the system filesystem
type defaultConfigFS struct{}

func (defaultConfigFS) WriteFile(name string, data []byte, perm os.FileMode) error {
return os.WriteFile(name, data, perm)
}

func (defaultConfigFS) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}

// Update adds Teleport configuration to kubeconfig.
//
// If `path` is empty, Update will try to guess it based on the environment or
// known defaults.
func Update(path string, v Values, storeAllCAs bool) error {
return UpdateConfig(path, v, storeAllCAs, defaultConfigFS{})
}

// UpdateConfig adds Teleport configuration to kubeconfig, reading and writing
// from the supplied ConfigFS
//
// If `path` is empty, Update will try to guess it based on the environment or
// known defaults.
func UpdateConfig(path string, v Values, storeAllCAs bool, fs ConfigFS) error {
contextTmpl, err := parseContextOverrideTemplate(v.OverrideContext)
if err != nil {
return trace.Wrap(err)
}

config, err := Load(path)
config, err := LoadConfig(path, fs)
if err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -237,7 +267,7 @@ func Update(path string, v Values, storeAllCAs bool) error {
log.WithError(err).Warn("Kubernetes integration is not supported when logging in with a non-rsa private key.")
}

return Save(path, *config)
return SaveConfig(path, *config, fs)
}

func setContext(contexts map[string]*clientcmdapi.Context, name, cluster, auth, kubeName, namespace string) {
Expand Down Expand Up @@ -337,37 +367,98 @@ func removeByServerAddr(config *clientcmdapi.Config, wantServer string) {
// Load tries to read a kubeconfig file and if it can't, returns an error.
// One exception, missing files result in empty configs, not an error.
func Load(path string) (*clientcmdapi.Config, error) {
return LoadConfig(path, defaultConfigFS{})
}

// LoadConfig tries to read a kubeconfig file and if it can't, returns an error.
// One exception, missing files result in empty configs, not an error.
func LoadConfig(path string, fs ConfigFS) (*clientcmdapi.Config, error) {
filename, err := finalPath(path)
if err != nil {
return nil, trace.Wrap(err)
}
config, err := clientcmd.LoadFromFile(filename)
if err != nil && !os.IsNotExist(err) {

configBytes, err := fs.ReadFile(filename)
switch {
case os.IsNotExist(err):
return clientcmdapi.NewConfig(), nil

case err != nil:
err = trace.ConvertSystemError(err)
return nil, trace.WrapWithMessage(err, "failed to load existing kubeconfig %q: %v", filename, err)
}

config, err := clientcmd.Load(configBytes)
if err != nil {
err = trace.ConvertSystemError(err)
return nil, trace.WrapWithMessage(err, "failed to parse existing kubeconfig %q: %v", filename, err)
}
if config == nil {
config = clientcmdapi.NewConfig()
}

// Now that we are using clientcmd.Load() we need to manually set all of the
// object origin values manually. We used to use clientcmd.LoadFile() that
// did it for us.
setConfigOriginsAndDefaults(config, filename)

return config, nil
}

// setConfigOriginsAndDefaults sets up the origin info for the config file.
func setConfigOriginsAndDefaults(config *clientcmdapi.Config, filename string) {
// set LocationOfOrigin on every Cluster, User, and Context
for key, obj := range config.AuthInfos {
obj.LocationOfOrigin = filename
config.AuthInfos[key] = obj
}
for key, obj := range config.Clusters {
obj.LocationOfOrigin = filename
config.Clusters[key] = obj
}
for key, obj := range config.Contexts {
obj.LocationOfOrigin = filename
config.Contexts[key] = obj
}

if config.AuthInfos == nil {
config.AuthInfos = map[string]*clientcmdapi.AuthInfo{}
}
if config.Clusters == nil {
config.Clusters = map[string]*clientcmdapi.Cluster{}
}
if config.Contexts == nil {
config.Contexts = map[string]*clientcmdapi.Context{}
}
}

// Save saves updated config to location specified by environment variable or
// default location
func Save(path string, config clientcmdapi.Config) error {
return SaveConfig(path, config, defaultConfigFS{})
}

// Save saves updated config to location specified by environment variable or
// default location.
func SaveConfig(path string, config clientcmdapi.Config, fs ConfigFS) error {
filename, err := finalPath(path)
if err != nil {
return trace.Wrap(err)
}

if err := clientcmd.WriteToFile(config, filename); err != nil {
configBytes, err := clientcmd.Write(config)
if err != nil {
return trace.ConvertSystemError(err)
}

if err := fs.WriteFile(filename, configBytes, 0600); err != nil {
return trace.ConvertSystemError(err)
}
return nil

}

// finalPath returns the final path to kubeceonfig using, in order of
// finalPath returns the final path to kubeconfig using, in order of
// precedence:
// - `customPath`, if not empty
// - ${KUBECONFIG} environment variable
Expand Down
10 changes: 10 additions & 0 deletions lib/tbot/config/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/identityfile"
"github.com/gravitational/teleport/lib/tbot/bot"
"github.com/gravitational/teleport/lib/tbot/identity"
)
Expand Down Expand Up @@ -129,6 +130,15 @@ func (b *BotConfigWriter) Stat(name string) (fs.FileInfo, error) {
return nil, &os.PathError{Op: "stat", Path: name, Err: os.ErrNotExist}
}

// ReadFile reads a given file. This implementation always returns not found.
func (b *BotConfigWriter) ReadFile(name string) ([]byte, error) {
return nil, &os.PathError{Op: "read", Path: name, Err: os.ErrNotExist}
}

// compile-time assertion that the BotConfigWriter implements the
// identityfile.ConfigWriter interface
var _ identityfile.ConfigWriter = (*BotConfigWriter)(nil)

// newClientKey returns a sane client.Key for the given bot identity.
func newClientKey(ident *identity.Identity, hostCAs []types.CertAuthority) (*client.Key, error) {
pk, err := keys.ParsePrivateKey(ident.PrivateKeyBytes)
Expand Down
8 changes: 4 additions & 4 deletions tool/tctl/common/auth_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,8 @@ func (a *AuthCommand) GenerateKeys(ctx context.Context) error {
// GenerateAndSignKeys generates a new keypair and signs it for role
func (a *AuthCommand) GenerateAndSignKeys(ctx context.Context, clusterAPI auth.ClientI) error {
if a.streamTarfile {
tarWriter := newTarWriter(os.Stdout, clockwork.NewRealClock())
defer tarWriter.Close()
tarWriter := newTarWriter(clockwork.NewRealClock())
defer tarWriter.Archive(os.Stdout)
a.identityWriter = tarWriter
}

Expand Down Expand Up @@ -952,7 +952,7 @@ func (a *AuthCommand) checkKubeCluster(ctx context.Context, clusterAPI auth.Clie
if a.outputFormat != identityfile.FormatKubernetes && a.kubeCluster != "" {
// User set --kube-cluster-name but it's not actually used for the chosen --format.
// Print a warning but continue.
fmt.Printf("Note: --kube-cluster-name is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
fmt.Fprintf(a.helperMsgDst(), "Note: --kube-cluster-name is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
}
if a.outputFormat != identityfile.FormatKubernetes {
return nil
Expand All @@ -979,7 +979,7 @@ func (a *AuthCommand) checkProxyAddr(ctx context.Context, clusterAPI auth.Client
if a.outputFormat != identityfile.FormatKubernetes && a.proxyAddr != "" {
// User set --proxy but it's not actually used for the chosen --format.
// Print a warning but continue.
fmt.Printf("Note: --proxy is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
fmt.Fprintf(a.helperMsgDst(), "Note: --proxy is only used with --format=%q, ignoring for --format=%q\n", identityfile.FormatKubernetes, a.outputFormat)
return nil
}
if a.outputFormat != identityfile.FormatKubernetes {
Expand Down
Loading