Skip to content
This repository has been archived by the owner on Jun 4, 2024. It is now read-only.

add terraform support for Teleport servers #1019

Merged
merged 13 commits into from
Mar 27, 2024
Merged
2 changes: 1 addition & 1 deletion terraform/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ ifeq ($(shell expr $(CURRENT_ULIMIT) \< 1024), 1)
@echo "ulimit -n is too low ($(CURRENT_ULIMIT)), please set ulimit -n 1024"
@exit -1
endif
TF_ACC=true go test ./test -v
TF_ACC=true go test ./test -v # -run '^\QTestTerraform\E$$/^\QTestNamelessOpenSSHServer\E$$'
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved

.PHONY: apply
apply: install
Expand Down
15 changes: 15 additions & 0 deletions terraform/example/server.tf.example
marcoandredinis marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
resource "teleport_server" "ssh_agentless" {
version = "v2"
sub_kind = "openssh"
// Name is not required for servers, this is a special case.
// When a name is not set, an UUID will be generated by Teleport and
// imported back into Terraform.
// Giving unique IDs to servers allows UUID-based dialing (as opposed to
// host-based dialing and IP-based dialing) which is more robust than its
// counterparts as it can point to a specific server if multiple servers
// share the same hostname/ip.
spec = {
addr = "127.0.0.1:22"
hostname = "test.local"
}
}
28 changes: 28 additions & 0 deletions terraform/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ type payload struct {
// existing resource when we're updating it. For example:
// "Spec.Audit.NextAuditDate" in AccessList resource
PropagatedFields []string
// Namespaced indicates that the resource get and delete methods need the
// deprecated namespace parameter (always the default namespace).
Namespaced bool
// ForceSetKind indicates that the resource kind must be forcefully set by the provider.
// This is required for weird resources (ServerV2) that support multiple kinds.
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved
// For those resource, we must set the kind, and don't want to have the user do it.
ForceSetKind string
}

func (p *payload) CheckAndSetDefaults() error {
Expand Down Expand Up @@ -424,6 +431,24 @@ var (
HasCheckAndSetDefaults: true,
PropagatedFields: []string{"Spec.Audit.NextAuditDate"},
}

server = payload{
Name: "Server",
TypeName: "ServerV2",
VarName: "server",
GetMethod: "GetNode",
CreateMethod: "UpsertNode",
UpdateMethod: "UpsertNode",
UpsertMethodArity: 2,
DeleteMethod: "DeleteNode",
ID: "server.Metadata.Name",
Kind: "node",
HasStaticID: false,
TerraformResourceType: "teleport_server",
HasCheckAndSetDefaults: true,
Namespaced: true,
ForceSetKind: "apitypes.KindNode",
}
)

