Skip to content

Commit

Permalink
Add --apply-on-starup flag (#19009)
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoShaka authored Dec 6, 2022
1 parent cb3e4d9 commit 4f89756
Show file tree
Hide file tree
Showing 12 changed files with 179 additions and 34 deletions.
1 change: 1 addition & 0 deletions docs/pages/reference/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ we recommend using a [configuration file](https://goteleport.com/docs/reference/
| `--ca-pin` | none | **string** `sha256:<hash>` | set CA pin to validate the Auth Server. Generated by `tctl status` |
| `--nodename` | value returned by the `hostname` command on the machine | **string** | assigns an alternative name for the node which can be used by clients to log in. |
| `-c, --config` | `/etc/teleport.yaml` | **string** `.yaml` filepath | starts services with config specified in the YAML file, overrides CLI flags if set |
| `--apply-on-startup` | none | **string** `.yaml` filepath | On startup, always apply resources described in the file at the given path. Only supports the following types: `token`. |
| `--bootstrap` | none | **string** `.yaml` filepath | bootstrap configured YAML resources {/* TODO link how to configure this file */} |
| `--labels` | none | **string** comma-separated list | assigns a set of labels to a node, for example env=dev,app=web. See the explanation of labeling mechanism in the [Labeling Nodes](../management/admin/labels.mdx) section. |
| `--insecure` | none | none | disable certificate validation on Proxy Service, validation still occurs on Auth Service. |
Expand Down
49 changes: 41 additions & 8 deletions lib/auth/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ type InitConfig struct {
// Authorities is a list of pre-configured authorities to supply on first start
Authorities []types.CertAuthority

// Resources is a list of previously backed-up resources used to
// ApplyOnStartupResources is a set of resources that should be applied
// on each Teleport start.
ApplyOnStartupResources []types.Resource

// BootstrapResources is a list of previously backed-up resources used to
// bootstrap backend on first start.
Resources []types.Resource
BootstrapResources []types.Resource

// AuthServiceName is a human-readable name of this CA. If several Auth services are running
// (managing multiple teleport clusters) this field is used to tell them apart in UIs
Expand Down Expand Up @@ -226,23 +230,32 @@ func Init(cfg InitConfig, opts ...ServerOption) (*Server, error) {
return nil, trace.Wrap(err)
}

// if resources are supplied, use them to bootstrap backend state
// if bootstrap resources are supplied, use them to bootstrap backend state
// on initial startup.
if len(cfg.Resources) > 0 {
if len(cfg.BootstrapResources) > 0 {
firstStart, err := isFirstStart(ctx, asrv, cfg)
if err != nil {
return nil, trace.Wrap(err)
}
if firstStart {
log.Infof("Applying %v bootstrap resources (first initialization)", len(cfg.Resources))
if err := checkResourceConsistency(ctx, asrv.keyStore, domainName, cfg.Resources...); err != nil {
log.Infof("Applying %v bootstrap resources (first initialization)", len(cfg.BootstrapResources))
if err := checkResourceConsistency(ctx, asrv.keyStore, domainName, cfg.BootstrapResources...); err != nil {
return nil, trace.Wrap(err, "refusing to bootstrap backend")
}
if err := local.CreateResources(ctx, cfg.Backend, cfg.Resources...); err != nil {
if err := local.CreateResources(ctx, cfg.Backend, cfg.BootstrapResources...); err != nil {
return nil, trace.Wrap(err, "backend bootstrap failed")
}
} else {
log.Warnf("Ignoring %v bootstrap resources (previously initialized)", len(cfg.Resources))
log.Warnf("Ignoring %v bootstrap resources (previously initialized)", len(cfg.BootstrapResources))
}
}

// if apply-on-startup resources are supplied, apply them
if len(cfg.ApplyOnStartupResources) > 0 {
log.Infof("Applying %v resources (apply-on-startup)", len(cfg.ApplyOnStartupResources))

if err := applyResources(ctx, asrv.Services, cfg.ApplyOnStartupResources); err != nil {
return nil, trace.Wrap(err, "applying resources failed")
}
}

Expand Down Expand Up @@ -1140,3 +1153,23 @@ func migrateDBAuthority(ctx context.Context, asrv *Server) error {

return nil
}

// Unlike when resources are loaded via --bootstrap, we're inserting elements via their service.
// This means consistency is checked. This function does not currently support applying resources
// with dependencies (like a user referring to a role) as it won't necessarily apply them in the
// right order.
func applyResources(ctx context.Context, service *Services, resources []types.Resource) error {
var err error
for _, resource := range resources {
switch r := resource.(type) {
case types.ProvisionToken:
err = service.Provisioner.UpsertToken(ctx, r)
default:
return trace.NotImplemented("cannot apply resource of type %T", resource)
}
if err != nil {
return trace.Wrap(err)
}
}
return nil
}
83 changes: 70 additions & 13 deletions lib/auth/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -732,53 +732,110 @@ func TestInit_bootstrap(t *testing.T) {
tests := []struct {
name string
modifyConfig func(*InitConfig)
wantErr bool
assertError require.ErrorAssertionFunc
}{
{
// Issue https://github.com/gravitational/teleport/issues/7853.
name: "OK bootstrap CAs",
modifyConfig: func(cfg *InitConfig) {
cfg.Resources = append(cfg.Resources, hostCA.Clone(), userCA.Clone(), jwtCA.Clone(), dbCA.Clone())
cfg.BootstrapResources = append(cfg.BootstrapResources, hostCA.Clone(), userCA.Clone(), jwtCA.Clone(), dbCA.Clone())
},
assertError: require.NoError,
},
{
name: "NOK bootstrap Host CA missing keys",
modifyConfig: func(cfg *InitConfig) {
cfg.Resources = append(cfg.Resources, invalidHostCA.Clone(), userCA.Clone(), jwtCA.Clone(), dbCA.Clone())
cfg.BootstrapResources = append(cfg.BootstrapResources, invalidHostCA.Clone(), userCA.Clone(), jwtCA.Clone(), dbCA.Clone())
},
wantErr: true,
assertError: require.Error,
},
{
name: "NOK bootstrap User CA missing keys",
modifyConfig: func(cfg *InitConfig) {
cfg.Resources = append(cfg.Resources, hostCA.Clone(), invalidUserCA.Clone(), jwtCA.Clone(), dbCA.Clone())
cfg.BootstrapResources = append(cfg.BootstrapResources, hostCA.Clone(), invalidUserCA.Clone(), jwtCA.Clone(), dbCA.Clone())
},
wantErr: true,
assertError: require.Error,
},
{
name: "NOK bootstrap JWT CA missing keys",
modifyConfig: func(cfg *InitConfig) {
cfg.Resources = append(cfg.Resources, hostCA.Clone(), userCA.Clone(), invalidJWTCA.Clone(), dbCA.Clone())
cfg.BootstrapResources = append(cfg.BootstrapResources, hostCA.Clone(), userCA.Clone(), invalidJWTCA.Clone(), dbCA.Clone())
},
wantErr: true,
assertError: require.Error,
},
{
name: "NOK bootstrap Database CA missing keys",
modifyConfig: func(cfg *InitConfig) {
cfg.Resources = append(cfg.Resources, hostCA.Clone(), userCA.Clone(), jwtCA.Clone(), invalidDBCA.Clone())
cfg.BootstrapResources = append(cfg.BootstrapResources, hostCA.Clone(), userCA.Clone(), jwtCA.Clone(), invalidDBCA.Clone())
},
wantErr: true,
assertError: require.Error,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.modifyConfig(&cfg)

_, err := Init(cfg)
hasErr := err != nil
require.Equal(t, test.wantErr, hasErr, err)
test.assertError(t, err)
})
}
}

const (
userYAML = `kind: user
version: v2
metadata:
name: joe
spec:
roles: ["admin"]`
tokenYAML = `kind: token
version: v2
metadata:
name: github-token
expires: "3000-01-01T00:00:00Z"
spec:
roles: [Bot]
join_method: github
bot_name: github-demo
github:
allow:
- repository: gravitational/example`
)

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

user := resourceFromYAML(t, userYAML).(types.User)
token := resourceFromYAML(t, tokenYAML).(types.ProvisionToken)

tests := []struct {
name string
modifyConfig func(*InitConfig)
assertError require.ErrorAssertionFunc
}{
{
name: "Apply unsupported resource",
modifyConfig: func(cfg *InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
},
assertError: require.Error,
},
{
name: "Apply ProvisionToken",
modifyConfig: func(cfg *InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
},
assertError: require.NoError,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.modifyConfig(&cfg)

_, err := Init(cfg)
test.assertError(t, err)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/client/api_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,7 @@ func newStandaloneTeleport(t *testing.T, clock clockwork.Clock) *standaloneBundl
},
})
require.NoError(t, err)
cfg.Auth.Resources = []types.Resource{user, role}
cfg.Auth.BootstrapResources = []types.Resource{user, role}
cfg.Auth.StaticTokens, err = types.NewStaticTokens(types.StaticTokensSpecV2{
StaticTokens: []types.ProvisionTokenV1{
{
Expand Down
17 changes: 16 additions & 1 deletion lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ type CommandLineFlags struct {
AdvertiseIP string
// --config flag
ConfigFile string
// --apply-on-startup contains the path of a YAML manifest whose resources should be
// applied on startup. Unlike the bootstrap flag, the resources are always applied,
// even if the cluster is already initialized. Existing resources will be updated.
ApplyOnStartupFile string
// Bootstrap flag contains a YAML file that defines a set of resources to bootstrap
// a cluster.
BootstrapFile string
Expand Down Expand Up @@ -1898,7 +1902,18 @@ func Configure(clf *CommandLineFlags, cfg *service.Config) error {
if len(resources) < 1 {
return trace.BadParameter("no resources found: %q", clf.BootstrapFile)
}
cfg.Auth.Resources = resources
cfg.Auth.BootstrapResources = resources
}

if clf.ApplyOnStartupFile != "" {
resources, err := ReadResources(clf.ApplyOnStartupFile)
if err != nil {
return trace.Wrap(err)
}
if len(resources) < 1 {
return trace.BadParameter("no resources found: %q", clf.ApplyOnStartupFile)
}
cfg.Auth.ApplyOnStartupResources = resources
}

// Apply command line --debug flag to override logger severity.
Expand Down
8 changes: 6 additions & 2 deletions lib/service/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -609,9 +609,13 @@ type AuthConfig struct {
// that will be added by this auth server on the first start
Authorities []types.CertAuthority

// Resources is a set of previously backed up resources
// BootstrapResources is a set of previously backed up resources
// used to bootstrap backend state on the first start.
Resources []types.Resource
BootstrapResources []types.Resource

// ApplyOnStartupResources is a set of resources that should be applied
// on each Teleport start.
ApplyOnStartupResources []types.Resource

// Roles is a set of roles to pre-provision for this cluster
Roles []types.Role
Expand Down
3 changes: 2 additions & 1 deletion lib/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,8 @@ func (process *TeleportProcess) initAuthService() error {
HostUUID: cfg.HostUUID,
NodeName: cfg.Hostname,
Authorities: cfg.Auth.Authorities,
Resources: cfg.Auth.Resources,
ApplyOnStartupResources: cfg.Auth.ApplyOnStartupResources,
BootstrapResources: cfg.Auth.BootstrapResources,
ReverseTunnels: cfg.ReverseTunnels,
Trust: cfg.Trust,
Presence: cfg.Presence,
Expand Down
18 changes: 18 additions & 0 deletions lib/services/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,24 @@ func init() {
}
return role, nil
})
RegisterResourceMarshaler(types.KindToken, func(resource types.Resource, opts ...MarshalOption) ([]byte, error) {
token, ok := resource.(types.ProvisionToken)
if !ok {
return nil, trace.BadParameter("expected Token, got %T", resource)
}
bytes, err := MarshalProvisionToken(token, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return bytes, nil
})
RegisterResourceUnmarshaler(types.KindToken, func(bytes []byte, opts ...MarshalOption) (types.Resource, error) {
token, err := UnmarshalProvisionToken(bytes, opts...)
if err != nil {
return nil, trace.Wrap(err)
}
return token, nil
})
}

// MarshalResource attempts to marshal a resource dynamically, returning NotImplementedError
Expand Down
5 changes: 4 additions & 1 deletion tool/teleport/common/teleport.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,11 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
start.Flag("config",
fmt.Sprintf("Path to a configuration file [%v]", defaults.ConfigFilePath)).
Short('c').ExistingFileVar(&ccf.ConfigFile)
start.Flag("apply-on-startup",
fmt.Sprintf("Path to a non-empty YAML file containing resources to apply on startup. Works on initialized clusters, unlike --bootstrap. Only supports the following types: %s.", types.KindToken)).
ExistingFileVar(&ccf.ApplyOnStartupFile)
start.Flag("bootstrap",
"Path to bootstrap file (ignored if already initialized)").ExistingFileVar(&ccf.BootstrapFile)
"Path to a non-empty YAML file containing bootstrap resources (ignored if already initialized)").ExistingFileVar(&ccf.BootstrapFile)
start.Flag("config-string",
"Base64 encoded configuration string").Hidden().Envar(defaults.ConfigEnvar).
StringVar(&ccf.ConfigString)
Expand Down
21 changes: 17 additions & 4 deletions tool/teleport/common/teleport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,24 @@ func TestTeleportMain(t *testing.T) {
InitOnly: true,
})
require.Equal(t, "start", cmd)
require.Equal(t, len(bootstrapEntries), len(conf.Auth.Resources))
require.Equal(t, len(bootstrapEntries), len(conf.Auth.BootstrapResources))
for i, entry := range bootstrapEntries {
require.Equal(t, entry.kind, conf.Auth.Resources[i].GetKind(), entry.fileName)
require.Equal(t, entry.name, conf.Auth.Resources[i].GetName(), entry.fileName)
require.NoError(t, conf.Auth.Resources[i].CheckAndSetDefaults(), entry.fileName)
require.Equal(t, entry.kind, conf.Auth.BootstrapResources[i].GetKind(), entry.fileName)
require.Equal(t, entry.name, conf.Auth.BootstrapResources[i].GetName(), entry.fileName)
require.NoError(t, conf.Auth.BootstrapResources[i].CheckAndSetDefaults(), entry.fileName)
}
})
t.Run("ApplyOnStartup", func(t *testing.T) {
_, cmd, conf := Run(Options{
Args: []string{"start", "--apply-on-startup", bootstrapFile},
InitOnly: true,
})
require.Equal(t, "start", cmd)
require.Equal(t, len(bootstrapEntries), len(conf.Auth.ApplyOnStartupResources))
for i, entry := range bootstrapEntries {
require.Equal(t, entry.kind, conf.Auth.ApplyOnStartupResources[i].GetKind(), entry.fileName)
require.Equal(t, entry.name, conf.Auth.ApplyOnStartupResources[i].GetName(), entry.fileName)
require.NoError(t, conf.Auth.ApplyOnStartupResources[i].CheckAndSetDefaults(), entry.fileName)
}
})
}
Expand Down
4 changes: 2 additions & 2 deletions tool/tsh/tsh_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func (s *suite) setupRootCluster(t *testing.T, options testSuiteOptions) {
s.user, err = types.NewUser("alice")
require.NoError(t, err)
s.user.SetRoles([]string{"access", "ssh-login", "kube-login"})
cfg.Auth.Resources = []types.Resource{s.connector, s.user, sshLoginRole, kubeLoginRole}
cfg.Auth.BootstrapResources = []types.Resource{s.connector, s.user, sshLoginRole, kubeLoginRole}

if options.rootConfigFunc != nil {
options.rootConfigFunc(cfg)
Expand Down Expand Up @@ -189,7 +189,7 @@ func (s *suite) setupLeafCluster(t *testing.T, options testSuiteOptions) {
},
})
require.NoError(t, err)
cfg.Auth.Resources = []types.Resource{sshLoginRole}
cfg.Auth.BootstrapResources = []types.Resource{sshLoginRole}
if options.leafConfigFunc != nil {
options.leafConfigFunc(cfg)
}
Expand Down
2 changes: 1 addition & 1 deletion tool/tsh/tsh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2437,7 +2437,7 @@ func makeTestServers(t *testing.T, opts ...testServerOptFunc) (auth *service.Tel
cfg.DataDir = t.TempDir()

cfg.SetAuthServerAddress(utils.NetAddr{AddrNetwork: "tcp", Addr: net.JoinHostPort("127.0.0.1", ports.Pop())})
cfg.Auth.Resources = options.bootstrap
cfg.Auth.BootstrapResources = options.bootstrap
cfg.Auth.StorageConfig.Params = backend.Params{defaults.BackendPath: filepath.Join(cfg.DataDir, defaults.BackendDir)}
cfg.Auth.StaticTokens, err = types.NewStaticTokens(types.StaticTokensSpecV2{
StaticTokens: []types.ProvisionTokenV1{{
Expand Down

0 comments on commit 4f89756

Please sign in to comment.