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
44 changes: 0 additions & 44 deletions lib/tbot/bot/service.go

This file was deleted.

29 changes: 24 additions & 5 deletions lib/tbot/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ type BotConfig struct {
Onboarding OnboardingConfig `yaml:"onboarding,omitempty"`
Storage *StorageConfig `yaml:"storage,omitempty"`
Outputs Outputs `yaml:"outputs,omitempty"`
Services Services `yaml:"services,omitempty"`
Services ServiceConfigs `yaml:"services,omitempty"`

Debug bool `yaml:"debug"`
AuthServer string `yaml:"auth_server,omitempty"`
Expand Down Expand Up @@ -374,6 +374,13 @@ func (conf *BotConfig) CheckAndSetDefaults() error {
}
}

// Validate configured services
for i, service := range conf.Services {
if err := service.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err, "validating service[%d]", i)
}
}

if conf.CertificateTTL == 0 {
conf.CertificateTTL = DefaultCertificateTTL
}
Expand Down Expand Up @@ -418,11 +425,17 @@ func (conf *BotConfig) CheckAndSetDefaults() error {
return nil
}

// Services assists polymorphic unmarshaling of a slice of Services.
type Services []bot.Service
// ServiceConfig is an interface over the various service configurations.
type ServiceConfig interface {
Type() string
CheckAndSetDefaults() error
}

// ServiceConfigs assists polymorphic unmarshaling of a slice of ServiceConfigs.
type ServiceConfigs []ServiceConfig

