Skip to content

Commit

Permalink
Add Terraform Provider native MachineID support
Browse files Browse the repository at this point in the history
  • Loading branch information
hugoShaka committed Jul 25, 2024
1 parent aadc044 commit 824faa7
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 12 deletions.
4 changes: 4 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,4 +500,8 @@ const (
EnvVarTerraformRetryMaxTries = "TF_TELEPORT_RETRY_MAX_TRIES"
// EnvVarTerraformDialTimeoutDuration is the environment variable configuring the Terraform provider dial timeout.
EnvVarTerraformDialTimeoutDuration = "TF_TELEPORT_DIAL_TIMEOUT_DURATION"
// EnvVarTerraformJoinMethod is the environment variable configuring the Terraform provider native MachineID join method.
EnvVarTerraformJoinMethod = "TF_TELEPORT_JOIN_METHOD"
// EnvVarTerraformJoinToken is the environment variable configuring the Terraform provider native MachineID join token.
EnvVarTerraformJoinToken = "TF_TELEPORT_JOIN_TOKEN"
)
26 changes: 19 additions & 7 deletions integrations/lib/embeddedtbot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (b *EmbeddedBot) Start(ctx context.Context) error {
}
}

func (b *EmbeddedBot) waitForClient(ctx context.Context, deadline time.Duration) (*client.Client, error) {
func (b *EmbeddedBot) waitForCredentials(ctx context.Context, deadline time.Duration) (client.Credentials, error) {
waitCtx, cancel := context.WithTimeout(ctx, deadline)
defer cancel()

Expand All @@ -148,20 +148,32 @@ func (b *EmbeddedBot) waitForClient(ctx context.Context, deadline time.Duration)
log.Infof("credential ready")
}

c, err := b.buildClient(ctx)
return c, trace.Wrap(err)

return b.credential, nil
}

// StartAndWaitForClient starts the EmbeddedBot and waits for a client to be available.
// This is the proper way of starting the EmbeddedBot. It returns an error if the
// EmbeddedBot is not able to get a certificate before the deadline.
// It returns an error if the EmbeddedBot is not able to get a certificate before the deadline.
// If you need a client.Credentials instead, you can use StartAndWaitForCredentials.
func (b *EmbeddedBot) StartAndWaitForClient(ctx context.Context, deadline time.Duration) (*client.Client, error) {
b.start(ctx)
c, err := b.waitForClient(ctx, deadline)
_, err := b.waitForCredentials(ctx, deadline)
if err != nil {
return nil, trace.Wrap(err)
}

c, err := b.buildClient(ctx)
return c, trace.Wrap(err)
}

// StartAndWaitForCredentials starts the EmbeddedBot and waits for credentials to become ready.
// It returns an error if the EmbeddedBot is not able to get a certificate before the deadline.
// If you need a client.Client instead, you can use StartAndWaitForClient.
func (b *EmbeddedBot) StartAndWaitForCredentials(ctx context.Context, deadline time.Duration) (client.Credentials, error) {
b.start(ctx)
creds, err := b.waitForCredentials(ctx, deadline)
return creds, trace.Wrap(err)
}

// buildClient reads tbot's memory disttination, retrieves the certificates
// and builds a new Teleport client using those certs.
func (b *EmbeddedBot) buildClient(ctx context.Context) (*client.Client, error) {
Expand Down
2 changes: 2 additions & 0 deletions integrations/terraform/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julienschmidt/httprouter v1.3.0 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
Expand Down Expand Up @@ -291,6 +292,7 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
Expand Down
12 changes: 12 additions & 0 deletions integrations/terraform/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,8 @@ github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk=
github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
Expand Down Expand Up @@ -1514,6 +1516,10 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
Expand Down Expand Up @@ -1839,6 +1845,8 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8=
github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
Expand Down Expand Up @@ -1928,6 +1936,8 @@ github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE=
Expand Down Expand Up @@ -2443,6 +2453,8 @@ golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNq
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0=
gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0=
Expand Down
74 changes: 74 additions & 0 deletions integrations/terraform/provider/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import (
"text/template"
"time"

apitypes "github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/lib/embeddedtbot"
tbotconfig "github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-log/tflog"
Expand All @@ -34,6 +37,7 @@ import (
)

var supportedCredentialSources = CredentialSources{
CredentialsFromNativeMachineID{},
CredentialsFromKeyAndCertPath{},
CredentialsFromKeyAndCertBase64{},
CredentialsFromIdentityFilePath{},
Expand Down Expand Up @@ -453,6 +457,76 @@ func (CredentialsFromProfile) Credentials(ctx context.Context, config providerDa
return client.LoadProfile(profileDir, profileName), nil
}

// CredentialsFromNativeMachineID builds credentials by performing a MachineID join and
type CredentialsFromNativeMachineID struct{}

// Name implements CredentialSource and returns the source name.
func (CredentialsFromNativeMachineID) Name() string {
return "by performing native MachineID joining"
}

// IsActive implements CredentialSource and returns if the source is active and why.
func (CredentialsFromNativeMachineID) IsActive(config providerData) (bool, string) {
joinMethod := stringFromConfigOrEnv(config.JoinMethod, constants.EnvVarTerraformJoinMethod, "")
joinToken := stringFromConfigOrEnv(config.JoinToken, constants.EnvVarTerraformJoinToken, "")

// This method is active as soon as a token or a join method are set.
active := joinMethod != "" || joinToken != ""
return activeReason(
active,
attributeTerraformJoinMethod, attributeTerraformJoinToken,
constants.EnvVarTerraformJoinMethod, constants.EnvVarTerraformJoinToken,
)
}

// Credentials implements CredentialSource and returns a client.Credentials for the provider.
func (CredentialsFromNativeMachineID) Credentials(ctx context.Context, config providerData) (client.Credentials, error) {
joinMethod := stringFromConfigOrEnv(config.JoinMethod, constants.EnvVarTerraformJoinMethod, "")
joinToken := stringFromConfigOrEnv(config.JoinToken, constants.EnvVarTerraformJoinToken, "")
addr := stringFromConfigOrEnv(config.Addr, constants.EnvVarTerraformAddress, "")
caPath := stringFromConfigOrEnv(config.RootCaPath, constants.EnvVarTerraformRootCertificates, "")

if joinMethod == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformJoinMethod, constants.EnvVarTerraformJoinMethod)
}
if joinToken == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformJoinMethod, constants.EnvVarTerraformJoinMethod)
}
if addr == "" {
return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformAddress, constants.EnvVarTerraformAddress)
}

