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 16, 2024
1 parent 3a97915 commit ade6fae
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 13 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
5 changes: 4 additions & 1 deletion integrations/terraform/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ require (
google.golang.org/protobuf v1.34.2
)

require github.com/hashicorp/terraform-plugin-log v0.9.0

require (
cel.dev/expr v0.15.0 // indirect
cloud.google.com/go v0.115.0 // indirect
Expand Down Expand Up @@ -250,7 +252,6 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.1 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
Expand Down Expand Up @@ -278,6 +279,7 @@ require (
github.com/jmoiron/sqlx v1.3.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.1.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 @@ -373,6 +375,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/std-uritemplate/std-uritemplate/go v0.0.55 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
Expand Down
14 changes: 14 additions & 0 deletions integrations/terraform/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,8 @@ github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6
github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fsouza/fake-gcs-server v1.49.2 h1:fukDqzEQM50QkA0jAbl6cLqeDu3maQjwZBuys759TR4=
github.com/fsouza/fake-gcs-server v1.49.2/go.mod h1:17SYzJEXRcaAA5ATwwvgBkSIqIy7r1icnGM0y/y4foY=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
Expand Down Expand Up @@ -1130,6 +1132,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-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
Expand Down Expand Up @@ -1639,6 +1643,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 @@ -2024,6 +2032,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/std-uritemplate/std-uritemplate/go v0.0.55 h1:muSH037g97K7U2f94G9LUuE8tZlJsoSSrPsO9V281WY=
github.com/std-uritemplate/std-uritemplate/go v0.0.55/go.mod h1:rG/bqh/ThY4xE5de7Rap3vaDkYUT76B0GPJ0loYeTTc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down Expand Up @@ -2120,6 +2130,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 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
Expand Down Expand Up @@ -2677,6 +2689,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 @@ -6,6 +6,9 @@ import (
"fmt"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/constants"
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 @@ -14,6 +17,7 @@ import (
)

var supportedCredentialSources = CredentialSources{
CredentialsFromNativeMachineID{},
CredentialsFromKeyAndCertPath{},
CredentialsFromKeyAndCertBase64{},
CredentialsFromIdentityFilePath{},
Expand Down Expand Up @@ -387,6 +391,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 ade6fae

Please sign in to comment.