func (o *Services) UnmarshalYAML(node *yaml.Node) error {
var out []bot.Service
func (o *ServiceConfigs) UnmarshalYAML(node *yaml.Node) error {
var out []ServiceConfig
for _, node := range node.Content {
header := struct {
Type string `yaml:"type"`
Expand All @@ -438,6 +451,12 @@ func (o *Services) UnmarshalYAML(node *yaml.Node) error {
return trace.Wrap(err)
}
out = append(out, v)
case DatabaseTunnelServiceType:
v := &DatabaseTunnelService{}
if err := node.Decode(v); err != nil {
return trace.Wrap(err)
}
out = append(out, v)
default:
return trace.BadParameter("unrecognized service type (%s)", header.Type)
}
Expand Down
2 changes: 1 addition & 1 deletion lib/tbot/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ func TestBotConfig_YAML(t *testing.T) {
},
},
},
Services: []bot.Service{
Services: []ServiceConfig{
&ExampleService{
Message: "llama",
},
Expand Down
10 changes: 10 additions & 0 deletions lib/tbot/config/output_client_credential.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ func (o *UnstableClientCredentialOutput) SSHClientConfig() (*ssh.ClientConfig, e
return o.facade.SSHClientConfig()
}

// Facade returns the underlying facade
func (o *UnstableClientCredentialOutput) Facade() (*identity.Facade, error) {
o.mu.Lock()
defer o.mu.Unlock()
if o.facade == nil {
return nil, trace.BadParameter("credentials not yet ready")
}
return o.facade, nil
}

// Render implements the Destination interface and is called regularly by the
// bot with new credentials. Render passes these credentials down to the
// underlying facade so that they can be used in TLS/SSH configs.
Expand Down
82 changes: 82 additions & 0 deletions lib/tbot/config/service_database_tunnel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Teleport
* Copyright (C) 2024 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 (
"net/url"

"github.com/gravitational/trace"
"gopkg.in/yaml.v3"
)

const DatabaseTunnelServiceType = "database-tunnel"

// DatabaseTunnelService opens an authenticated tunnel for Database Access.
type DatabaseTunnelService struct {
// Listen is the address on which database tunnel should listen. Example:
// - "tcp://127.0.0.1:3306"
// - "tcp://0.0.0.0:3306
Listen string `yaml:"listen"`
// Roles is the list of roles to request for the tunnel.
// If empty, it defaults to all the bot's roles.
Roles []string `yaml:"roles,omitempty"`
// Service is the service name of the Teleport database. Generally this is
// the name of the Teleport resource. This field is required for all types
// of database.
Service string `yaml:"service"`
// Database is the name of the database to proxy to.
Database string `yaml:"database"`
// Username is the database username to proxy as.
Username string `yaml:"username"`
}

func (s *DatabaseTunnelService) Type() string {
return DatabaseTunnelServiceType
}

func (s *DatabaseTunnelService) MarshalYAML() (interface{}, error) {
type raw DatabaseTunnelService
return withTypeHeader((*raw)(s), DatabaseTunnelServiceType)
}

func (s *DatabaseTunnelService) UnmarshalYAML(node *yaml.Node) error {
// Alias type to remove UnmarshalYAML to avoid recursion
type raw DatabaseTunnelService
if err := node.Decode((*raw)(s)); err != nil {
return trace.Wrap(err)
}
return nil
}

func (s *DatabaseTunnelService) CheckAndSetDefaults() error {
switch {
case s.Listen == "":
return trace.BadParameter("listen: should not be empty")
case s.Service == "":
return trace.BadParameter("service: should not be empty")
case s.Database == "":
return trace.BadParameter("database: should not be empty")
case s.Username == "":
return trace.BadParameter("username: should not be empty")
}
if _, err := url.Parse(s.Listen); err != nil {
return trace.Wrap(err, "parsing listen")
}
return nil
}
108 changes: 108 additions & 0 deletions lib/tbot/config/service_database_tunnel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* Teleport
* Copyright (C) 2024 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 "testing"

func TestDatabaseTunnelService_YAML(t *testing.T) {
t.Parallel()

tests := []testYAMLCase[DatabaseTunnelService]{
{
name: "full",
in: DatabaseTunnelService{
Listen: "tcp://0.0.0.0:3621",
Roles: []string{"role1", "role2"},
Service: "service",
Database: "database",
Username: "username",
},
},
}
testYAML(t, tests)
}

func TestDatabaseTunnelService_CheckAndSetDefaults(t *testing.T) {
t.Parallel()

tests := []testCheckAndSetDefaultsCase[*DatabaseTunnelService]{
{
name: "valid",
in: func() *DatabaseTunnelService {
return &DatabaseTunnelService{
Listen: "tcp://0.0.0.0:3621",
Roles: []string{"role1", "role2"},
Service: "service",
Database: "database",
Username: "username",
}
},
wantErr: "",
},
{
name: "missing listen",
in: func() *DatabaseTunnelService {
return &DatabaseTunnelService{
Roles: []string{"role1", "role2"},
Service: "service",
Database: "database",
Username: "username",
}
},
wantErr: "listen: should not be empty",
},
{
name: "missing service",
in: func() *DatabaseTunnelService {
return &DatabaseTunnelService{
Listen: "tcp://0.0.0.0:3621",
Roles: []string{"role1", "role2"},
Database: "database",
Username: "username",
}
},
wantErr: "service: should not be empty",
},
{
name: "missing database",
in: func() *DatabaseTunnelService {
return &DatabaseTunnelService{
Listen: "tcp://0.0.0.0:3621",
Roles: []string{"role1", "role2"},
Service: "service",
Username: "username",
}
},
wantErr: "database: should not be empty",
},
{
name: "missing username",
in: func() *DatabaseTunnelService {
return &DatabaseTunnelService{
Listen: "tcp://0.0.0.0:3621",
Roles: []string{"role1", "role2"},
Service: "service",
Database: "database",
}
},
wantErr: "username: should not be empty",
},
}
testCheckAndSetDefaults(t, tests)
}
22 changes: 5 additions & 17 deletions lib/tbot/config/service_example.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@
package config

import (
"context"
"fmt"
"time"

"github.com/gravitational/trace"
"gopkg.in/yaml.v3"
)
Expand All @@ -36,17 +32,6 @@ type ExampleService struct {
Message string `yaml:"message"`
}

func (s *ExampleService) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Second * 5):
fmt.Println("Example Service prints message:", s.Message)
}
}
}

func (s *ExampleService) Type() string {
return ExampleServiceType
}
Expand All @@ -65,6 +50,9 @@ func (s *ExampleService) UnmarshalYAML(node *yaml.Node) error {
return nil
}

func (s *ExampleService) String() string {
return fmt.Sprintf("%s:%s", ExampleServiceType, s.Message)
func (s *ExampleService) CheckAndSetDefaults() error {
if s.Message == "" {
return trace.BadParameter("message: should not be empty")
}
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type: database-tunnel
listen: tcp://0.0.0.0:3621
roles:
- role1
- role2
service: service
database: database
username: username
Loading