// TODO: reject token JoinMethod (or gate behind an env var)

if err := apitypes.ValidateJoinMethod(apitypes.JoinMethod(joinMethod)); err != nil {
return nil, trace.Wrap(err, "Invalid Join Method")
}
botConfig := &embeddedtbot.BotConfig{
AuthServer: addr,
Onboarding: tbotconfig.OnboardingConfig{
TokenValue: joinToken,
CAPath: caPath,
JoinMethod: apitypes.JoinMethod(joinMethod),
},
CertificateTTL: time.Hour,
RenewalInterval: 20 * time.Minute,
}
bot, err := embeddedtbot.New(botConfig)
if err != nil {
return nil, trace.Wrap(err, "Failed to create bot configuration, this is a provider bug, please open a GitHub issue.")
}

preflightCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
defer cancel()
_, err = bot.Preflight(preflightCtx)
if err != nil {
return nil, trace.Wrap(err, "Failed to preflight bot configuration")
}

creds, err := bot.StartAndWaitForCredentials(ctx, 20*time.Second /* deadline */)
return creds, trace.Wrap(err, "Waiting for bot to obtain credentials")
}

// activeReason renders a user-friendly active reason message describing if the credentials source is active
// and which parameters are controlling its activity.
func activeReason(active bool, params ...string) (bool, string) {
Expand Down
41 changes: 41 additions & 0 deletions integrations/terraform/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ const (
attributeTerraformRetryMaxTries = "retry_max_tries"
// attributeTerraformDialTimeoutDuration is the attribute configuring the Terraform provider dial timeout.
attributeTerraformDialTimeoutDuration = "dial_timeout_duration"
// attributeTerraformJoinMethod is the attribute configuring the Terraform provider native MachineID join method.
attributeTerraformJoinMethod = "join_method"
// attributeTerraformJoinToken is the attribute configuring the Terraform provider native MachineID join token.
attributeTerraformJoinToken = "join_token"
)

type RetryConfig struct {
Expand All @@ -95,6 +99,7 @@ type Provider struct {
configured bool
Client *client.Client
RetryConfig RetryConfig
cancel context.CancelFunc
}

// providerData provider schema struct
Expand Down Expand Up @@ -131,6 +136,10 @@ type providerData struct {
RetryMaxTries types.String `tfsdk:"retry_max_tries"`
// DialTimeout sets timeout when trying to connect to the server.
DialTimeoutDuration types.String `tfsdk:"dial_timeout_duration"`
// JoinMethod is the MachineID join method.
JoinMethod types.String `tfsdk:"join_method"`
// JoinMethod is the MachineID join token.
JoinToken types.String `tfsdk:"join_token"`
}

// New returns an empty provider struct
Expand Down Expand Up @@ -229,6 +238,18 @@ func (p *Provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics)
Optional: true,
Description: fmt.Sprintf("DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformDialTimeoutDuration),
},
attributeTerraformJoinMethod: {
Type: types.StringType,
Sensitive: false,
Optional: true,
Description: fmt.Sprintf("Enables the native Terraform MachineID support. When set, Terraform uses MachineID to securely join the Teleport cluster and obtain credentials. See [the join method reference](./join-methods.mdx) for possible values, you must use [a delegated join method](./join-methods.mdx#secret-vs-delegated). This can also be set with the environment variable `%s`.", constants.EnvVarTerraformJoinMethod),
},
attributeTerraformJoinToken: {
Type: types.StringType,
Sensitive: false,
Optional: true,
Description: fmt.Sprintf("Name of the token used for the native MachineID joining. This value is not sensitive for [delegated join methods](./join-methods.mdx#secret-vs-delegated). This can also be set with the environment variable `%s`.", constants.EnvVarTerraformJoinToken),
},
},
}, nil
}
Expand All @@ -249,6 +270,13 @@ func (p *Provider) IsConfigured(diags diag.Diagnostics) bool {
func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) {
p.configureLog()

// We wrap the provider's context into a cancellable one.
// This allows us to cancel the context and properly close the client and any background task potentially running
// (e.g. MachineID bot renewing creds). This is required during the tests as the provider is run multiple times.
// You can cancel the context by calling Provider.Close()
ctx, cancel := context.WithCancel(ctx)
p.cancel = cancel

var config providerData
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
Expand Down Expand Up @@ -486,3 +514,16 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc
"teleport_access_list": dataSourceTeleportAccessListType{},
}, nil
}

// Close closes the provider's client and cancels its context.
// This is needed in the tests to avoid accumulating clients and running out of file descriptors.
func (p *Provider) Close() error {
var err error
if p.Client != nil {
err = p.Client.Close()
}
if p.cancel != nil {
p.cancel()
}
return err
}
Loading

0 comments on commit 824faa7

Please sign in to comment.