func main() {
Expand Down Expand Up @@ -469,6 +494,8 @@ func genTFSchema() {
generateDataSource(oktaImportRule, pluralDataSource)
generateResource(accessList, pluralResource)
generateDataSource(accessList, pluralDataSource)
generateResource(server, pluralResource)
generateDataSource(server, pluralDataSource)
}

func generateResource(p payload, tpl string) {
Expand Down Expand Up @@ -540,6 +567,7 @@ var (
"session_recording_config": tfschema.GenSchemaSessionRecordingConfigV2,
"trusted_cluster": tfschema.GenSchemaTrustedClusterV2,
"user": tfschema.GenSchemaUserV2,
"server": tfschema.GenSchemaServerV2,
}

// hiddenFields are fields that are not outputted to the reference doc.
Expand Down
5 changes: 4 additions & 1 deletion terraform/gen/plural_data_source.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
{{- if .Namespaced }}
"github.com/gravitational/teleport/api/defaults"
{{- end }}

{{ schemaImport . }}
)
Expand Down Expand Up @@ -65,7 +68,7 @@ func (r dataSourceTeleport{{.Name}}) Read(ctx context.Context, req tfsdk.ReadDat
return
}

{{.VarName}}I, err := r.p.Client.{{.GetMethod}}(ctx, id.Value{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}I, err := r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}id.Value{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading {{.Name}}", trace.Wrap(err), "{{.Kind}}"))
return
Expand Down
38 changes: 23 additions & 15 deletions terraform/gen/plural_resource.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ import (
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/jonboulle/clockwork"
{{- if .Namespaced }}
"github.com/gravitational/teleport/api/defaults"
{{- end }}

{{ schemaImport . }}
)
Expand Down Expand Up @@ -116,9 +119,22 @@ func (r resourceTeleport{{.Name}}) Create(ctx context.Context, req tfsdk.CreateR
{{- else }}
{{.VarName}}Resource := {{.VarName}}
{{end}}

{{- if .ForceSetKind }}
{{.VarName}}Resource.Kind = {{.ForceSetKind}}
{{- end}}

{{if .HasCheckAndSetDefaults -}}
err = {{.VarName}}Resource.CheckAndSetDefaults()
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error setting {{.Name}} defaults", trace.Wrap(err), "{{.Kind}}"))
return
}
{{- end}}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CheckAndSetDefaults and kind setting were moved before

id := {{.VarName}}Resource.Metadata.Name

because the name itself can be changed during this step (e.g. nameless openssh servers).

id := {{.VarName}}Resource.Metadata.Name

_, err = r.p.Client.{{.GetMethod}}(ctx, id{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
_, err = r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}id{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if !trace.IsNotFound(err) {
if err == nil {
existErr := fmt.Sprintf("{{.Name}} exists in Teleport. Either remove it (tctl rm {{.Kind}}/%v)"+
Expand All @@ -132,14 +148,6 @@ func (r resourceTeleport{{.Name}}) Create(ctx context.Context, req tfsdk.CreateR
return
}

{{if .HasCheckAndSetDefaults -}}
err = {{.VarName}}Resource.CheckAndSetDefaults()
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error setting {{.Name}} defaults", trace.Wrap(err), "{{.Kind}}"))
return
}
{{- end}}

{{if eq .UpsertMethodArity 2}}_, {{end}}err = r.p.Client.{{.CreateMethod}}(ctx, {{.VarName}}Resource)
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error creating {{.Name}}", trace.Wrap(err), "{{.Kind}}"))
Expand All @@ -159,7 +167,7 @@ func (r resourceTeleport{{.Name}}) Create(ctx context.Context, req tfsdk.CreateR
backoff := backoff.NewDecorr(r.p.RetryConfig.Base, r.p.RetryConfig.Cap, clockwork.NewRealClock())
for {
tries = tries + 1
{{.VarName}}I, err = r.p.Client.{{.GetMethod}}(ctx, id{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}I, err = r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}id{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if trace.IsNotFound(err) {
if bErr := backoff.Do(ctx); bErr != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading {{.Name}}", trace.Wrap(err), "{{.Kind}}"))
Expand Down Expand Up @@ -231,7 +239,7 @@ func (r resourceTeleport{{.Name}}) Read(ctx context.Context, req tfsdk.ReadResou
return
}

{{.VarName}}I, err := r.p.Client.{{.GetMethod}}(ctx, id.Value{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}I, err := r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}id.Value{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if trace.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
Expand Down Expand Up @@ -300,7 +308,7 @@ func (r resourceTeleport{{.Name}}) Update(ctx context.Context, req tfsdk.UpdateR
{{- end}}
name := {{.VarName}}Resource.Metadata.Name

{{.VarName}}Before, err := r.p.Client.{{.GetMethod}}(ctx, name{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}Before, err := r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}name{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading {{.Name}}", err, "{{.Kind}}"))
return
Expand Down Expand Up @@ -331,7 +339,7 @@ func (r resourceTeleport{{.Name}}) Update(ctx context.Context, req tfsdk.UpdateR
backoff := backoff.NewDecorr(r.p.RetryConfig.Base, r.p.RetryConfig.Cap, clockwork.NewRealClock())
for {
tries = tries + 1
{{.VarName}}I, err = r.p.Client.{{.GetMethod}}(ctx, name{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}I, err = r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}name{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading {{.Name}}", err, "{{.Kind}}"))
return
Expand Down Expand Up @@ -386,7 +394,7 @@ func (r resourceTeleport{{.Name}}) Delete(ctx context.Context, req tfsdk.DeleteR
return
}

err := r.p.Client.{{.DeleteMethod}}(ctx, id.Value)
err := r.p.Client.{{.DeleteMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}id.Value)
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error deleting {{.TypeName}}", trace.Wrap(err), "{{.Kind}}"))
return
Expand All @@ -397,7 +405,7 @@ func (r resourceTeleport{{.Name}}) Delete(ctx context.Context, req tfsdk.DeleteR

// ImportState imports {{.Name}} state
func (r resourceTeleport{{.Name}}) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
{{.VarName}}, err := r.p.Client.{{.GetMethod}}(ctx, req.ID{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
{{.VarName}}, err := r.p.Client.{{.GetMethod}}(ctx, {{if .Namespaced}}defaults.Namespace, {{end}}req.ID{{if ne .WithSecrets ""}}, {{.WithSecrets}}{{end}})
if err != nil {
resp.Diagnostics.Append(diagFromWrappedErr("Error reading {{.Name}}", trace.Wrap(err), "{{.Kind}}"))
return
Expand Down
2 changes: 1 addition & 1 deletion terraform/gen/singular_resource.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (r resourceTeleport{{.Name}}Type) NewResource(_ context.Context, p tfsdk.Pr
}, nil
}

// Create creates the provision token
// Create creates the {{.Name}}
func (r resourceTeleport{{.Name}}) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
if !r.p.IsConfigured(resp.Diagnostics) {
return
Expand Down
45 changes: 43 additions & 2 deletions terraform/protoc-gen-terraform-teleport.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ types:
- "ProvisionTokenV2"
- "RoleV6"
- "SAMLConnectorV2"
- "ServerV2"
- "SessionRecordingConfigV2"
- "TrustedClusterV2"
- "UserV2"
Expand Down Expand Up @@ -101,6 +102,13 @@ injected_fields:
computed: true
plan_modifiers:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
ServerV2:
-
name: id
type: github.com/hashicorp/terraform-plugin-framework/types.StringType
computed: true
plan_modifiers:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
SessionRecordingConfigV2:
-
name: id
Expand Down Expand Up @@ -144,6 +152,9 @@ exclude_fields:
- "RoleV6.Spec.Allow.Namespaces" # These fields are not settable via API
- "RoleV6.Spec.Deny.Namespaces"

# Server
- "ServerSpecV2.CmdLabels"

# SessionRecordingConfig
- "SessionRecordingConfigV2.Metadata.Name" # It's a singleton resource

Expand Down Expand Up @@ -241,7 +252,23 @@ computed_fields:
- "SAMLConnectorV2.Spec.EncryptionKeyPair.Cert"
- "SAMLConnectorV2.Kind"

# Session recording
# Server
- "ServerV2.Kind"
# Name is not required for servers, this is a special case.
# When a name is not set, an UUID will be generated by Teleport and
# imported back into Terraform.
# Giving unique IDs to servers allows UUID-based dialing (as opposed to
# host-based dialing and IP-based dialing) which is more robust than its
# counterparts as it can point to a specific server if multiple servers
# share the same hostname/ip.
- "ServerV2.Metadata.Name"
# Metadata must be marked computed as well, because we ccan potentially compute Metadata.Name.
# If there's no metadata and the attribute is not marked as comnputed, it will
# keep its values.Null = true field, which means its content won't be imported
# back into the state.
- "ServerV2.Metadata"

# Session recording
- "SessionRecordingConfigV2.Spec.Mode"
- "SessionRecordingConfigV2.Kind"

Expand Down Expand Up @@ -290,7 +317,7 @@ required_fields:
- "ProvisionTokenV2.Spec.Roles"
- "ProvisionTokenV2.Version"

# Role
# Role
- "RoleV6.Metadata.Name"
- "RoleV6.Version"

Expand All @@ -301,6 +328,10 @@ required_fields:
- "SAMLConnectorV2.Metadata.Name"
- "SAMLConnectorV2.Version"

# Server
- "ServerV2.Version"
- "ServerV2.SubKind"

# Trusted cluster
- "TrustedClusterV2.Metadata.Name"
- "TrustedClusterV2.Version"
Expand Down Expand Up @@ -340,6 +371,9 @@ plan_modifiers:
ProvisionTokenV2.Metadata.Name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
ServerV2.Metadata.Name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"

# Version MUST NOT change. Due to the way Terraform imports back the resource
# in its state (the provider relies on `USeStateForUnknown`) and the fact
Expand Down Expand Up @@ -376,6 +410,9 @@ plan_modifiers:
UserV2.Version:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
ServerV2.Version:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
SessionRecordingConfigV2.Version:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
Expand Down Expand Up @@ -418,6 +455,10 @@ validators:
- UseVersionBetween(2,2)
SAMLConnectorV2.Spec:
- UseAnyOfValidator("entity_descriptor", "entity_descriptor_url")
ServerV2.Version:
- UseVersionBetween(2,2)
ServerV2.SubKind:
- UseValueIn([]string{"openssh", "openssh-ec2-ice"})
hugoShaka marked this conversation as resolved.
Show resolved Hide resolved
SessionRecordingConfigV2.Version:
- UseVersionBetween(2,2)
SessionRecordingConfigV2.Metadata.Labels:
Expand Down
Loading
Loading