From ebb728fbd2f3b256acdc0361b91b03b17abfd16c Mon Sep 17 00:00:00 2001 From: rosstimothy <39066650+rosstimothy@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:19:03 -0400 Subject: [PATCH] Terraform: Convert Auth Preference to generic provider (#69099) --- integrations/terraform/gen/main.go | 18 - .../data_source_teleport_auth_preference.go | 89 ---- .../provider/internal/legacy/registry.go | 2 - .../resource_teleport_auth_preference.go | 384 ------------------ .../internal/resources/auth_preference.go | 67 +++ .../internal/teleport/auth_preference.go | 72 ++++ .../provider/internal/tfdriver/resource.go | 79 ++-- integrations/terraform/provider/provider.go | 2 + .../terraform/testlib/auth_preference_test.go | 17 +- 9 files changed, 187 insertions(+), 543 deletions(-) delete mode 100755 integrations/terraform/provider/internal/legacy/data_source_teleport_auth_preference.go delete mode 100755 integrations/terraform/provider/internal/legacy/resource_teleport_auth_preference.go create mode 100644 integrations/terraform/provider/internal/resources/auth_preference.go create mode 100644 integrations/terraform/provider/internal/teleport/auth_preference.go diff --git a/integrations/terraform/gen/main.go b/integrations/terraform/gen/main.go index 3ca7aae79f1..05d16876b69 100644 --- a/integrations/terraform/gen/main.go +++ b/integrations/terraform/gen/main.go @@ -261,22 +261,6 @@ const ( ) var ( - authPreference = payload{ - Name: "AuthPreference", - TypeName: "AuthPreferenceV2", - VarName: "authPreference", - GetMethod: "GetAuthPreference", - CreateMethod: "UpsertAuthPreference", - UpdateMethod: "UpsertAuthPreference", - UpsertMethodArity: 2, - DeleteMethod: "ResetAuthPreference", - ID: `"auth_preference"`, - Kind: "cluster_auth_preference", - HasStaticID: false, - TerraformResourceType: "teleport_auth_preference", - HasCheckAndSetDefaults: true, - } - clusterMaintenance = payload{ Name: "ClusterMaintenanceConfig", TypeName: "ClusterMaintenanceConfigV1", @@ -1121,8 +1105,6 @@ func main() { } func genTFSchema() { - generateResource(authPreference, singularResource) - generateDataSource(authPreference, singularDataSource) generateResource(clusterMaintenance, singularResource) generateDataSource(clusterMaintenance, singularDataSource) generateResource(clusterNetworking, singularResource) diff --git a/integrations/terraform/provider/internal/legacy/data_source_teleport_auth_preference.go b/integrations/terraform/provider/internal/legacy/data_source_teleport_auth_preference.go deleted file mode 100755 index 51287af6125..00000000000 --- a/integrations/terraform/provider/internal/legacy/data_source_teleport_auth_preference.go +++ /dev/null @@ -1,89 +0,0 @@ -// Code generated by _gen/main.go DO NOT EDIT -/* -Copyright 2015-2024 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package legacy - -import ( - "context" - - apitypes "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/gravitational/teleport/integrations/terraform/tfschema" - "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag" -) - -// dataSourceTeleportAuthPreferenceType is the data source metadata type -type dataSourceTeleportAuthPreferenceType struct{} - -// dataSourceTeleportAuthPreference is the resource -type dataSourceTeleportAuthPreference struct { - p Provider -} - -// GetSchema returns the data source schema -func (r dataSourceTeleportAuthPreferenceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfschema.GenSchemaAuthPreferenceV2(ctx) -} - -// NewDataSource creates the empty data source -func (r dataSourceTeleportAuthPreferenceType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { - return dataSourceTeleportAuthPreference{ - p: p.(Provider), - }, nil -} - -// Read reads teleport AuthPreference -func (r dataSourceTeleportAuthPreference) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) { - authPreferenceI, err := r.p.Client().GetAuthPreference(ctx) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - var state types.Object - resp.Diagnostics.Append(req.Config.Get(ctx, &state)...) - if resp.Diagnostics.HasError() { - return - } - - authPreference := authPreferenceI.(*apitypes.AuthPreferenceV2) - diags := tfschema.CopyAuthPreferenceV2ToTerraform(ctx, authPreference, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - // Todo: Remove after updating terraform-plugin to >=v1.5.0. - // terraform-plugin-testing version <1.5.0 requires data resources to - // implement the 'id' attribute. - // https://developer.hashicorp.com/terraform/plugin/framework/acctests#no-id-found-in-attributes - v, ok := state.Attrs["id"] - if !ok || v.IsNull() { - id := authPreference.GetName() - state.Attrs["id"] = types.String{Value: id} - } - - diags = resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} diff --git a/integrations/terraform/provider/internal/legacy/registry.go b/integrations/terraform/provider/internal/legacy/registry.go index d25b1d22190..04c5ad5d22b 100644 --- a/integrations/terraform/provider/internal/legacy/registry.go +++ b/integrations/terraform/provider/internal/legacy/registry.go @@ -22,7 +22,6 @@ import "github.com/hashicorp/terraform-plugin-framework/tfsdk" func DataSourceTypes() map[string]tfsdk.DataSourceType { return map[string]tfsdk.DataSourceType{ "teleport_app_auth_config": dataSourceTeleportAppAuthConfigType{}, - "teleport_auth_preference": dataSourceTeleportAuthPreferenceType{}, "teleport_cluster_maintenance_config": dataSourceTeleportClusterMaintenanceConfigType{}, "teleport_cluster_networking_config": dataSourceTeleportClusterNetworkingConfigType{}, "teleport_database": dataSourceTeleportDatabaseType{}, @@ -68,7 +67,6 @@ func DataSourceTypes() map[string]tfsdk.DataSourceType { func ResourceTypes() map[string]tfsdk.ResourceType { return map[string]tfsdk.ResourceType{ "teleport_app_auth_config": resourceTeleportAppAuthConfigType{}, - "teleport_auth_preference": resourceTeleportAuthPreferenceType{}, "teleport_cluster_maintenance_config": resourceTeleportClusterMaintenanceConfigType{}, "teleport_cluster_networking_config": resourceTeleportClusterNetworkingConfigType{}, "teleport_database": resourceTeleportDatabaseType{}, diff --git a/integrations/terraform/provider/internal/legacy/resource_teleport_auth_preference.go b/integrations/terraform/provider/internal/legacy/resource_teleport_auth_preference.go deleted file mode 100755 index bd2c789f2bf..00000000000 --- a/integrations/terraform/provider/internal/legacy/resource_teleport_auth_preference.go +++ /dev/null @@ -1,384 +0,0 @@ -// Code generated by _gen/main.go DO NOT EDIT -/* -Copyright 2015-2024 Gravitational, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package legacy - -import ( - "context" - "fmt" - - apitypes "github.com/gravitational/teleport/api/types" - "github.com/gravitational/trace" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/gravitational/teleport/integrations/terraform/tfschema" - "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag" -) - -// resourceTeleportAuthPreferenceType is the resource metadata type -type resourceTeleportAuthPreferenceType struct{} - -// resourceTeleportAuthPreference is the resource -type resourceTeleportAuthPreference struct { - p Provider -} - -// GetSchema returns the resource schema -func (r resourceTeleportAuthPreferenceType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { - return tfschema.GenSchemaAuthPreferenceV2(ctx) -} - -// NewResource creates the empty resource -func (r resourceTeleportAuthPreferenceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceTeleportAuthPreference{ - p: p.(Provider), - }, nil -} - -// Create creates the AuthPreference -func (r resourceTeleportAuthPreference) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - if !r.p.IsConfigured(resp.Diagnostics) { - return - } - - var plan types.Object - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - authPreference := &apitypes.AuthPreferenceV2{} - diags = tfschema.CopyAuthPreferenceV2FromTerraform(ctx, plan, authPreference) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err := authPreference.CheckAndSetDefaults() - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error setting AuthPreference defaults", trace.Wrap(err), "cluster_auth_preference")) - return - } - - - - authPreferenceBefore, err := r.p.Client().GetAuthPreference(ctx) - if err != nil && !trace.IsNotFound(err) { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - _, err = r.p.Client().UpsertAuthPreference(ctx, authPreference) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error creating AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - var authPreferenceI apitypes.AuthPreference - - // Try getting the resource until it exists and is different than the previous ones. - // There are two types of singleton resources: - // - the ones who can deleted and return a trace.NotFoundErr - // - the ones who cannot be deleted, only reset. In this case, the resource revision is used to know if the change got applied. - tries := 0 - retry, err := r.p.Retry() - if err != nil { - return - } - for { - tries = tries + 1 - authPreferenceI, err = r.p.Client().GetAuthPreference(ctx) - if trace.IsNotFound(err) { - select { - case <-ctx.Done(): - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(ctx.Err()), "cluster_auth_preference")) - return - case <-retry.After(): - } - if tries >= r.p.MaxRetries() { - diagMessage := fmt.Sprintf("Error reading AuthPreference (tried %d times) - state outdated, please import resource", tries) - resp.Diagnostics.AddError(diagMessage, "cluster_auth_preference") - return - } - continue - } - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - previousMetadata := authPreferenceBefore.GetMetadata() - currentMetadata := authPreferenceI.GetMetadata() - if previousMetadata.GetRevision() != currentMetadata.GetRevision() || false { - break - } - select { - case <-ctx.Done(): - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(ctx.Err()), "cluster_auth_preference")) - return - case <-retry.After(): - } - if tries >= r.p.MaxRetries() { - diagMessage := fmt.Sprintf("Error reading AuthPreference (tried %d times) - state outdated, please import resource", tries) - resp.Diagnostics.AddError(diagMessage, "cluster_auth_preference") - return - } - } - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - authPreference, ok := authPreferenceI.(*apitypes.AuthPreferenceV2) - if !ok { - resp.Diagnostics.Append( - tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Errorf("Can not convert %T to AuthPreferenceV2", authPreferenceI), "cluster_auth_preference"), - ) - return - } - - diags = tfschema.CopyAuthPreferenceV2ToTerraform(ctx, authPreference, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - plan.Attrs["id"] = types.String{Value: "auth_preference"} - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// Read reads teleport AuthPreference -func (r resourceTeleportAuthPreference) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { - var state types.Object - diags := req.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - authPreferenceI, err := r.p.Client().GetAuthPreference(ctx) - if trace.IsNotFound(err) { - resp.State.RemoveResource(ctx) - return - } - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - - authPreference := authPreferenceI.(*apitypes.AuthPreferenceV2) - diags = tfschema.CopyAuthPreferenceV2ToTerraform(ctx, authPreference, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// Update updates teleport AuthPreference -func (r resourceTeleportAuthPreference) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { - if !r.p.IsConfigured(resp.Diagnostics) { - return - } - - var plan types.Object - diags := req.Plan.Get(ctx, &plan) - - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - authPreference := &apitypes.AuthPreferenceV2{} - diags = tfschema.CopyAuthPreferenceV2FromTerraform(ctx, plan, authPreference) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - err := authPreference.CheckAndSetDefaults() - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - authPreferenceBefore, err := r.p.Client().GetAuthPreference(ctx) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - _, err = r.p.Client().UpsertAuthPreference(ctx, authPreference) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - var authPreferenceI apitypes.AuthPreference - - tries := 0 - retry, err := r.p.Retry() - if err != nil { - return - } - for { - tries = tries + 1 - authPreferenceI, err = r.p.Client().GetAuthPreference(ctx) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - if authPreferenceBefore.GetMetadata().Revision != authPreferenceI.GetMetadata().Revision || false { - break - } - select { - case <-ctx.Done(): - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(ctx.Err()), "cluster_auth_preference")) - return - case <-retry.After(): - } - if tries >= r.p.MaxRetries() { - diagMessage := fmt.Sprintf("Error reading AuthPreference (tried %d times) - state outdated, please import resource", tries) - resp.Diagnostics.AddError(diagMessage, "cluster_auth_preference") - return - } - } - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - - authPreference = authPreferenceI.(*apitypes.AuthPreferenceV2) - diags = tfschema.CopyAuthPreferenceV2ToTerraform(ctx, authPreference, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = resp.State.Set(ctx, plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// Delete deletes Teleport AuthPreference -func (r resourceTeleportAuthPreference) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { - err := r.p.Client().ResetAuthPreference(ctx) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error deleting AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - - resp.State.RemoveResource(ctx) -} - -// ImportState imports AuthPreference state -func (r resourceTeleportAuthPreference) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - authPreferenceI, err := r.p.Client().GetAuthPreference(ctx) - if err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating AuthPreference", trace.Wrap(err), "cluster_auth_preference")) - return - } - authPreference := authPreferenceI.(*apitypes.AuthPreferenceV2) - - var state types.Object - - diags := resp.State.Get(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - diags = tfschema.CopyAuthPreferenceV2ToTerraform(ctx, authPreference, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - id := authPreference.GetName() - - state.Attrs["id"] = types.String{Value: id} - - diags = resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -// ModifyPlan modifies the planned value, normalizing null values. -func (r resourceTeleportAuthPreference) ModifyPlan(ctx context.Context, req tfsdk.ModifyResourcePlanRequest, resp *tfsdk.ModifyResourcePlanResponse) { - // If the entire plan is null, the resource is planned for destruction. - if req.Plan.Raw.IsNull() { - return - } - - // If the state is null, the resource is being created. No need to modify plan. - if req.State.Raw.IsNull() { - return - } - - var config types.Object - resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) - if resp.Diagnostics.HasError() { - return - } - - authPreference := &apitypes.AuthPreferenceV2{} - resp.Diagnostics.Append(tfschema.CopyAuthPreferenceV2FromTerraform(ctx, config, authPreference)...) - if resp.Diagnostics.HasError() { - return - } - - authPreferenceResource := authPreference - - if err := authPreferenceResource.CheckAndSetDefaults(); err != nil { - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error setting AuthPreference defaults", trace.Wrap(err), "cluster_auth_preference")) - return - } - - authPreference = authPreferenceResource - - const preserveUnknown = true - resp.Diagnostics.Append(tfschema.CopyAuthPreferenceV2ToTerraformPreserveUnknown(ctx, authPreference, &config, preserveUnknown)...) - if resp.Diagnostics.HasError() { - return - } - - var plan types.Object - resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) - if resp.Diagnostics.HasError() { - return - } - plan.Attrs["spec"] = config.Attrs["spec"] - - resp.Diagnostics.Append(resp.Plan.Set(ctx, &plan)...) -} diff --git a/integrations/terraform/provider/internal/resources/auth_preference.go b/integrations/terraform/provider/internal/resources/auth_preference.go new file mode 100644 index 00000000000..ec4518236a2 --- /dev/null +++ b/integrations/terraform/provider/internal/resources/auth_preference.go @@ -0,0 +1,67 @@ +// Teleport +// Copyright (C) 2026 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 . + +package resources + +import ( + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + + "github.com/gravitational/teleport/api/types" + + "github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport" + "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver" + "github.com/gravitational/teleport/integrations/terraform/tfschema" +) + +// authPreferenceID is the static identifier used in the terraform state. +// Note that types.MetaNameClusterAuthPreference is "cluster-auth-preference" +// which can't be used here without causing churn. +const authPreferenceID = "auth_preference" + +// NewAuthPreferenceDataSourceType returns the cluster auth preference data source type. +func NewAuthPreferenceDataSourceType() tfdriver.DataSourceType[types.AuthPreferenceV2, tfdriver.SingletonIdentifier] { + return tfdriver.DataSourceType[types.AuthPreferenceV2, tfdriver.SingletonIdentifier]{ + NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[types.AuthPreferenceV2, tfdriver.SingletonIdentifier] { + return teleport.NewAuthPreferenceClient(clientFromProvider(p)) + }, + Kind: types.KindClusterAuthPreference, + Codec: tfdriver.DataSourceCodecFuncs[types.AuthPreferenceV2]{ + SchemaFunc: tfschema.GenSchemaAuthPreferenceV2, + ToStateFunc: tfschema.CopyAuthPreferenceV2ToTerraform, + }, + Identifier: tfdriver.SingletonIdentifierFromName(authPreferenceID), + } +} + +// NewAuthPreferenceResourceType returns the cluster auth preference resource type. +func NewAuthPreferenceResourceType() tfdriver.ResourceType[types.AuthPreferenceV2, tfdriver.SingletonIdentifier] { + return tfdriver.ResourceType[types.AuthPreferenceV2, tfdriver.SingletonIdentifier]{ + NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[types.AuthPreferenceV2, tfdriver.SingletonIdentifier] { + return teleport.NewAuthPreferenceClient(clientFromProvider(p)) + }, + Kind: types.KindClusterAuthPreference, + Codec: tfdriver.ResourceCodecFuncs[types.AuthPreferenceV2]{ + SchemaFunc: tfschema.GenSchemaAuthPreferenceV2, + ToStateFunc: tfschema.CopyAuthPreferenceV2ToTerraform, + FromPlanFunc: tfschema.CopyAuthPreferenceV2FromTerraform, + }, + Normalizer: tfdriver.CheckAndSetDefaults[types.AuthPreferenceV2](), + Identifier: tfdriver.SingletonIdentifierPolicy[types.AuthPreferenceV2](authPreferenceID), + ResourceRevision: func(st *types.AuthPreferenceV2) string { + return st.GetMetadata().Revision + }, + } +} diff --git a/integrations/terraform/provider/internal/teleport/auth_preference.go b/integrations/terraform/provider/internal/teleport/auth_preference.go new file mode 100644 index 00000000000..daef400cde5 --- /dev/null +++ b/integrations/terraform/provider/internal/teleport/auth_preference.go @@ -0,0 +1,72 @@ +// Teleport +// Copyright (C) 2026 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 . + +package teleport + +import ( + "context" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/client" + clusterconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/clusterconfig/v1" + "github.com/gravitational/teleport/api/types" + + "github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver" +) + +// NewAuthPreferenceClient returns a Teleport client to interact with the cluster auth preference. +func NewAuthPreferenceClient(c *client.Client) AuthPreferenceClient { + return AuthPreferenceClient{client: c} +} + +// AuthPreferenceClient manages the Teleport cluster auth preference.. +type AuthPreferenceClient struct { + client *client.Client +} + +// Get reads a the Teleport clust auth preference. +func (r AuthPreferenceClient) Get(ctx context.Context, _ tfdriver.SingletonIdentifier) (*types.AuthPreferenceV2, error) { + pref, err := r.client.ClusterConfigClient().GetAuthPreference(ctx, &clusterconfigv1.GetAuthPreferenceRequest{}) + if err != nil { + return nil, trace.Wrap(err) + } + + return pref, nil +} + +// Create creates the Teleport cluster auth preference. +func (r AuthPreferenceClient) Create(ctx context.Context, pref *types.AuthPreferenceV2) error { + if _, err := r.client.UpsertAuthPreference(ctx, pref); err != nil { + return trace.Wrap(err) + } + + return nil +} + +// Upsert updates the Teleport cluster auth preference. +func (r AuthPreferenceClient) Upsert(ctx context.Context, pref *types.AuthPreferenceV2) error { + if _, err := r.client.UpsertAuthPreference(ctx, pref); err != nil { + return trace.Wrap(err) + } + + return nil +} + +// Delete resets the Teleport cluster auth preference. +func (r AuthPreferenceClient) Delete(ctx context.Context, _ tfdriver.SingletonIdentifier) error { + return trace.Wrap(r.client.ResetAuthPreference(ctx)) +} diff --git a/integrations/terraform/provider/internal/tfdriver/resource.go b/integrations/terraform/provider/internal/tfdriver/resource.go index 937b6e2453d..b3f74883854 100644 --- a/integrations/terraform/provider/internal/tfdriver/resource.go +++ b/integrations/terraform/provider/internal/tfdriver/resource.go @@ -130,19 +130,21 @@ func (r Resource[T, I]) Create(ctx context.Context, req tfsdk.CreateResourceRequ } id := r.resource.Identifier.FromResource(val) - _, err := r.resourceClient.Get(ctx, id) - if !trace.IsNotFound(err) { - if err == nil { - resp.Diagnostics.Append( - tfdiag.DiagFromErr( - fmt.Sprintf("%q exists in Teleport", r.resource.Kind), - trace.Errorf("%[1]s exists in Teleport. Either remove it (tctl rm %[1]s/%[2]v)"+ - " or import it to the existing state (terraform import teleport_%[1]s.%[2]v %[2]v)", r.resource.Kind, id), - ), - ) - return - } + _, singletonResource := any(id).(SingletonIdentifier) + resourceBefore, err := r.resourceClient.Get(ctx, id) + switch { + case trace.IsNotFound(err): + case err == nil && !singletonResource: + resp.Diagnostics.Append( + tfdiag.DiagFromErr( + fmt.Sprintf("%q exists in Teleport", r.resource.Kind), + trace.Errorf("%[1]s exists in Teleport. Either remove it (tctl rm %[1]s/%[2]v)"+ + " or import it to the existing state (terraform import teleport_%[1]s.%[2]v %[2]v)", r.resource.Kind, id), + ), + ) + return + case err != nil: resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr(fmt.Sprintf("Error reading %q", r.resource.Kind), trace.Wrap(err), r.resource.Kind)) return } @@ -159,42 +161,41 @@ func (r Resource[T, I]) Create(ctx context.Context, req tfsdk.CreateResourceRequ for tries := 1; ; tries++ { retrieved, err := r.resourceClient.Get(ctx, id) - if trace.IsNotFound(err) { - if tries >= r.runtime.MaxRetries() { - diagMessage := fmt.Sprintf("Error reading %q (tried %d times) - state outdated, please import resource", r.resource.Kind, tries) - resp.Diagnostics.AddError(diagMessage, r.resource.Kind) - return - } - - select { - case <-ctx.Done(): - resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr(fmt.Sprintf("Error reading %q", r.resource.Kind), trace.Wrap(ctx.Err()), r.resource.Kind)) - return - case <-retry.After(): - } - - continue - } - if err != nil { + switch { + case trace.IsNotFound(err): + case err != nil: resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr(fmt.Sprintf("Error reading %q", r.resource.Kind), trace.Wrap(err), r.resource.Kind)) return - } + case singletonResource && + resourceBefore != nil && + r.resource.ResourceRevision(resourceBefore) == r.resource.ResourceRevision(retrieved): + default: + diags = r.resource.Codec.ToState(ctx, retrieved, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + plan.Attrs["id"] = types.String{Value: id.String()} + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) - diags = r.resource.Codec.ToState(ctx, retrieved, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { return } - plan.Attrs["id"] = types.String{Value: id.String()} - - diags = resp.State.Set(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { + if tries >= r.runtime.MaxRetries() { + diagMessage := fmt.Sprintf("Error reading %q (tried %d times) - state outdated, please import resource", r.resource.Kind, tries) + resp.Diagnostics.AddError(diagMessage, r.resource.Kind) return } - return + select { + case <-ctx.Done(): + resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr(fmt.Sprintf("Error reading %q", r.resource.Kind), trace.Wrap(ctx.Err()), r.resource.Kind)) + return + case <-retry.After(): + } } } diff --git a/integrations/terraform/provider/provider.go b/integrations/terraform/provider/provider.go index 0481b12beae..317d8e3f1bf 100644 --- a/integrations/terraform/provider/provider.go +++ b/integrations/terraform/provider/provider.go @@ -567,6 +567,7 @@ func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp "teleport_access_list": resources.NewAccessListResourceType(), "teleport_access_list_member": resources.NewAccessListMemberResourceType(), "teleport_app": resources.NewAppResourceType(), + "teleport_auth_preference": resources.NewAuthPreferenceResourceType(), "teleport_kube_cluster": resources.NewKubernetesClusterResourceType(), "teleport_saml_connector": resources.NewSAMLConnectorResourceType(), "teleport_scoped_role": resources.NewScopedRoleResourceType(), @@ -587,6 +588,7 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc "teleport_access_list": resources.NewAccessListDataSourceType(), "teleport_access_list_member": resources.NewAccessListMemberDataSourceType(), "teleport_app": resources.NewAppDataSourceType(), + "teleport_auth_preference": resources.NewAuthPreferenceDataSourceType(), "teleport_kube_cluster": resources.NewKubernetesClusterDataSourceType(), "teleport_saml_connector": resources.NewSAMLConnectorDataSourceType(), "teleport_scoped_role": resources.NewScopedRoleDataSourceType(), diff --git a/integrations/terraform/testlib/auth_preference_test.go b/integrations/terraform/testlib/auth_preference_test.go index 206456cab26..e02ae64d49d 100644 --- a/integrations/terraform/testlib/auth_preference_test.go +++ b/integrations/terraform/testlib/auth_preference_test.go @@ -17,7 +17,6 @@ limitations under the License. package testlib import ( - "context" "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -80,12 +79,8 @@ func (s *TerraformSuiteOSS) TestAuthPreference() { } func (s *TerraformSuiteOSS) TestImportAuthPreference() { - ctx, cancel := context.WithCancel(context.Background()) - s.T().Cleanup(cancel) - r := "teleport_auth_preference" - id := "test_import" - name := r + "." + id + name := r + "." + "auth_preference" authPreference := &types.AuthPreferenceV2{ Metadata: types.Metadata{}, @@ -96,14 +91,14 @@ func (s *TerraformSuiteOSS) TestImportAuthPreference() { err := authPreference.CheckAndSetDefaults() require.NoError(s.T(), err) - authPreferencesBefore, err := s.client.GetAuthPreference(ctx) + authPreferencesBefore, err := s.client.GetAuthPreference(s.T().Context()) require.NoError(s.T(), err) - _, err = s.client.ClusterConfigClient().UpsertAuthPreference(ctx, &clusterconfigv1.UpsertAuthPreferenceRequest{AuthPreference: authPreference}) + _, err = s.client.ClusterConfigClient().UpsertAuthPreference(s.T().Context(), &clusterconfigv1.UpsertAuthPreferenceRequest{AuthPreference: authPreference}) require.NoError(s.T(), err) require.Eventually(s.T(), func() bool { - authPreferencesCurrent, err := s.client.GetAuthPreference(ctx) + authPreferencesCurrent, err := s.client.GetAuthPreference(s.T().Context()) require.NoError(s.T(), err) return authPreferencesBefore.GetMetadata().Revision != authPreferencesCurrent.GetMetadata().Revision @@ -114,10 +109,10 @@ func (s *TerraformSuiteOSS) TestImportAuthPreference() { IsUnitTest: true, Steps: []resource.TestStep{ { - Config: s.terraformConfig + "\n" + `resource "` + r + `" "` + id + `" { }`, + Config: s.terraformConfig + "\n" + `resource "` + r + `" "auth_preference" { }`, ResourceName: name, ImportState: true, - ImportStateId: id, + ImportStateId: "auth_preference", ImportStateCheck: func(state []*terraform.InstanceState) error { require.Equal(s.T(), "cluster_auth_preference", state[0].Attributes["kind"]) require.Equal(s.T(), "true", state[0].Attributes["spec.disconnect_expired_cert"])