Terraform: convert scoped resources to generic provider (#68866)

The currently supported set of scoped resources: ScopedRole, ScopedToken,
ScopedRoleAssignment, KubernetesCluster, and App have all been migrated
to the new generic terraform provider mechanism. This is required for those
resources to properly provide the name and scope pair which uniquely identifies
a scoped resource.

Note: not all scoped resources currently support being identifier by a the
(name, scope) pair. However, this conversion is a prereq for when they do.
At such a time, those resources should only require changing the identifier
type and plumbing the name and scope into the Get and Delete requests.
This commit is contained in:
rosstimothy
2026-07-27 19:37:20 +00:00
committed by GitHub
parent ae3ede8e1c
commit c97d53fd48
43 changed files with 2010 additions and 1692 deletions
+16 -4
View File
@@ -299,10 +299,16 @@ func (c *Client) GetAccessListMemberV2(ctx context.Context, req *accesslistv1.Ge
// GetStaticAccessListMember returns the specified access_list_member resource. If returns error if
// the target access_list is not of type static.
func (c *Client) GetStaticAccessListMember(ctx context.Context, accessList, memberName string) (*accesslist.AccessListMember, error) {
resp, err := c.grpcClient.GetStaticAccessListMember(ctx, &accesslistv1.GetStaticAccessListMemberRequest{
return c.GetStaticAccessListMemberV2(ctx, accesslistv1.GetStaticAccessListMemberRequest_builder{
AccessList: accessList,
MemberName: memberName,
})
}.Build())
}
// GetStaticAccessListMemberV2 returns the specified access_list_member resource. If returns error if
// the target access_list is not of type static.
func (c *Client) GetStaticAccessListMemberV2(ctx context.Context, req *accesslistv1.GetStaticAccessListMemberRequest) (*accesslist.AccessListMember, error) {
resp, err := c.grpcClient.GetStaticAccessListMember(ctx, req)
if err != nil {
return nil, trace.Wrap(err)
}
@@ -378,10 +384,16 @@ func (c *Client) UpdateAccessListMember(ctx context.Context, member *accesslist.
// DeleteStaticAccessListMember hard deletes the specified access_list_member. It returns error and
// does nothing if the target access_list is not of static type.
func (c *Client) DeleteStaticAccessListMember(ctx context.Context, accessList, memberName string) error {
_, err := c.grpcClient.DeleteStaticAccessListMember(ctx, &accesslistv1.DeleteStaticAccessListMemberRequest{
return c.DeleteStaticAccessListMemberV2(ctx, accesslistv1.DeleteStaticAccessListMemberRequest_builder{
AccessList: accessList,
MemberName: memberName,
})
}.Build())
}
// DeleteStaticAccessListMemberV2 hard deletes the specified access_list_member. It returns error and
// does nothing if the target access_list is not of static type.
func (c *Client) DeleteStaticAccessListMemberV2(ctx context.Context, req *accesslistv1.DeleteStaticAccessListMemberRequest) error {
_, err := c.grpcClient.DeleteStaticAccessListMember(ctx, req)
return trace.Wrap(err)
}
@@ -52,7 +52,6 @@ Optional:
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
- `namespace` (String) namespace is object namespace. The field should be called "namespace" when it returns in Teleport 2.4.
- `revision` (String) revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.
@@ -52,7 +52,6 @@ Optional:
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
- `namespace` (String) namespace is object namespace. The field should be called "namespace" when it returns in Teleport 2.4.
- `revision` (String) revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.
@@ -95,7 +95,6 @@ Optional:
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
- `namespace` (String) namespace is object namespace. The field should be called "namespace" when it returns in Teleport 2.4.
- `revision` (String) revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.
@@ -127,7 +127,6 @@ Optional:
- `expires` (String) expires is a global expiry time header can be set on any resource in the system.
- `labels` (Map of String) labels is a set of labels.
- `namespace` (String) namespace is object namespace. The field should be called "namespace" when it returns in Teleport 2.4.
- `revision` (String) revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.
-155
View File
@@ -356,21 +356,6 @@ var (
HasCheckAndSetDefaults: true,
}
kubernetesCluster = payload{
Name: "KubeCluster",
TypeName: "KubernetesClusterV3",
VarName: "kubeCluster",
GetMethod: "GetKubernetesCluster",
CreateMethod: "CreateKubernetesCluster",
UpdateMethod: "UpdateKubernetesCluster",
DeleteMethod: "DeleteKubernetesCluster",
ID: `kubeCluster.Metadata.Name`,
Kind: "kube_cluster",
HasStaticID: false,
TerraformResourceType: "teleport_kube_cluster",
HasCheckAndSetDefaults: true,
}
lock = payload{
Name: "Lock",
TypeName: "LockV2",
@@ -591,52 +576,6 @@ var (
HasCheckAndSetDefaults: true,
}
accessList = payload{
Name: "AccessList",
TypeName: "AccessList",
VarName: "accessList",
GetMethod: "AccessListClient().GetAccessList",
CreateMethod: "AccessListClient().UpsertAccessList",
UpsertMethodArity: 2,
UpdateMethod: "AccessListClient().UpsertAccessList",
DeleteMethod: "AccessListClient().DeleteAccessList",
ID: "accessList.Header.Metadata.Name",
Kind: "access_list",
HasStaticID: false,
SchemaPackage: "schemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1",
ProtoPackage: "accesslist",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1",
TerraformResourceType: "teleport_access_list",
ConvertPackagePath: "github.com/gravitational/teleport/api/types/accesslist/convert/v1",
HasCheckAndSetDefaults: true,
PropagatedFields: []string{"Spec.Audit.NextAuditDate"},
}
accessListMember = payload{
Name: "Member",
TypeName: "Member",
VarName: "accessListMember",
GetMethod: "AccessListClient().GetStaticAccessListMember",
CreateMethod: "AccessListClient().UpsertStaticAccessListMember",
UpsertMethodArity: 2,
UpdateMethod: "AccessListClient().UpsertStaticAccessListMember",
DeleteMethod: "AccessListClient().DeleteStaticAccessListMember",
IDPrefix: "accessListMember.Spec.AccessList",
ID: "accessListMember.Header.Metadata.Name",
Kind: "access_list_member",
HasStaticID: false,
SchemaPackage: "schemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1",
ProtoPackage: "accesslist",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1",
TerraformResourceType: "teleport_access_list_member",
ConvertPackagePath: "github.com/gravitational/teleport/api/types/accesslist/convert/v1",
ConvertToProtoFunc: "ToMemberProto",
ConvertFromProtoFunc: "FromMemberProto",
HasCheckAndSetDefaults: true,
}
server = payload{
Name: "Server",
TypeName: "ServerV2",
@@ -1055,88 +994,6 @@ var (
ForceSetKind: "apitypes.KindClassifier",
}
scopedRole = payload{
Name: "ScopedRole",
TypeName: "ScopedRole",
VarName: "scopedRole",
GetMethod: "ScopedAccessServiceClient().GetScopedRole",
CreateMethod: "ScopedAccessServiceClient().CreateScopedRole",
UpdateMethod: "ScopedAccessServiceClient().UpsertScopedRole",
UpsertMethodArity: 2,
DeleteMethod: "ScopedAccessServiceClient().DeleteScopedRole",
ID: "scopedRole.Metadata.Name",
Kind: "scoped_role",
HasStaticID: false,
ProtoPackage: "accessv1",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1",
SchemaPackage: "schemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/v1",
TerraformResourceType: "teleport_scoped_role",
IsPlainStruct: true,
ExtraImports: []string{"apitypes \"github.com/gravitational/teleport/lib/scopes/access\""},
ForceSetKind: "apitypes.KindScopedRole",
RequestWrapper: &RequestWrapper{
RequestResourceField: "Role",
GetRequest: "GetScopedRoleRequest",
CreateRequest: "CreateScopedRoleRequest",
UpdateRequest: "UpsertScopedRoleRequest",
DeleteRequest: "DeleteScopedRoleRequest",
},
}
scopedRoleAssignment = payload{
Name: "ScopedRoleAssignment",
TypeName: "ScopedRoleAssignment",
VarName: "scopedRoleAssignment",
GetMethod: "ScopedAccessServiceClient().GetScopedRoleAssignment",
CreateMethod: "ScopedAccessServiceClient().CreateScopedRoleAssignment",
UpdateMethod: "ScopedAccessServiceClient().UpsertScopedRoleAssignment",
UpsertMethodArity: 2,
DeleteMethod: "ScopedAccessServiceClient().DeleteScopedRoleAssignment",
ID: "scopedRoleAssignment.Metadata.Name",
Kind: "scoped_role_assignment",
HasStaticID: false,
ProtoPackage: "accessv1",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1",
SchemaPackage: "assignmentschemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/assignment/v1",
TerraformResourceType: "teleport_scoped_role_assignment",
IsPlainStruct: true,
ExtraImports: []string{"apitypes \"github.com/gravitational/teleport/lib/scopes/access\""},
ForceSetKind: "apitypes.KindScopedRoleAssignment",
RequestWrapper: &RequestWrapper{
RequestResourceField: "Assignment",
GetRequest: "GetScopedRoleAssignmentRequest",
CreateRequest: "CreateScopedRoleAssignmentRequest",
UpdateRequest: "UpsertScopedRoleAssignmentRequest",
DeleteRequest: "DeleteScopedRoleAssignmentRequest",
},
DefaultSubKind: "\"dynamic\"",
}
scopedToken = payload{
Name: "ScopedToken",
TypeName: "ScopedToken",
VarName: "scopedToken",
GetMethod: "GetScopedToken",
CreateMethod: "CreateScopedToken",
UpdateMethod: "UpsertScopedToken",
UpsertMethodArity: 2,
DeleteMethod: "DeleteScopedToken",
ID: "scopedToken.Metadata.Name",
Kind: "scoped_token",
WithSecrets: "true",
HasStaticID: false,
ProtoPackage: "joiningv1",
ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/joining/v1",
SchemaPackage: "schemav1",
SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/joining/v1",
TerraformResourceType: "teleport_scoped_token",
IsPlainStruct: true,
ExtraImports: []string{"apitypes \"github.com/gravitational/teleport/api/types\""},
ForceSetKind: "apitypes.KindScopedToken",
}
workloadCluster = payload{
Name: "WorkloadCluster",
TypeName: "WorkloadCluster",
@@ -1281,8 +1138,6 @@ func genTFSchema() {
generateDataSource(dynamicWindowsDesktop, pluralDataSource)
generateResource(githubConnector, pluralResource)
generateDataSource(githubConnector, pluralDataSource)
generateResource(kubernetesCluster, pluralResource)
generateDataSource(kubernetesCluster, pluralDataSource)
generateResource(lock, pluralResource)
generateDataSource(lock, pluralDataSource)
generateResource(oidcConnector, pluralResource)
@@ -1309,10 +1164,6 @@ func genTFSchema() {
generateDataSource(deviceTrust, pluralDataSource)
generateResource(oktaImportRule, pluralResource)
generateDataSource(oktaImportRule, pluralDataSource)
generateResource(accessList, pluralResource)
generateDataSource(accessList, pluralDataSource)
generateResource(accessListMember, pluralResource)
generateDataSource(accessListMember, pluralDataSource)
generateResource(server, pluralResource)
generateDataSource(server, pluralDataSource)
generateResource(installer, pluralResource)
@@ -1347,12 +1198,6 @@ func genTFSchema() {
generateDataSource(classifier, pluralDataSource)
generateResource(retrievalModel, singularResource)
generateDataSource(retrievalModel, singularDataSource)
generateResource(scopedRole, pluralResource)
generateDataSource(scopedRole, pluralDataSource)
generateResource(scopedRoleAssignment, pluralResource)
generateDataSource(scopedRoleAssignment, pluralDataSource)
generateResource(scopedToken, pluralResource)
generateDataSource(scopedToken, pluralDataSource)
generateResource(workloadCluster, pluralResource)
generateDataSource(workloadCluster, pluralDataSource)
generateResource(databaseObjectImportRule, pluralResource)
@@ -42,11 +42,13 @@ injected_fields:
exclude_fields:
# Metadata (we id resources by name on our side)
- "AccessList.header.metadata.id"
- "AccessList.header.metadata.revision"
# Read only field
- "AccessList.spec.owners.ineligible_status"
- "AccessList.status"
# Metadata (we id resources by name on our side)
- "Member.header.metadata.id"
- "Member.header.metadata.revision"
# Read only field
- "Member.spec.ineligible_status"
- "Member.status"
@@ -56,12 +58,10 @@ computed_fields:
- "AccessList.header.kind"
- "AccessList.header.metadata.expires"
- "AccessList.header.metadata.namespace"
- "AccessList.header.metadata.revision"
- "AccessList.spec.audit.next_audit_date"
- "Member.header.kind"
- "Member.header.metadata.expires"
- "Member.header.metadata.namespace"
- "Member.header.metadata.revision"
- "Member.spec.name"
- "Member.spec.joined"
- "Member.spec.added_by"
@@ -79,6 +79,12 @@ plan_modifiers:
# Force to recreate resource if it's ID changes
Metadata.name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
# Scope is part of the access_list ID.
AccessList.scope:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
# Scope is part of the access_list_member ID.
Member.scope:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
# spec.access_list is a part of access_list_member ID. In the backend member's ID is
# constructed like:
# "{access_list_member.spec.access_list}/{access_list_member.header.metadata.name}"
@@ -513,6 +513,11 @@ plan_modifiers:
# Force to recreate resource if it's name changes
Metadata.Name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
# Scope is part of the scoped resource identity and must not be updated in-place.
AppV3.scope:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
KubernetesClusterV3.scope:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
ServerV2.Metadata.Name:
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()"
- "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()"
@@ -1,97 +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"
convert "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag"
)
// dataSourceTeleportAccessListType is the data source metadata type
type dataSourceTeleportAccessListType struct{}
// dataSourceTeleportAccessList is the resource
type dataSourceTeleportAccessList struct {
p Provider
}
// GetSchema returns the data source schema
func (r dataSourceTeleportAccessListType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return schemav1.GenSchemaAccessList(ctx)
}
// NewDataSource creates the empty data source
func (r dataSourceTeleportAccessListType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) {
return dataSourceTeleportAccessList{
p: p.(Provider),
}, nil
}
// Read reads teleport AccessList
func (r dataSourceTeleportAccessList) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) {
var id types.String
diags := req.Config.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListI, err := r.p.Client().AccessListClient().GetAccessList(ctx, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(err), "access_list"))
return
}
var state types.Object
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
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() {
state.Attrs["id"] = id
}
accessList := convert.ToProto(accessListI)
diags = schemav1.CopyAccessListToTerraform(ctx, accessList, &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
}
}
@@ -1,103 +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"
convert "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag"
)
// dataSourceTeleportMemberType is the data source metadata type
type dataSourceTeleportMemberType struct{}
// dataSourceTeleportMember is the resource
type dataSourceTeleportMember struct {
p Provider
}
// GetSchema returns the data source schema
func (r dataSourceTeleportMemberType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return schemav1.GenSchemaMember(ctx)
}
// NewDataSource creates the empty data source
func (r dataSourceTeleportMemberType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) {
return dataSourceTeleportMember{
p: p.(Provider),
}, nil
}
// Read reads teleport Member
func (r dataSourceTeleportMember) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) {
var idPrefix types.String
diags := req.Config.GetAttribute(ctx, path.Root("spec").AtName("access_list"), &idPrefix)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var id types.String
diags = req.Config.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListMemberI, err := r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix.Value, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(err), "access_list_member"))
return
}
var state types.Object
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
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() {
state.Attrs["id"] = types.String{Value: formatID(idPrefix.Value, id.Value)}
}
accessListMember := convert.ToMemberProto(accessListMemberI)
diags = schemav1.CopyMemberToTerraform(ctx, accessListMember, &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
}
}
@@ -1,97 +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/path"
"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"
)
// dataSourceTeleportKubeClusterType is the data source metadata type
type dataSourceTeleportKubeClusterType struct{}
// dataSourceTeleportKubeCluster is the resource
type dataSourceTeleportKubeCluster struct {
p Provider
}
// GetSchema returns the data source schema
func (r dataSourceTeleportKubeClusterType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfschema.GenSchemaKubernetesClusterV3(ctx)
}
// NewDataSource creates the empty data source
func (r dataSourceTeleportKubeClusterType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) {
return dataSourceTeleportKubeCluster{
p: p.(Provider),
}, nil
}
// Read reads teleport KubeCluster
func (r dataSourceTeleportKubeCluster) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) {
var id types.String
diags := req.Config.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
kubeClusterI, err := r.p.Client().GetKubernetesCluster(ctx, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
var state types.Object
resp.Diagnostics.Append(req.Config.Get(ctx, &state)...)
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() {
state.Attrs["id"] = id
}
kubeCluster := kubeClusterI.(*apitypes.KubernetesClusterV3)
diags = tfschema.CopyKubernetesClusterV3ToTerraform(ctx, kubeCluster, &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
}
}
@@ -1,45 +0,0 @@
/*
Copyright 2025 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 (
"strings"
"github.com/gravitational/trace"
)
// formatID takes the prefix and the name of the resource and converts it to a string ID.
func formatID(prefix, name string) string {
return prefix + "/" + name
}
// parseID takes the ID generated by [formatID] and splits it back to the prefix and resource name.
func parseID(id string) (prefix, name string, err error) {
split := strings.Split(id, "/")
if len(split) != 2 {
return "", "", trace.BadParameter("expected id %q to have a single %q separator", id, "/")
}
prefix = split[0]
name = split[1]
if prefix == "" {
return "", "", trace.BadParameter("expected id %q prefix to be non-empty", id)
}
if name == "" {
return "", "", trace.BadParameter("expected id %q name to be non-empty", id)
}
return prefix, name, nil
}
@@ -29,7 +29,6 @@ func DataSourceTypes() map[string]tfsdk.DataSourceType {
"teleport_discovery_config": dataSourceTeleportDiscoveryConfigType{},
"teleport_dynamic_windows_desktop": dataSourceTeleportDynamicWindowsDesktopType{},
"teleport_github_connector": dataSourceTeleportGithubConnectorType{},
"teleport_kube_cluster": dataSourceTeleportKubeClusterType{},
"teleport_lock": dataSourceTeleportLockType{},
"teleport_provision_token": dataSourceTeleportProvisionTokenType{},
"teleport_oidc_connector": dataSourceTeleportOIDCConnectorType{},
@@ -43,8 +42,6 @@ func DataSourceTypes() map[string]tfsdk.DataSourceType {
"teleport_login_rule": dataSourceTeleportLoginRuleType{},
"teleport_trusted_device": dataSourceTeleportDeviceV1Type{},
"teleport_okta_import_rule": dataSourceTeleportOktaImportRuleType{},
"teleport_access_list": dataSourceTeleportAccessListType{},
"teleport_access_list_member": dataSourceTeleportMemberType{},
"teleport_installer": dataSourceTeleportInstallerType{},
"teleport_access_monitoring_rule": dataSourceTeleportAccessMonitoringRuleType{},
"teleport_static_host_user": dataSourceTeleportStaticHostUserType{},
@@ -54,8 +51,6 @@ func DataSourceTypes() map[string]tfsdk.DataSourceType {
"teleport_health_check_config": dataSourceTeleportHealthCheckConfigType{},
"teleport_vnet_config": dataSourceTeleportVnetConfigType{},
"teleport_integration": dataSourceTeleportIntegrationType{},
"teleport_scoped_role": dataSourceTeleportScopedRoleType{},
"teleport_scoped_role_assignment": dataSourceTeleportScopedRoleAssignmentType{},
"teleport_db_object_import_rule": dataSourceTeleportDatabaseObjectImportRuleType{},
"teleport_classifier": dataSourceTeleportClassifierType{},
// TODO(bl-nero): Add teleport_inference_* data sources after data sources
@@ -81,7 +76,6 @@ func ResourceTypes() map[string]tfsdk.ResourceType {
"teleport_discovery_config": resourceTeleportDiscoveryConfigType{},
"teleport_dynamic_windows_desktop": resourceTeleportDynamicWindowsDesktopType{},
"teleport_github_connector": resourceTeleportGithubConnectorType{},
"teleport_kube_cluster": resourceTeleportKubeClusterType{},
"teleport_lock": resourceTeleportLockType{},
"teleport_provision_token": resourceTeleportProvisionTokenType{},
"teleport_oidc_connector": resourceTeleportOIDCConnectorType{},
@@ -96,8 +90,6 @@ func ResourceTypes() map[string]tfsdk.ResourceType {
"teleport_login_rule": resourceTeleportLoginRuleType{},
"teleport_trusted_device": resourceTeleportDeviceV1Type{},
"teleport_okta_import_rule": resourceTeleportOktaImportRuleType{},
"teleport_access_list": resourceTeleportAccessListType{},
"teleport_access_list_member": resourceTeleportMemberType{},
"teleport_server": resourceTeleportServerType{},
"teleport_installer": resourceTeleportInstallerType{},
"teleport_access_monitoring_rule": resourceTeleportAccessMonitoringRuleType{},
@@ -114,8 +106,6 @@ func ResourceTypes() map[string]tfsdk.ResourceType {
"teleport_classifier": resourceTeleportClassifierType{},
"teleport_retrieval_model": resourceTeleportRetrievalModelType{},
"teleport_workload_cluster": resourceTeleportWorkloadClusterType{},
"teleport_scoped_role": resourceTeleportScopedRoleType{},
"teleport_scoped_role_assignment": resourceTeleportScopedRoleAssignmentType{},
"teleport_db_object_import_rule": resourceTeleportDatabaseObjectImportRuleType{},
"teleport_client_ip_restriction": resourceTeleportClientIPRestrictionType{},
}
@@ -1,343 +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"
accesslist "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
convert "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag"
)
// resourceTeleportAccessListType is the resource metadata type
type resourceTeleportAccessListType struct{}
// resourceTeleportAccessList is the resource
type resourceTeleportAccessList struct {
p Provider
}
// GetSchema returns the resource schema
func (r resourceTeleportAccessListType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return schemav1.GenSchemaAccessList(ctx)
}
// NewResource creates the empty resource
func (r resourceTeleportAccessListType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
return resourceTeleportAccessList{
p: p.(Provider),
}, nil
}
// Create creates the AccessList
func (r resourceTeleportAccessList) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
var err error
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
}
accessList := &accesslist.AccessList{}
diags = schemav1.CopyAccessListFromTerraform(ctx, plan, accessList)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListResource, err := convert.FromProto(accessList)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Errorf("Can not convert %T to AccessList: %s", accessListResource, err), "access_list"))
return
}
err = accessListResource.CheckAndSetDefaults()
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error setting AccessList defaults", trace.Wrap(err), "access_list"))
return
}
id := accessListResource.Metadata.Name
_, err = r.p.Client().AccessListClient().GetAccessList(ctx, id)
if !trace.IsNotFound(err) {
if err == nil {
existErr := fmt.Sprintf("AccessList exists in Teleport. Either remove it (tctl rm access_list/%v)"+
" or import it to the existing state (terraform import teleport_access_list.%v %v)", id, id, id)
resp.Diagnostics.Append(tfdiag.DiagFromErr("AccessList exists in Teleport", trace.Errorf(existErr)))
return
}
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(err), "access_list"))
return
}
_, err = r.p.Client().AccessListClient().UpsertAccessList(ctx, accessListResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error creating AccessList", trace.Wrap(err), "access_list"))
return
}
var accessListI = accessListResource
// Try getting the resource until it exists.
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
accessListI, err = r.p.Client().AccessListClient().GetAccessList(ctx, id)
if trace.IsNotFound(err) {
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(ctx.Err()), "access_list"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading AccessList (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "access_list")
return
}
continue
}
break
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(err), "access_list"))
return
}
accessListResource = accessListI
accessList = convert.ToProto(accessListResource)
diags = schemav1.CopyAccessListToTerraform(ctx, accessList, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
plan.Attrs["id"] = types.String{Value: accessList.Header.Metadata.Name}
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read reads teleport AccessList
func (r resourceTeleportAccessList) 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
}
var id types.String
diags = req.State.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListI, err := r.p.Client().AccessListClient().GetAccessList(ctx, id.Value)
if trace.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(err), "access_list"))
return
}
accessList := convert.ToProto(accessListI)
diags = schemav1.CopyAccessListToTerraform(ctx, accessList, &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 AccessList
func (r resourceTeleportAccessList) 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
}
accessList := &accesslist.AccessList{}
diags = schemav1.CopyAccessListFromTerraform(ctx, plan, accessList)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListResource, err := convert.FromProto(accessList)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Errorf("Can not convert %T to AccessList: %s", accessListResource, err), "access_list"))
return
}
if err := accessListResource.CheckAndSetDefaults(); err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating AccessList", err, "access_list"))
return
}
name := accessListResource.Metadata.Name
accessListBefore, err := r.p.Client().AccessListClient().GetAccessList(ctx, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", err, "access_list"))
return
}
accessListResource.Spec.Audit.NextAuditDate = accessListBefore.Spec.Audit.NextAuditDate
_, err = r.p.Client().AccessListClient().UpsertAccessList(ctx, accessListResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating AccessList", err, "access_list"))
return
}
var accessListI = accessListResource
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
accessListI, err = r.p.Client().AccessListClient().GetAccessList(ctx, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", err, "access_list"))
return
}
if accessListBefore.GetMetadata().Revision != accessListI.GetMetadata().Revision || false {
break
}
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(ctx.Err()), "access_list"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading AccessList (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "access_list")
return
}
}
accessListResource = accessListI
diags = schemav1.CopyAccessListToTerraform(ctx, accessList, &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 AccessList
func (r resourceTeleportAccessList) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) {
var id types.String
diags := req.State.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.p.Client().AccessListClient().DeleteAccessList(ctx, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error deleting AccessList", trace.Wrap(err), "access_list"))
return
}
resp.State.RemoveResource(ctx)
}
// ImportState imports AccessList state
func (r resourceTeleportAccessList) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
accessList, err := r.p.Client().AccessListClient().GetAccessList(ctx, req.ID)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading AccessList", trace.Wrap(err), "access_list"))
return
}
accessListResource := convert.ToProto(accessList)
var state types.Object
diags := resp.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = schemav1.CopyAccessListToTerraform(ctx, accessListResource, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
id := accessList.Metadata.Name
state.Attrs["id"] = types.String{Value: id}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
@@ -1,364 +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"
accesslist "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
convert "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdiag"
)
// resourceTeleportMemberType is the resource metadata type
type resourceTeleportMemberType struct{}
// resourceTeleportMember is the resource
type resourceTeleportMember struct {
p Provider
}
// GetSchema returns the resource schema
func (r resourceTeleportMemberType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return schemav1.GenSchemaMember(ctx)
}
// NewResource creates the empty resource
func (r resourceTeleportMemberType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
return resourceTeleportMember{
p: p.(Provider),
}, nil
}
// Create creates the Member
func (r resourceTeleportMember) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
var err error
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
}
accessListMember := &accesslist.Member{}
diags = schemav1.CopyMemberFromTerraform(ctx, plan, accessListMember)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListMemberResource, err := convert.FromMemberProto(accessListMember)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Errorf("Can not convert %T to Member: %s", accessListMemberResource, err), "access_list_member"))
return
}
err = accessListMemberResource.CheckAndSetDefaults()
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error setting Member defaults", trace.Wrap(err), "access_list_member"))
return
}
idPrefix := accessListMemberResource.Spec.AccessList
id := accessListMemberResource.Metadata.Name
_, err = r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix, id)
if !trace.IsNotFound(err) {
if err == nil {
id := formatID(idPrefix, id)
existErr := fmt.Sprintf("Member exists in Teleport. Either remove it (tctl rm access_list_member/%v)"+
" or import it to the existing state (terraform import teleport_access_list_member.%v %v)", id, id, id)
resp.Diagnostics.Append(tfdiag.DiagFromErr("Member exists in Teleport", trace.Errorf(existErr)))
return
}
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(err), "access_list_member"))
return
}
_, err = r.p.Client().AccessListClient().UpsertStaticAccessListMember(ctx, accessListMemberResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error creating Member", trace.Wrap(err), "access_list_member"))
return
}
var accessListMemberI = accessListMemberResource
// Try getting the resource until it exists.
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
accessListMemberI, err = r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix, id)
if trace.IsNotFound(err) {
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(ctx.Err()), "access_list_member"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading Member (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "access_list_member")
return
}
continue
}
break
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(err), "access_list_member"))
return
}
accessListMemberResource = accessListMemberI
accessListMember = convert.ToMemberProto(accessListMemberResource)
diags = schemav1.CopyMemberToTerraform(ctx, accessListMember, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
plan.Attrs["id"] = types.String{Value: formatID(idPrefix, accessListMember.Header.Metadata.Name)}
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read reads teleport Member
func (r resourceTeleportMember) 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
}
var idPrefix types.String
diags = req.State.GetAttribute(ctx, path.Root("spec").AtName("access_list"), &idPrefix)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var id types.String
diags = req.State.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListMemberI, err := r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix.Value, id.Value)
if trace.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(err), "access_list_member"))
return
}
accessListMember := convert.ToMemberProto(accessListMemberI)
diags = schemav1.CopyMemberToTerraform(ctx, accessListMember, &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 Member
func (r resourceTeleportMember) 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
}
accessListMember := &accesslist.Member{}
diags = schemav1.CopyMemberFromTerraform(ctx, plan, accessListMember)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
accessListMemberResource, err := convert.FromMemberProto(accessListMember)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Errorf("Can not convert %T to Member: %s", accessListMemberResource, err), "access_list_member"))
return
}
if err := accessListMemberResource.CheckAndSetDefaults(); err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating Member", err, "access_list_member"))
return
}
idPrefix := accessListMemberResource.Spec.AccessList
name := accessListMemberResource.Metadata.Name
accessListMemberBefore, err := r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", err, "access_list_member"))
return
}
_, err = r.p.Client().AccessListClient().UpsertStaticAccessListMember(ctx, accessListMemberResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating Member", err, "access_list_member"))
return
}
var accessListMemberI = accessListMemberResource
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
accessListMemberI, err = r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", err, "access_list_member"))
return
}
if accessListMemberBefore.GetMetadata().Revision != accessListMemberI.GetMetadata().Revision || false {
break
}
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(ctx.Err()), "access_list_member"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading Member (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "access_list_member")
return
}
}
accessListMemberResource = accessListMemberI
diags = schemav1.CopyMemberToTerraform(ctx, accessListMember, &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 Member
func (r resourceTeleportMember) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) {
var idPrefix types.String
diags := req.State.GetAttribute(ctx, path.Root("spec").AtName("access_list"), &idPrefix)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var id types.String
diags = req.State.GetAttribute(ctx, path.Root("header").AtName("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.p.Client().AccessListClient().DeleteStaticAccessListMember(ctx, idPrefix.Value, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error deleting Member", trace.Wrap(err), "access_list_member"))
return
}
resp.State.RemoveResource(ctx)
}
// ImportState imports Member state
func (r resourceTeleportMember) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
idPrefix, name, err := parseID(req.ID)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error parsing Member ID", trace.Wrap(err), "access_list_member"))
return
}
accessListMember, err := r.p.Client().AccessListClient().GetStaticAccessListMember(ctx, idPrefix, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading Member", trace.Wrap(err), "access_list_member"))
return
}
accessListMemberResource := convert.ToMemberProto(accessListMember)
var state types.Object
diags := resp.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = schemav1.CopyMemberToTerraform(ctx, accessListMemberResource, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
id := req.ID
state.Attrs["id"] = types.String{Value: id}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
@@ -1,346 +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/path"
"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"
)
// resourceTeleportKubeClusterType is the resource metadata type
type resourceTeleportKubeClusterType struct{}
// resourceTeleportKubeCluster is the resource
type resourceTeleportKubeCluster struct {
p Provider
}
// GetSchema returns the resource schema
func (r resourceTeleportKubeClusterType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) {
return tfschema.GenSchemaKubernetesClusterV3(ctx)
}
// NewResource creates the empty resource
func (r resourceTeleportKubeClusterType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) {
return resourceTeleportKubeCluster{
p: p.(Provider),
}, nil
}
// Create creates the KubeCluster
func (r resourceTeleportKubeCluster) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) {
var err error
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
}
kubeCluster := &apitypes.KubernetesClusterV3{}
diags = tfschema.CopyKubernetesClusterV3FromTerraform(ctx, plan, kubeCluster)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
kubeClusterResource := kubeCluster
err = kubeClusterResource.CheckAndSetDefaults()
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error setting KubeCluster defaults", trace.Wrap(err), "kube_cluster"))
return
}
id := kubeClusterResource.Metadata.Name
_, err = r.p.Client().GetKubernetesCluster(ctx, id)
if !trace.IsNotFound(err) {
if err == nil {
existErr := fmt.Sprintf("KubeCluster exists in Teleport. Either remove it (tctl rm kube_cluster/%v)"+
" or import it to the existing state (terraform import teleport_kube_cluster.%v %v)", id, id, id)
resp.Diagnostics.Append(tfdiag.DiagFromErr("KubeCluster exists in Teleport", trace.Errorf(existErr)))
return
}
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
err = r.p.Client().CreateKubernetesCluster(ctx, kubeClusterResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error creating KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
// Not really an inferface, just using the same name for easier templating.
var kubeClusterI apitypes.KubeCluster
// Try getting the resource until it exists.
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
kubeClusterI, err = r.p.Client().GetKubernetesCluster(ctx, id)
if trace.IsNotFound(err) {
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(ctx.Err()), "kube_cluster"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading KubeCluster (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "kube_cluster")
return
}
continue
}
break
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
kubeClusterResource, ok := kubeClusterI.(*apitypes.KubernetesClusterV3)
if !ok {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Errorf("Can not convert %T to KubernetesClusterV3", kubeClusterI), "kube_cluster"))
return
}
kubeCluster = kubeClusterResource
diags = tfschema.CopyKubernetesClusterV3ToTerraform(ctx, kubeCluster, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
plan.Attrs["id"] = types.String{Value: kubeCluster.Metadata.Name}
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read reads teleport KubeCluster
func (r resourceTeleportKubeCluster) 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
}
var id types.String
diags = req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
kubeClusterI, err := r.p.Client().GetKubernetesCluster(ctx, id.Value)
if trace.IsNotFound(err) {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
kubeCluster := kubeClusterI.(*apitypes.KubernetesClusterV3)
diags = tfschema.CopyKubernetesClusterV3ToTerraform(ctx, kubeCluster, &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 KubeCluster
func (r resourceTeleportKubeCluster) 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
}
kubeCluster := &apitypes.KubernetesClusterV3{}
diags = tfschema.CopyKubernetesClusterV3FromTerraform(ctx, plan, kubeCluster)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
kubeClusterResource := kubeCluster
if err := kubeClusterResource.CheckAndSetDefaults(); err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating KubeCluster", err, "kube_cluster"))
return
}
name := kubeClusterResource.Metadata.Name
kubeClusterBefore, err := r.p.Client().GetKubernetesCluster(ctx, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", err, "kube_cluster"))
return
}
err = r.p.Client().UpdateKubernetesCluster(ctx, kubeClusterResource)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error updating KubeCluster", err, "kube_cluster"))
return
}
// Not really an inferface, just using the same name for easier templating.
var kubeClusterI apitypes.KubeCluster
tries := 0
retry, err := r.p.Retry()
if err != nil {
return
}
for {
tries = tries + 1
kubeClusterI, err = r.p.Client().GetKubernetesCluster(ctx, name)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", err, "kube_cluster"))
return
}
if kubeClusterBefore.GetMetadata().Revision != kubeClusterI.GetMetadata().Revision || false {
break
}
select {
case <-ctx.Done():
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(ctx.Err()), "kube_cluster"))
return
case <-retry.After():
}
if tries >= r.p.MaxRetries() {
diagMessage := fmt.Sprintf("Error reading KubeCluster (tried %d times) - state outdated, please import resource", tries)
resp.Diagnostics.AddError(diagMessage, "kube_cluster")
return
}
}
kubeClusterResource, ok := kubeClusterI.(*apitypes.KubernetesClusterV3)
if !ok {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Errorf("Can not convert %T to KubernetesClusterV3", kubeClusterI), "kube_cluster"))
return
}
diags = tfschema.CopyKubernetesClusterV3ToTerraform(ctx, kubeCluster, &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 KubeCluster
func (r resourceTeleportKubeCluster) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) {
var id types.String
diags := req.State.GetAttribute(ctx, path.Root("metadata").AtName("name"), &id)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
err := r.p.Client().DeleteKubernetesCluster(ctx, id.Value)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error deleting KubernetesClusterV3", trace.Wrap(err), "kube_cluster"))
return
}
resp.State.RemoveResource(ctx)
}
// ImportState imports KubeCluster state
func (r resourceTeleportKubeCluster) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) {
kubeCluster, err := r.p.Client().GetKubernetesCluster(ctx, req.ID)
if err != nil {
resp.Diagnostics.Append(tfdiag.DiagFromWrappedErr("Error reading KubeCluster", trace.Wrap(err), "kube_cluster"))
return
}
kubeClusterResource := kubeCluster.(*apitypes.KubernetesClusterV3)
var state types.Object
diags := resp.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = tfschema.CopyKubernetesClusterV3ToTerraform(ctx, kubeClusterResource, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
id := kubeClusterResource.GetName()
state.Attrs["id"] = types.String{Value: id}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
@@ -0,0 +1,100 @@
// 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 <http://www.gnu.org/licenses/>.
package resources
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
apitypes "github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
convertv1 "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
)
// NewAccessListDataSourceType returns the app data source type.
func NewAccessListDataSourceType() tfdriver.DataSourceType[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.DataSourceType[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier]{
NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewAccessListClient(clientFromProvider(p))
},
Kind: apitypes.KindAccessList,
Codec: tfdriver.DataSourceCodecFuncs[accesslist.AccessList]{
SchemaFunc: schemav1.GenSchemaAccessList,
ToStateFunc: func(ctx context.Context, al *accesslist.AccessList, o *types.Object) diag.Diagnostics {
return schemav1.CopyAccessListToTerraform(ctx, convertv1.ToProto(al), o)
},
},
Identifier: tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierFromPath(
path.Root("header").AtName("metadata").AtName("name"),
path.Root("scope"),
),
}
}
// NewAccessListResourceType returns the app resource type.
func NewAccessListResourceType() tfdriver.ResourceType[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.ResourceType[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier]{
NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[accesslist.AccessList, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewAccessListClient(clientFromProvider(p))
},
Kind: apitypes.KindAccessList,
Codec: tfdriver.ResourceCodecFuncs[accesslist.AccessList]{
SchemaFunc: schemav1.GenSchemaAccessList,
ToStateFunc: func(ctx context.Context, al *accesslist.AccessList, o *types.Object) diag.Diagnostics {
return schemav1.CopyAccessListToTerraform(ctx, convertv1.ToProto(al), o)
},
FromPlanFunc: func(ctx context.Context, o types.Object, al *accesslist.AccessList) diag.Diagnostics {
protoACL := new(accesslistv1.AccessList)
diags := schemav1.CopyAccessListFromTerraform(ctx, o, protoACL)
if diags.HasError() {
return diags
}
converted, err := convertv1.FromProto(protoACL)
if err != nil {
diags.AddError("Error converting access list", err.Error())
return diags
}
*al = *converted
return diags
},
},
Normalizer: tfdriver.CheckAndSetDefaults[accesslist.AccessList](),
Identifier: tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierPolicy(
path.Root("header").AtName("metadata").AtName("name"),
path.Root("scope"),
func(av *accesslist.AccessList) (string, string) {
return av.GetMetadata().Name, av.GetScope()
},
),
ResourceRevision: func(st *accesslist.AccessList) string {
return st.GetMetadata().Revision
},
}
}
@@ -0,0 +1,99 @@
// 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 <http://www.gnu.org/licenses/>.
package resources
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
apitypes "github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/accesslist"
convertv1 "github.com/gravitational/teleport/api/types/accesslist/convert/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/accesslist/v1"
)
// NewAccessListMemberDataSourceType returns the app data source type.
func NewAccessListMemberDataSourceType() tfdriver.DataSourceType[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier] {
return tfdriver.DataSourceType[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier]{
NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier] {
return teleport.NewAccessListMemberClient(clientFromProvider(p))
},
Kind: apitypes.KindAccessListMember,
Codec: tfdriver.DataSourceCodecFuncs[accesslist.AccessListMember]{
SchemaFunc: schemav1.GenSchemaMember,
ToStateFunc: func(ctx context.Context, alm *accesslist.AccessListMember, o *types.Object) diag.Diagnostics {
return schemav1.CopyMemberToTerraform(ctx, convertv1.ToMemberProto(alm), o)
},
},
Identifier: tfdriver.ScopeQualifiedCompositeIdentifierFromPath(
path.Root("spec").AtName("access_list"),
path.Root("header").AtName("metadata").AtName("name"),
),
}
}
// NewAccessListMemberResourceType returns the app resource type.
func NewAccessListMemberResourceType() tfdriver.ResourceType[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier] {
return tfdriver.ResourceType[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier]{
NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[accesslist.AccessListMember, tfdriver.ScopeQualifiedCompositeIdentifier] {
return teleport.NewAccessListMemberClient(clientFromProvider(p))
},
Kind: apitypes.KindAccessListMember,
Codec: tfdriver.ResourceCodecFuncs[accesslist.AccessListMember]{
SchemaFunc: schemav1.GenSchemaMember,
ToStateFunc: func(ctx context.Context, alm *accesslist.AccessListMember, o *types.Object) diag.Diagnostics {
return schemav1.CopyMemberToTerraform(ctx, convertv1.ToMemberProto(alm), o)
},
FromPlanFunc: func(ctx context.Context, o types.Object, alm *accesslist.AccessListMember) diag.Diagnostics {
protoMember := new(accesslistv1.Member)
diags := schemav1.CopyMemberFromTerraform(ctx, o, protoMember)
if diags.HasError() {
return diags
}
converted, err := convertv1.FromMemberProto(protoMember)
if err != nil {
diags.AddError("Error converting access list member", err.Error())
return diags
}
*alm = *converted
return diags
},
},
Normalizer: tfdriver.CheckAndSetDefaults[accesslist.AccessListMember](),
Identifier: tfdriver.ScopeQualifiedCompositeIdentifierPolicy(
path.Root("spec").AtName("access_list"),
path.Root("header").AtName("metadata").AtName("name"),
func(av *accesslist.AccessListMember) (string, string) {
return av.Spec.AccessList, av.GetMetadata().Name
}),
ResourceRevision: func(st *accesslist.AccessListMember) string {
return st.GetMetadata().Revision
},
}
}
@@ -0,0 +1,71 @@
// 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 <http://www.gnu.org/licenses/>.
package resources
import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
apitypes "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"
)
// NewKubernetesClusterDataSourceType returns the app data source type.
func NewKubernetesClusterDataSourceType() tfdriver.DataSourceType[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.DataSourceType[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier]{
NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewKubernetesClient(clientFromProvider(p))
},
Kind: apitypes.KindKubernetesCluster,
Codec: tfdriver.DataSourceCodecFuncs[apitypes.KubernetesClusterV3]{
SchemaFunc: tfschema.GenSchemaKubernetesClusterV3,
ToStateFunc: tfschema.CopyKubernetesClusterV3ToTerraform,
},
Identifier: tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierFromPath(
path.Root("metadata").AtName("name"),
path.Root("scope"),
),
}
}
// NewKubernetesClusterResourceType returns the app resource type.
func NewKubernetesClusterResourceType() tfdriver.ResourceType[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.ResourceType[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier]{
NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[apitypes.KubernetesClusterV3, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewKubernetesClient(clientFromProvider(p))
},
Kind: apitypes.KindKubernetesCluster,
Codec: tfdriver.ResourceCodecFuncs[apitypes.KubernetesClusterV3]{
SchemaFunc: tfschema.GenSchemaKubernetesClusterV3,
ToStateFunc: tfschema.CopyKubernetesClusterV3ToTerraform,
FromPlanFunc: tfschema.CopyKubernetesClusterV3FromTerraform,
},
Normalizer: tfdriver.CheckAndSetDefaults[apitypes.KubernetesClusterV3](),
Identifier: tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierPolicy(
path.Root("metadata").AtName("name"),
path.Root("scope"),
func(av *apitypes.KubernetesClusterV3) (string, string) {
return av.GetMetadata().Name, av.GetScope()
}),
ResourceRevision: func(st *apitypes.KubernetesClusterV3) string {
return st.GetMetadata().Revision
},
}
}
@@ -0,0 +1,69 @@
// 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 <http://www.gnu.org/licenses/>.
package resources
import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/v1"
)
// NewScopedRoleDataSourceType returns the scoped role data source type.
func NewScopedRoleDataSourceType() tfdriver.DataSourceType[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.DataSourceType[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier]{
NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewScopedRoleClient(clientFromProvider(p))
},
Identifier: tfdriver.ScopeQualifiedNameIdentifierFromPath(path.Root("metadata").AtName("name"), path.Root("scope")),
Kind: scopedaccess.KindScopedRole,
Codec: tfdriver.DataSourceCodecFuncs[accessv1.ScopedRole]{
SchemaFunc: schemav1.GenSchemaScopedRole,
ToStateFunc: schemav1.CopyScopedRoleToTerraform,
},
}
}
// NewScopedRoleResourceType returns the scoped role resource type.
func NewScopedRoleResourceType() tfdriver.ResourceType[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.ResourceType[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier]{
NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[accessv1.ScopedRole, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewScopedRoleClient(clientFromProvider(p))
},
Kind: scopedaccess.KindScopedRole,
Codec: tfdriver.ResourceCodecFuncs[accessv1.ScopedRole]{
SchemaFunc: schemav1.GenSchemaScopedRole,
ToStateFunc: schemav1.CopyScopedRoleToTerraform,
FromPlanFunc: schemav1.CopyScopedRoleFromTerraform,
},
Normalizer: tfdriver.ForceKind[accessv1.ScopedRole](scopedaccess.KindScopedRole),
Identifier: tfdriver.ScopeQualifiedNameIdentifierPolicy(
path.Root("metadata").AtName("name"),
path.Root("scope"),
func(st *accessv1.ScopedRole) (string, string) {
return st.GetMetadata().GetName(), st.GetScope()
}),
ResourceRevision: func(st *accessv1.ScopedRole) string {
return st.GetMetadata().GetRevision()
},
}
}
@@ -0,0 +1,69 @@
// 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 <http://www.gnu.org/licenses/>.
package resources
import (
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
scopedaccess "github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/teleport"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
schemav1 "github.com/gravitational/teleport/integrations/terraform/tfschema/scopes/access/assignment/v1"
)
// NewScopedRoleAssignmentDataSourceType returns the scoped token data source type.
func NewScopedRoleAssignmentDataSourceType() tfdriver.DataSourceType[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.DataSourceType[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier]{
NewDataSourceClient: func(p tfsdk.Provider) tfdriver.DataSourceClient[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewScopedRoleAssignmentClient(clientFromProvider(p))
},
Identifier: tfdriver.ScopeQualifiedNameIdentifierFromPath(path.Root("metadata").AtName("name"), path.Root("scope")),
Kind: scopedaccess.KindScopedRoleAssignment,
Codec: tfdriver.DataSourceCodecFuncs[accessv1.ScopedRoleAssignment]{
SchemaFunc: schemav1.GenSchemaScopedRoleAssignment,
ToStateFunc: schemav1.CopyScopedRoleAssignmentToTerraform,
},
}
}
// NewScopedRoleAssignmentResourceType returns the scoped token resource type.
func NewScopedRoleAssignmentResourceType() tfdriver.ResourceType[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier] {
return tfdriver.ResourceType[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier]{
NewResourceClient: func(p tfsdk.Provider) tfdriver.ResourceClient[accessv1.ScopedRoleAssignment, tfdriver.ScopeQualifiedNameIdentifier] {
return teleport.NewScopedRoleAssignmentClient(clientFromProvider(p))
},
Kind: scopedaccess.KindScopedRoleAssignment,
Codec: tfdriver.ResourceCodecFuncs[accessv1.ScopedRoleAssignment]{
SchemaFunc: schemav1.GenSchemaScopedRoleAssignment,
ToStateFunc: schemav1.CopyScopedRoleAssignmentToTerraform,
FromPlanFunc: schemav1.CopyScopedRoleAssignmentFromTerraform,
},
Normalizer: tfdriver.ForceKind[accessv1.ScopedRoleAssignment](scopedaccess.KindScopedRoleAssignment),
Identifier: tfdriver.ScopeQualifiedNameIdentifierPolicy(
path.Root("metadata").AtName("name"),
path.Root("scope"),
func(st *accessv1.ScopedRoleAssignment) (string, string) {
return st.GetMetadata().GetName(), st.GetScope()
}),
ResourceRevision: func(st *accessv1.ScopedRoleAssignment) string {
return st.GetMetadata().GetRevision()
},
}
}
@@ -0,0 +1,84 @@
// 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 <http://www.gnu.org/licenses/>.
package teleport
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
)
// NewAccessListClient returns an access list client.
func NewAccessListClient(c *client.Client) AccessListClient {
return AccessListClient{client: c}
}
// AccessListClient manages access list resources.
type AccessListClient struct {
client *client.Client
}
// Get reads an access list by name.
func (r AccessListClient) Get(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) (*accesslist.AccessList, error) {
al, err := r.client.AccessListClient().GetAccessListV2(ctx, &accesslistv1.GetAccessListRequest{
Name: id.Name,
Scope: id.Scope,
})
if err != nil {
return nil, trace.Wrap(err)
}
return al, nil
}
// Create creates an access list.
func (r AccessListClient) Create(ctx context.Context, al *accesslist.AccessList) error {
if _, err := r.client.AccessListClient().UpsertAccessList(ctx, al); err != nil {
return trace.Wrap(err)
}
return nil
}
// PrepareUpdate preserves server-computed fields that should not be reset on update.
func (r AccessListClient) PrepareUpdate(before, after *accesslist.AccessList) error {
after.Spec.Audit.NextAuditDate = before.Spec.Audit.NextAuditDate
return nil
}
// Upsert updates an access list.
func (r AccessListClient) Upsert(ctx context.Context, al *accesslist.AccessList) error {
if _, err := r.client.AccessListClient().UpsertAccessList(ctx, al); err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes an access list by name.
func (r AccessListClient) Delete(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) error {
return trace.Wrap(r.client.AccessListClient().DeleteAccessListV2(ctx, &accesslistv1.DeleteAccessListRequest{
Name: id.Name,
Scope: id.Scope,
}))
}
@@ -0,0 +1,82 @@
// 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 <http://www.gnu.org/licenses/>.
package teleport
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/api/types/accesslist"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
)
// NewAccessListMemberClient returns an access list member client.
func NewAccessListMemberClient(c *client.Client) AccessListMemberClient {
return AccessListMemberClient{client: c}
}
// AccessListMemberClient manages access list member resources.
type AccessListMemberClient struct {
client *client.Client
}
// Get reads an access list member by name.
func (r AccessListMemberClient) Get(ctx context.Context, id tfdriver.ScopeQualifiedCompositeIdentifier) (*accesslist.AccessListMember, error) {
member, err := r.client.AccessListClient().GetStaticAccessListMemberV2(ctx, accesslistv1.GetStaticAccessListMemberRequest_builder{
AccessListScope: id.Prefix.Scope,
AccessList: id.Prefix.Name,
MemberScope: id.Name.Scope,
MemberName: id.Name.Name,
}.Build())
if err != nil {
return nil, trace.Wrap(err)
}
return member, nil
}
// Create creates an access list member.
func (r AccessListMemberClient) Create(ctx context.Context, member *accesslist.AccessListMember) error {
if _, err := r.client.AccessListClient().UpsertStaticAccessListMember(ctx, member); err != nil {
return trace.Wrap(err)
}
return nil
}
// Upsert updates an access list member.
func (r AccessListMemberClient) Upsert(ctx context.Context, member *accesslist.AccessListMember) error {
if _, err := r.client.AccessListClient().UpsertStaticAccessListMember(ctx, member); err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes an access list member by name.
func (r AccessListMemberClient) Delete(ctx context.Context, id tfdriver.ScopeQualifiedCompositeIdentifier) error {
return trace.Wrap(r.client.AccessListClient().DeleteStaticAccessListMemberV2(ctx, accesslistv1.DeleteStaticAccessListMemberRequest_builder{
AccessListScope: id.Prefix.Scope,
AccessList: id.Prefix.Name,
MemberScope: id.Name.Scope,
MemberName: id.Name.Name,
}.Build()))
}
@@ -0,0 +1,83 @@
// 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 <http://www.gnu.org/licenses/>.
package teleport
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
presencev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/presence/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
)
// NewKubernetesClient returns a Teleport client to interact with Kubernetes Cluster resources.
func NewKubernetesClient(c *client.Client) KubernetesClient {
return KubernetesClient{client: c}
}
// KubernetesClient manages Kubernetes Cluster resources in Teleport.
type KubernetesClient struct {
client *client.Client
}
// Get reads a Kubernetes Cluster by name.
func (r KubernetesClient) Get(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) (*types.KubernetesClusterV3, error) {
cluster, err := r.client.GetKubeCluster(ctx, presencev1.GetKubeClusterRequest_builder{
Name: id.Name,
Scope: id.Scope,
}.Build())
if err != nil {
return nil, trace.Wrap(err)
}
clusterv3, ok := cluster.(*types.KubernetesClusterV3)
if !ok {
return nil, trace.BadParameter("unexpected Kubernetes cluster type: %T", cluster)
}
return clusterv3, nil
}
// Create creates a Kubernetes Cluster resource in Teleport.
func (r KubernetesClient) Create(ctx context.Context, cluster *types.KubernetesClusterV3) error {
if err := r.client.CreateKubernetesCluster(ctx, cluster); err != nil {
return trace.Wrap(err)
}
return nil
}
// Upsert updates a Kubernetes Cluster resource in Teleport.
func (r KubernetesClient) Upsert(ctx context.Context, cluster *types.KubernetesClusterV3) error {
if err := r.client.UpdateKubernetesCluster(ctx, cluster); err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes a Kubernetes Cluster resource from Teleport by name.
func (r KubernetesClient) Delete(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) error {
return trace.Wrap(r.client.DeleteKubeCluster(ctx, presencev1.DeleteKubeClusterRequest_builder{
Name: id.Name,
Scope: id.Scope,
}.Build()))
}
@@ -0,0 +1,84 @@
// 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 <http://www.gnu.org/licenses/>.
package teleport
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
)
// NewScopedRoleClient returns a scoped role client.
func NewScopedRoleClient(c *client.Client) ScopedRoleClient {
return ScopedRoleClient{client: c}
}
// ScopedRoleClient manages scoped role resources.
type ScopedRoleClient struct {
client *client.Client
}
// Get reads a scoped role by name.
func (r ScopedRoleClient) Get(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) (*accessv1.ScopedRole, error) {
resp, err := r.client.ScopedAccessServiceClient().GetScopedRole(ctx, &accessv1.GetScopedRoleRequest{
Name: id.Name,
Scope: id.Scope,
})
if err != nil {
return nil, trace.Wrap(err)
}
return resp.GetRole(), nil
}
// Create creates a scoped role.
func (r ScopedRoleClient) Create(ctx context.Context, role *accessv1.ScopedRole) error {
_, err := r.client.ScopedAccessServiceClient().CreateScopedRole(ctx, &accessv1.CreateScopedRoleRequest{
Role: role,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Upsert updates a scoped role.
func (r ScopedRoleClient) Upsert(ctx context.Context, role *accessv1.ScopedRole) error {
_, err := r.client.ScopedAccessServiceClient().UpsertScopedRole(ctx, &accessv1.UpsertScopedRoleRequest{
Role: role,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes a scoped role by name.
func (r ScopedRoleClient) Delete(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) error {
_, err := r.client.ScopedAccessServiceClient().DeleteScopedRole(ctx, &accessv1.DeleteScopedRoleRequest{
Name: id.Name,
Scope: id.Scope,
})
return trace.Wrap(err)
}
@@ -0,0 +1,88 @@
// 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 <http://www.gnu.org/licenses/>.
package teleport
import (
"context"
"github.com/gravitational/trace"
"github.com/gravitational/teleport/api/client"
accessv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/scopes/access/v1"
"github.com/gravitational/teleport/lib/scopes/access"
"github.com/gravitational/teleport/integrations/terraform/provider/internal/tfdriver"
)
// NewScopedRoleAssignmentClient returns a scoped role assignment client.
func NewScopedRoleAssignmentClient(c *client.Client) ScopedRoleAssignmentClient {
return ScopedRoleAssignmentClient{client: c}
}
// ScopedRoleAssignmentClient manages scoped role assignment resources.
type ScopedRoleAssignmentClient struct {
client *client.Client
}
// Get reads a scoped role assignment by name.
func (r ScopedRoleAssignmentClient) Get(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) (*accessv1.ScopedRoleAssignment, error) {
resp, err := r.client.ScopedAccessServiceClient().GetScopedRoleAssignment(ctx, &accessv1.GetScopedRoleAssignmentRequest{
Name: id.Name,
Scope: id.Scope,
SubKind: access.SubKindDynamic,
})
if err != nil {
return nil, trace.Wrap(err)
}
return resp.GetAssignment(), nil
}
// Create creates a scoped role assignment.
func (r ScopedRoleAssignmentClient) Create(ctx context.Context, sra *accessv1.ScopedRoleAssignment) error {
_, err := r.client.ScopedAccessServiceClient().CreateScopedRoleAssignment(ctx, &accessv1.CreateScopedRoleAssignmentRequest{
Assignment: sra,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Upsert updates a scoped role assignment.
func (r ScopedRoleAssignmentClient) Upsert(ctx context.Context, sra *accessv1.ScopedRoleAssignment) error {
_, err := r.client.ScopedAccessServiceClient().UpsertScopedRoleAssignment(ctx, &accessv1.UpsertScopedRoleAssignmentRequest{
Assignment: sra,
})
if err != nil {
return trace.Wrap(err)
}
return nil
}
// Delete deletes a scoped role assignment by name.
func (r ScopedRoleAssignmentClient) Delete(ctx context.Context, id tfdriver.ScopeQualifiedNameIdentifier) error {
_, err := r.client.ScopedAccessServiceClient().DeleteScopedRoleAssignment(ctx, &accessv1.DeleteScopedRoleAssignmentRequest{
Name: id.Name,
Scope: id.Scope,
SubKind: access.SubKindDynamic,
})
return trace.Wrap(err)
}
@@ -84,6 +84,18 @@ func (n CompositeIdentifier) String() string {
return n.Prefix + "/" + n.Name
}
// ScopeQualifiedCompositeIdentifier identifies a resource by a possibly
// scope-qualified prefix and a possibly scope-qualified name.
type ScopeQualifiedCompositeIdentifier struct {
Prefix ScopeQualifiedNameIdentifier
Name ScopeQualifiedNameIdentifier
}
// String returns the Terraform import ID.
func (n ScopeQualifiedCompositeIdentifier) String() string {
return n.Prefix.String() + "/" + n.Name.String()
}
// SingletonIdentifier identifies a resource with a fixed name.
type SingletonIdentifier struct {
Name string
@@ -143,6 +155,16 @@ func NewScopedQualifiedNameIdentifier(s string) (ScopeQualifiedNameIdentifier, e
return ScopeQualifiedNameIdentifier{Name: sqn.Name, Scope: sqn.Scope}, nil
}
// NewPossiblyUnscopedScopeQualifiedNameIdentifier parses an identifier that may
// be either a scope-qualified name or an unscoped bare name.
func NewPossiblyUnscopedScopeQualifiedNameIdentifier(s string) (ScopeQualifiedNameIdentifier, error) {
if strings.Contains(s, scopes.QualifiedNameSeparator) {
return NewScopedQualifiedNameIdentifier(s)
}
return ScopeQualifiedNameIdentifier{Name: s}, nil
}
// ScopeQualifiedNameIdentifierFromPath returns a scope qualified name extractor.
func ScopeQualifiedNameIdentifierFromPath(namePath, scopePath path.Path) TerraformIdentifierExtractor[ScopeQualifiedNameIdentifier] {
return func(ctx context.Context, reader TerraformAttributeReader) (ScopeQualifiedNameIdentifier, diag.Diagnostics) {
@@ -168,6 +190,36 @@ func ScopeQualifiedNameIdentifierFromPath(namePath, scopePath path.Path) Terrafo
}
}
// PossiblyUnscopedScopeQualifiedNameIdentifierFromPath returns an extractor for
// identifiers that may be either scope-qualified or unscoped.
func PossiblyUnscopedScopeQualifiedNameIdentifierFromPath(namePath, scopePath path.Path) TerraformIdentifierExtractor[ScopeQualifiedNameIdentifier] {
return func(ctx context.Context, reader TerraformAttributeReader) (ScopeQualifiedNameIdentifier, diag.Diagnostics) {
var identifier types.String
diags := reader.GetAttribute(ctx, namePath, &identifier)
if diags.HasError() {
return ScopeQualifiedNameIdentifier{}, diags
}
var scope types.String
diags.Append(reader.GetAttribute(ctx, scopePath, &scope)...)
if diags.HasError() {
return ScopeQualifiedNameIdentifier{}, diags
}
if scope.Null || scope.Unknown || scope.Value == "" {
return ScopeQualifiedNameIdentifier{Name: identifier.Value}, diags
}
sqn := scopes.QualifiedName{Name: identifier.Value, Scope: scope.Value}
if err := sqn.WeakValidate(); err != nil {
diags.Append(tfdiag.DiagFromErr("malformed scope qualified name", err))
return ScopeQualifiedNameIdentifier{}, diags
}
return ScopeQualifiedNameIdentifier{Name: sqn.Name, Scope: sqn.Scope}, diags
}
}
// ScopeQualifiedNameIdentifierPolicy returns a policy for scope qualified names.
func ScopeQualifiedNameIdentifierPolicy[T any](namePath, scopePath path.Path, resourceNameAndScope func(*T) (name, scope string)) IdentifierPolicy[T, ScopeQualifiedNameIdentifier] {
return IdentifierPolicy[T, ScopeQualifiedNameIdentifier]{
@@ -180,6 +232,19 @@ func ScopeQualifiedNameIdentifierPolicy[T any](namePath, scopePath path.Path, re
}
}
// PossiblyUnscopedScopeQualifiedNameIdentifierPolicy returns a policy for
// identifiers that may be either scope-qualified or unscoped.
func PossiblyUnscopedScopeQualifiedNameIdentifierPolicy[T any](namePath, scopePath path.Path, resourceNameAndScope func(*T) (name, scope string)) IdentifierPolicy[T, ScopeQualifiedNameIdentifier] {
return IdentifierPolicy[T, ScopeQualifiedNameIdentifier]{
FromState: PossiblyUnscopedScopeQualifiedNameIdentifierFromPath(namePath, scopePath),
FromResource: func(resource *T) ScopeQualifiedNameIdentifier {
name, scope := resourceNameAndScope(resource)
return ScopeQualifiedNameIdentifier{Name: name, Scope: scope}
},
FromImportID: NewPossiblyUnscopedScopeQualifiedNameIdentifier,
}
}
// NewCompositeIdentifier parses a composite identifier.
func NewCompositeIdentifier(s string) (CompositeIdentifier, error) {
split := strings.Split(s, "/")
@@ -231,6 +296,130 @@ func CompositeIdentifierPolicy[T any](prefixPath, namePath path.Path, resourcePr
}
}
// NewScopeQualifiedCompositeIdentifier parses a scope-qualified composite
// identifier in "prefix/name" form, where either side may be a scope-qualified
// name. For example: "access-list/alice", "/scope::access-list/alice", or
// "/scope::access-list//scope::child-list".
func NewScopeQualifiedCompositeIdentifier(s string) (ScopeQualifiedCompositeIdentifier, error) {
prefix, name, err := splitScopeQualifiedCompositeIdentifier(s)
if err != nil {
return ScopeQualifiedCompositeIdentifier{}, trace.Wrap(err)
}
return newScopeQualifiedCompositeIdentifier(prefix, name)
}
func splitScopeQualifiedCompositeIdentifier(s string) (prefix, name string, err error) {
if strings.HasPrefix(s, "/") {
scope, rest, ok := strings.Cut(s, scopes.QualifiedNameSeparator)
if !ok {
return "", "", trace.BadParameter("expected scoped composite id %q prefix to contain %q", s, scopes.QualifiedNameSeparator)
}
prefixName, name, ok := strings.Cut(rest, "/")
if !ok {
return "", "", trace.BadParameter("expected id %q to have a %q separator after prefix", s, "/")
}
return scope + scopes.QualifiedNameSeparator + prefixName, name, nil
}
prefix, name, ok := strings.Cut(s, "/")
if !ok {
return "", "", trace.BadParameter("expected id %q to have a %q separator", s, "/")
}
if strings.Contains(name, "/") && !strings.HasPrefix(name, "/") {
return "", "", trace.BadParameter("expected unscoped member name in id %q not to contain %q", s, "/")
}
return prefix, name, nil
}
func newScopeQualifiedCompositeIdentifier(prefix, name string) (ScopeQualifiedCompositeIdentifier, error) {
if prefix == "" {
return ScopeQualifiedCompositeIdentifier{}, trace.BadParameter("prefix must be non-empty")
}
if name == "" {
return ScopeQualifiedCompositeIdentifier{}, trace.BadParameter("name must be non-empty")
}
prefixSQN, err := NewPossiblyUnscopedScopeQualifiedNameIdentifier(prefix)
if err != nil {
return ScopeQualifiedCompositeIdentifier{}, trace.Wrap(err)
}
nameSQN, err := NewPossiblyUnscopedScopeQualifiedNameIdentifier(name)
if err != nil {
return ScopeQualifiedCompositeIdentifier{}, trace.Wrap(err)
}
return ScopeQualifiedCompositeIdentifier{Prefix: prefixSQN, Name: nameSQN}, nil
}
// ScopeQualifiedCompositeIdentifierFromPath returns an extractor for a
// scope-qualified composite identifier.
func ScopeQualifiedCompositeIdentifierFromPath(prefixPath, namePath path.Path) TerraformIdentifierExtractor[ScopeQualifiedCompositeIdentifier] {
return func(ctx context.Context, reader TerraformAttributeReader) (ScopeQualifiedCompositeIdentifier, diag.Diagnostics) {
var prefix types.String
diags := reader.GetAttribute(ctx, prefixPath, &prefix)
if diags.HasError() {
return ScopeQualifiedCompositeIdentifier{}, diags
}
var name types.String
diags.Append(reader.GetAttribute(ctx, namePath, &name)...)
if diags.HasError() {
return ScopeQualifiedCompositeIdentifier{}, diags
}
prefixSQN, err := scopeQualifiedNameIdentifierFromPossiblyQualifiedString(prefix.Value)
if err != nil {
diags.Append(tfdiag.DiagFromErr("malformed scope qualified prefix", err))
return ScopeQualifiedCompositeIdentifier{}, diags
}
nameSQN, err := scopeQualifiedNameIdentifierFromPossiblyQualifiedString(name.Value)
if err != nil {
diags.Append(tfdiag.DiagFromErr("malformed scope qualified name", err))
return ScopeQualifiedCompositeIdentifier{}, diags
}
return ScopeQualifiedCompositeIdentifier{Prefix: prefixSQN, Name: nameSQN}, diags
}
}
func scopeQualifiedNameIdentifierFromPossiblyQualifiedString(s string) (ScopeQualifiedNameIdentifier, error) {
if !strings.Contains(s, scopes.QualifiedNameSeparator) {
return ScopeQualifiedNameIdentifier{Name: s}, nil
}
sqn, err := scopes.ParseQualifiedName(s)
if err != nil {
return ScopeQualifiedNameIdentifier{}, trace.Wrap(err)
}
if err := sqn.WeakValidate(); err != nil {
return ScopeQualifiedNameIdentifier{}, trace.Wrap(err)
}
return ScopeQualifiedNameIdentifier{Name: sqn.Name, Scope: sqn.Scope}, nil
}
// ScopeQualifiedCompositeIdentifierPolicy returns a policy for scope-qualified
// composite identifiers.
func ScopeQualifiedCompositeIdentifierPolicy[T any](prefixPath, namePath path.Path, resourcePrefixAndName func(*T) (prefix, name string)) IdentifierPolicy[T, ScopeQualifiedCompositeIdentifier] {
return IdentifierPolicy[T, ScopeQualifiedCompositeIdentifier]{
FromState: ScopeQualifiedCompositeIdentifierFromPath(prefixPath, namePath),
FromResource: func(resource *T) ScopeQualifiedCompositeIdentifier {
prefix, name := resourcePrefixAndName(resource)
prefixID, _ := scopeQualifiedNameIdentifierFromPossiblyQualifiedString(prefix)
nameID, _ := scopeQualifiedNameIdentifierFromPossiblyQualifiedString(name)
return ScopeQualifiedCompositeIdentifier{Prefix: prefixID, Name: nameID}
},
FromImportID: NewScopeQualifiedCompositeIdentifier,
}
}
// NewSingletonIdentifier returns a singleton identifier.
func NewSingletonIdentifier(s string) (SingletonIdentifier, error) {
return SingletonIdentifier{Name: s}, nil
@@ -191,6 +191,100 @@ func TestScopeQualifiedNameIdentifierFromPath(t *testing.T) {
}
}
func TestPossiblyUnscopedScopeQualifiedNameIdentifierFromPath(t *testing.T) {
cases := []struct {
name string
attributer attributeReaderFunc
expectedIdentifier tfdriver.ScopeQualifiedNameIdentifier
expectError bool
}{
{
name: "unscoped empty scope",
attributer: func(p path.Path, v any) diag.Diagnostics {
s, ok := v.(*types.String)
if !ok {
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("expected type string, but got %T", v))}
}
switch {
case p.Equal(path.Root("name")):
s.Value = "testing"
case p.Equal(path.Root("scope")):
s.Value = ""
}
return nil
},
expectedIdentifier: tfdriver.ScopeQualifiedNameIdentifier{Name: "testing"},
},
{
name: "unscoped null scope",
attributer: func(p path.Path, v any) diag.Diagnostics {
s, ok := v.(*types.String)
if !ok {
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("expected type string, but got %T", v))}
}
switch {
case p.Equal(path.Root("name")):
s.Value = "testing"
case p.Equal(path.Root("scope")):
s.Null = true
}
return nil
},
expectedIdentifier: tfdriver.ScopeQualifiedNameIdentifier{Name: "testing"},
},
{
name: "scoped",
attributer: func(p path.Path, v any) diag.Diagnostics {
s, ok := v.(*types.String)
if !ok {
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("expected type string, but got %T", v))}
}
switch {
case p.Equal(path.Root("name")):
s.Value = "testing"
case p.Equal(path.Root("scope")):
s.Value = "/foo/bar"
}
return nil
},
expectedIdentifier: tfdriver.ScopeQualifiedNameIdentifier{Name: "testing", Scope: "/foo/bar"},
},
{
name: "invalid scoped",
attributer: func(p path.Path, v any) diag.Diagnostics {
s, ok := v.(*types.String)
if !ok {
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("expected type string, but got %T", v))}
}
switch {
case p.Equal(path.Root("name")):
s.Value = "testing"
case p.Equal(path.Root("scope")):
s.Value = "/foo/bar&[]/"
}
return nil
},
expectError: true,
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
identifier, d := tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierFromPath(path.Root("name"), path.Root("scope"))(t.Context(), test.attributer)
assert.Equal(t, test.expectError, d.HasError())
assert.Equal(t, test.expectedIdentifier, identifier)
})
}
}
func TestNewScopeQualifiedNameIdentifier(t *testing.T) {
cases := []struct {
name string
@@ -226,6 +320,73 @@ func TestNewScopeQualifiedNameIdentifier(t *testing.T) {
}
func TestNewPossiblyUnscopedScopeQualifiedNameIdentifier(t *testing.T) {
cases := []struct {
name string
input string
errorAssertion require.ErrorAssertionFunc
expectedIdentifier tfdriver.ScopeQualifiedNameIdentifier
}{
{
name: "unscoped",
input: "testing",
errorAssertion: require.NoError,
expectedIdentifier: tfdriver.ScopeQualifiedNameIdentifier{Name: "testing"},
},
{
name: "scoped",
input: "/animals/llama::testing",
errorAssertion: require.NoError,
expectedIdentifier: tfdriver.ScopeQualifiedNameIdentifier{Name: "testing", Scope: "/animals/llama"},
},
{
name: "invalid scoped",
input: "/animals/llama::testing&[]/",
errorAssertion: require.Error,
},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
identifier, err := tfdriver.NewPossiblyUnscopedScopeQualifiedNameIdentifier(test.input)
test.errorAssertion(t, err)
require.Equal(t, test.expectedIdentifier, identifier)
})
}
}
func TestPossiblyUnscopedScopeQualifiedNameIdentifierPolicy(t *testing.T) {
type testResource struct {
Name string
Scope string
}
policy := tfdriver.PossiblyUnscopedScopeQualifiedNameIdentifierPolicy(
path.Root("name"),
path.Root("scope"),
func(resource *testResource) (string, string) {
return resource.Name, resource.Scope
},
)
require.Equal(t,
tfdriver.ScopeQualifiedNameIdentifier{Name: "unscoped"},
policy.FromResource(&testResource{Name: "unscoped"}),
)
require.Equal(t,
tfdriver.ScopeQualifiedNameIdentifier{Name: "scoped", Scope: "/foo/bar"},
policy.FromResource(&testResource{Name: "scoped", Scope: "/foo/bar"}),
)
identifier, err := policy.FromImportID("unscoped")
require.NoError(t, err)
require.Equal(t, tfdriver.ScopeQualifiedNameIdentifier{Name: "unscoped"}, identifier)
identifier, err = policy.FromImportID("/foo/bar::scoped")
require.NoError(t, err)
require.Equal(t, tfdriver.ScopeQualifiedNameIdentifier{Name: "scoped", Scope: "/foo/bar"}, identifier)
}
func TestCompositeIdentifierFromPath(t *testing.T) {
cases := []struct {
name string
@@ -424,6 +585,64 @@ func TestCompositeIdentifierPolicy(t *testing.T) {
require.Equal(t, tfdriver.CompositeIdentifier{Prefix: "import-prefix", Name: "import-name"}, fromImport)
}
func TestScopeQualifiedCompositeIdentifierPolicy(t *testing.T) {
type resource struct {
prefix string
name string
}
policy := tfdriver.ScopeQualifiedCompositeIdentifierPolicy(
path.Root("prefix"),
path.Root("name"),
func(r *resource) (prefix, name string) {
return r.prefix, r.name
},
)
fromState, diags := policy.FromState(t.Context(), attributeReaderFunc(func(p path.Path, v any) diag.Diagnostics {
s, ok := v.(*types.String)
if !ok {
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("expected type string, but got %T", v))}
}
switch {
case p.Equal(path.Root("prefix")):
s.Value = "/state/scope::state-prefix"
case p.Equal(path.Root("name")):
s.Value = "/state/scope::state-name"
default:
return diag.Diagnostics{diag.NewErrorDiagnostic("fail", fmt.Sprintf("unexpected path %s", p.String()))}
}
return nil
}))
require.False(t, diags.HasError(), diags)
require.Equal(t, tfdriver.ScopeQualifiedCompositeIdentifier{
Prefix: tfdriver.ScopeQualifiedNameIdentifier{Name: "state-prefix", Scope: "/state/scope"},
Name: tfdriver.ScopeQualifiedNameIdentifier{Name: "state-name", Scope: "/state/scope"},
}, fromState)
require.Equal(t, "/state/scope::state-prefix//state/scope::state-name", fromState.String())
fromResource := policy.FromResource(&resource{prefix: "/resource/scope::resource-prefix", name: "resource-name"})
require.Equal(t, tfdriver.ScopeQualifiedCompositeIdentifier{
Prefix: tfdriver.ScopeQualifiedNameIdentifier{Name: "resource-prefix", Scope: "/resource/scope"},
Name: tfdriver.ScopeQualifiedNameIdentifier{Name: "resource-name"},
}, fromResource)
require.Equal(t, "/resource/scope::resource-prefix/resource-name", fromResource.String())
fromImport, err := policy.FromImportID("/import/scope::import-prefix//import/scope::import-name")
require.NoError(t, err)
require.Equal(t, tfdriver.ScopeQualifiedCompositeIdentifier{
Prefix: tfdriver.ScopeQualifiedNameIdentifier{Name: "import-prefix", Scope: "/import/scope"},
Name: tfdriver.ScopeQualifiedNameIdentifier{Name: "import-name", Scope: "/import/scope"},
}, fromImport)
legacyImport, err := policy.FromImportID("import-prefix/import-name")
require.NoError(t, err)
require.Equal(t, tfdriver.ScopeQualifiedCompositeIdentifier{
Prefix: tfdriver.ScopeQualifiedNameIdentifier{Name: "import-prefix"},
Name: tfdriver.ScopeQualifiedNameIdentifier{Name: "import-name"},
}, legacyImport)
require.Equal(t, "import-prefix/import-name", legacyImport.String())
}
func TestScopeQualifiedNameIdentifierPolicy(t *testing.T) {
type resource struct {
name string
+14 -4
View File
@@ -564,8 +564,13 @@ func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp
resourceTypes := legacy.ResourceTypes()
genericResourceTypes := map[string]tfsdk.ResourceType{
"teleport_app": resources.NewAppResourceType(),
"teleport_scoped_token": resources.NewScopedTokenResourceType(),
"teleport_access_list": resources.NewAccessListResourceType(),
"teleport_access_list_member": resources.NewAccessListMemberResourceType(),
"teleport_app": resources.NewAppResourceType(),
"teleport_kube_cluster": resources.NewKubernetesClusterResourceType(),
"teleport_scoped_role": resources.NewScopedRoleResourceType(),
"teleport_scoped_role_assignment": resources.NewScopedRoleAssignmentResourceType(),
"teleport_scoped_token": resources.NewScopedTokenResourceType(),
}
maps.Insert(resourceTypes, maps.All(genericResourceTypes))
@@ -578,8 +583,13 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc
dataSourceTypes := legacy.DataSourceTypes()
genericDataSourceTypes := map[string]tfsdk.DataSourceType{
"teleport_app": resources.NewAppDataSourceType(),
"teleport_scoped_token": resources.NewScopedTokenDataSourceType(),
"teleport_access_list": resources.NewAccessListDataSourceType(),
"teleport_access_list_member": resources.NewAccessListMemberDataSourceType(),
"teleport_app": resources.NewAppDataSourceType(),
"teleport_kube_cluster": resources.NewKubernetesClusterDataSourceType(),
"teleport_scoped_role": resources.NewScopedRoleDataSourceType(),
"teleport_scoped_role_assignment": resources.NewScopedRoleAssignmentDataSourceType(),
"teleport_scoped_token": resources.NewScopedTokenDataSourceType(),
}
maps.Insert(dataSourceTypes, maps.All(genericDataSourceTypes))
@@ -0,0 +1,221 @@
// 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 <http://www.gnu.org/licenses/>.
package testlib
import (
"context"
"time"
"github.com/gravitational/trace"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/accesslist"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
"github.com/gravitational/teleport/lib/scopes"
)
func checkAccessListMemberExists(ctx context.Context, clt *accesslist.Client, accessList, memberName string, expires time.Time) resource.TestCheckFunc {
return checkAccessListMemberExistsWithScopes(ctx, clt, "", accessList, "", memberName, "", accessList, expires)
}
func checkAccessListMemberExistsWithScopes(ctx context.Context, clt *accesslist.Client, accessListScope, accessListName, memberScope, memberName, expectedScope, expectedAccessList string, expires time.Time) resource.TestCheckFunc {
return func(state *terraform.State) error {
member, err := clt.GetStaticAccessListMemberV2(ctx, accesslistv1.GetStaticAccessListMemberRequest_builder{
AccessListScope: accessListScope,
AccessList: accessListName,
MemberScope: memberScope,
MemberName: memberName,
}.Build())
if err != nil {
return trace.Wrap(err)
}
if member.GetName() != memberName {
return trace.CompareFailed("member name mismatch: got %q, want %q", member.GetName(), memberName)
}
if member.Spec.Name != memberName {
return trace.CompareFailed("member spec name mismatch: got %q, want %q", member.Spec.Name, memberName)
}
if member.Scope != expectedScope {
return trace.CompareFailed("member scope mismatch: got %q, want %q", member.Scope, expectedScope)
}
if member.Spec.AccessList != expectedAccessList {
return trace.CompareFailed("member access list mismatch: got %q, want %q", member.Spec.AccessList, expectedAccessList)
}
if !member.Spec.Expires.Equal(expires) {
return trace.CompareFailed("member expiry mismatch: got %q, want %q", member.Spec.Expires, expires)
}
return nil
}
}
func checkScopedListAccessListMemberExists(ctx context.Context, clt *accesslist.Client, scope, accessListName, memberName string, expires time.Time) resource.TestCheckFunc {
return func(state *terraform.State) error {
member, err := clt.GetStaticAccessListMemberV2(ctx, accesslistv1.GetStaticAccessListMemberRequest_builder{
AccessListScope: scope,
AccessList: accessListName,
MemberScope: scope,
MemberName: memberName,
}.Build())
if err != nil {
return trace.Wrap(err)
}
accessListSQN := scopes.QualifiedName{Scope: scope, Name: accessListName}.String()
memberSQN := scopes.QualifiedName{Scope: scope, Name: memberName}.String()
if member.GetName() != memberSQN {
return trace.CompareFailed("member name mismatch: got %q, want %q", member.GetName(), memberSQN)
}
if member.Spec.Name != memberSQN {
return trace.CompareFailed("member spec name mismatch: got %q, want %q", member.Spec.Name, memberSQN)
}
if member.Scope != scope {
return trace.CompareFailed("member scope mismatch: got %q, want %q", member.Scope, scope)
}
if member.Spec.AccessList != accessListSQN {
return trace.CompareFailed("member access list mismatch: got %q, want %q", member.Spec.AccessList, accessListSQN)
}
if member.Spec.MembershipKind != "MEMBERSHIP_KIND_SCOPED_LIST" {
return trace.CompareFailed("member kind mismatch: got %q, want %q", member.Spec.MembershipKind, "MEMBERSHIP_KIND_SCOPED_LIST")
}
if !member.Spec.Expires.Equal(expires) {
return trace.CompareFailed("member expiry mismatch: got %q, want %q", member.Spec.Expires, expires)
}
return nil
}
}
func (s *TerraformSuiteEnterprise) TestAccessListMember() {
require.True(s.T(),
s.teleportFeatures.GetAdvancedAccessWorkflows(),
"Test requires Advanced Access Workflows",
)
ctx := s.T().Context()
accessListName := "test-member-list"
memberName := "fighter"
resourceName := "teleport_access_list_member.fighter"
checkDestroyed := func(state *terraform.State) error {
_, err := s.client.AccessListClient().GetStaticAccessListMember(ctx, accessListName, memberName)
if trace.IsNotFound(err) {
return nil
}
return trace.Wrap(err)
}
createdExpires := time.Date(2038, 1, 1, 0, 0, 0, 0, time.UTC)
updatedExpires := time.Date(2038, 2, 1, 0, 0, 0, 0, time.UTC)
resource.Test(s.T(), resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
CheckDestroy: checkDestroyed,
Steps: []resource.TestStep{
{
Config: s.getFixture("access_list_member_0_create.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "header.metadata.name", memberName),
resource.TestCheckResourceAttr(resourceName, "spec.access_list", accessListName),
resource.TestCheckResourceAttr(resourceName, "spec.membership_kind", "1"),
resource.TestCheckResourceAttr(resourceName, "spec.expires", createdExpires.Format(time.RFC3339)),
checkAccessListMemberExists(ctx, s.client.AccessListClient(), accessListName, memberName, createdExpires),
),
},
{
Config: s.getFixture("access_list_member_0_create.tf"),
PlanOnly: true,
},
{
Config: s.getFixture("access_list_member_1_update.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "header.metadata.labels.class", "veteran"),
resource.TestCheckResourceAttr(resourceName, "spec.expires", updatedExpires.Format(time.RFC3339)),
checkAccessListMemberExists(ctx, s.client.AccessListClient(), accessListName, memberName, updatedExpires),
),
},
{
Config: s.getFixture("access_list_member_1_update.tf"),
PlanOnly: true,
},
},
})
}
func (s *TerraformSuiteEnterpriseScopedResources) TestAccessListMemberScopedAndUnscoped() {
require.True(s.T(),
s.teleportFeatures.GetAdvancedAccessWorkflows(),
"Test requires Advanced Access Workflows",
)
ctx := s.T().Context()
accessListClient := s.client.AccessListClient()
checkDestroyed := func(state *terraform.State) error {
for _, tc := range []struct {
accessListScope string
accessListName string
memberScope string
memberName string
}{
{accessListName: "test-unscoped-member-list", memberName: "unscoped-fighter"},
{accessListScope: "/foo/bar", accessListName: "test-scoped-member-list", memberName: "scoped-fighter"},
{accessListScope: "/foo/bar", accessListName: "test-scoped-member-list", memberScope: "/foo/bar", memberName: "test-scoped-child-member-list"},
} {
_, err := accessListClient.GetStaticAccessListMemberV2(ctx, accesslistv1.GetStaticAccessListMemberRequest_builder{
AccessListScope: tc.accessListScope,
AccessList: tc.accessListName,
MemberScope: tc.memberScope,
MemberName: tc.memberName,
}.Build())
if err != nil && !trace.IsNotFound(err) {
return trace.Wrap(err)
}
}
return nil
}
expires := time.Date(2038, 3, 1, 0, 0, 0, 0, time.UTC)
resource.Test(s.T(), resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
CheckDestroy: checkDestroyed,
Steps: []resource.TestStep{
{
Config: s.getFixture("access_list_member_scoped_and_unscoped.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("teleport_access_list_member.unscoped", "header.metadata.name", "unscoped-fighter"),
resource.TestCheckResourceAttr("teleport_access_list_member.unscoped", "spec.access_list", "test-unscoped-member-list"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped", "header.metadata.name", "scoped-fighter"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped", "spec.access_list", "/foo/bar::test-scoped-member-list"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped", "scope", "/foo/bar"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped_list", "header.metadata.name", "/foo/bar::test-scoped-child-member-list"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped_list", "spec.access_list", "/foo/bar::test-scoped-member-list"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped_list", "spec.membership_kind", "3"),
resource.TestCheckResourceAttr("teleport_access_list_member.scoped_list", "scope", "/foo/bar"),
checkAccessListMemberExistsWithScopes(ctx, accessListClient, "", "test-unscoped-member-list", "", "unscoped-fighter", "", "test-unscoped-member-list", expires),
checkAccessListMemberExistsWithScopes(ctx, accessListClient, "/foo/bar", "test-scoped-member-list", "", "scoped-fighter", "/foo/bar", "/foo/bar::test-scoped-member-list", expires),
checkScopedListAccessListMemberExists(ctx, accessListClient, "/foo/bar", "test-scoped-member-list", "test-scoped-child-member-list", expires),
),
},
{
Config: s.getFixture("access_list_member_scoped_and_unscoped.tf"),
PlanOnly: true,
},
},
})
}
@@ -28,6 +28,8 @@ import (
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/accesslist"
accesslistv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1"
)
type nextAuditDateComparer struct {
@@ -35,20 +37,29 @@ type nextAuditDateComparer struct {
nextAuditDate time.Time
}
func (c *nextAuditDateComparer) CaptureNextAuditDate(name string) resource.TestCheckFunc {
func (c *nextAuditDateComparer) CaptureNextAuditDate(ctx context.Context, scope, name string) resource.TestCheckFunc {
return func(state *terraform.State) error {
al, err := c.client.AccessListClient().GetAccessList(context.TODO(), name)
al, err := c.client.AccessListClient().GetAccessListV2(ctx, &accesslistv1.GetAccessListRequest{
Name: name,
Scope: scope,
})
if err != nil {
return trace.Wrap(err)
}
if al.Spec.Audit.NextAuditDate.IsZero() {
return trace.BadParameter("NextAuditDate must not be zero")
}
c.nextAuditDate = al.Spec.Audit.NextAuditDate
return nil
}
}
func (c *nextAuditDateComparer) TestNextAuditDateUnchanged(name string) resource.TestCheckFunc {
func (c *nextAuditDateComparer) TestNextAuditDateUnchanged(ctx context.Context, scope, name string) resource.TestCheckFunc {
return func(state *terraform.State) error {
al, err := c.client.AccessListClient().GetAccessList(context.TODO(), name)
al, err := c.client.AccessListClient().GetAccessListV2(ctx, &accesslistv1.GetAccessListRequest{
Name: name,
Scope: scope,
})
if err != nil {
return trace.Wrap(err)
}
@@ -60,6 +71,22 @@ func (c *nextAuditDateComparer) TestNextAuditDateUnchanged(name string) resource
}
}
func checkAccessListExists(ctx context.Context, clt *accesslist.Client, scope, name string) resource.TestCheckFunc {
return func(state *terraform.State) error {
al, err := clt.GetAccessListV2(ctx, &accesslistv1.GetAccessListRequest{
Name: name,
Scope: scope,
})
if err != nil {
return trace.Wrap(err)
}
if al.GetScope() != scope {
return trace.CompareFailed("access list scope mismatch: got %q, want %q", al.GetScope(), scope)
}
return nil
}
}
func (s *TerraformSuiteEnterprise) TestAccessList() {
require.True(s.T(),
s.teleportFeatures.GetAdvancedAccessWorkflows(),
@@ -67,7 +94,9 @@ func (s *TerraformSuiteEnterprise) TestAccessList() {
)
checkAccessListDestroyed := func(state *terraform.State) error {
_, err := s.client.AccessListClient().GetAccessList(context.TODO(), "test")
_, err := s.client.AccessListClient().GetAccessListV2(s.T().Context(), &accesslistv1.GetAccessListRequest{
Name: "test",
})
if trace.IsNotFound(err) {
return nil
}
@@ -91,7 +120,7 @@ func (s *TerraformSuiteEnterprise) TestAccessList() {
resource.TestCheckResourceAttr(name, "spec.membership_requires.roles.0", "minion"),
resource.TestCheckResourceAttr(name, "spec.grants.roles.0", "crane-operator"),
resource.TestCheckResourceAttr(name, "spec.audit.recurrence.frequency", "3"),
auditDateChecker.CaptureNextAuditDate("test"),
auditDateChecker.CaptureNextAuditDate(s.T().Context(), "", "test"),
),
},
{
@@ -104,7 +133,7 @@ func (s *TerraformSuiteEnterprise) TestAccessList() {
resource.TestCheckResourceAttr(name, "spec.grants.traits.0.key", "allowed-machines"),
resource.TestCheckResourceAttr(name, "spec.grants.traits.0.values.0", "crane"),
resource.TestCheckResourceAttr(name, "spec.grants.traits.0.values.1", "forklift"),
auditDateChecker.TestNextAuditDateUnchanged("test"),
auditDateChecker.TestNextAuditDateUnchanged(s.T().Context(), "", "test"),
),
},
{
@@ -115,7 +144,7 @@ func (s *TerraformSuiteEnterprise) TestAccessList() {
Config: s.getFixture("access_list_2_expiring.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "header.metadata.expires", "2038-01-01T00:00:00Z"),
auditDateChecker.TestNextAuditDateUnchanged("test"),
auditDateChecker.TestNextAuditDateUnchanged(s.T().Context(), "", "test"),
),
},
{
@@ -125,3 +154,94 @@ func (s *TerraformSuiteEnterprise) TestAccessList() {
},
})
}
func (s *TerraformSuiteEnterprise) TestAccessListPreservesNextAuditDate() {
require.True(s.T(),
s.teleportFeatures.GetAdvancedAccessWorkflows(),
"Test requires Advanced Access Workflows",
)
checkAccessListDestroyed := func(state *terraform.State) error {
_, err := s.client.AccessListClient().GetAccessListV2(s.T().Context(), &accesslistv1.GetAccessListRequest{
Name: "test-next-audit-date",
})
if trace.IsNotFound(err) {
return nil
}
return err
}
name := "teleport_access_list.test"
auditDateChecker := nextAuditDateComparer{client: s.client}
resource.Test(s.T(), resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
CheckDestroy: checkAccessListDestroyed,
Steps: []resource.TestStep{
{
Config: s.getFixture("access_list_next_audit_date_0_create.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "header.metadata.name", "test-next-audit-date"),
resource.TestCheckResourceAttr(name, "spec.description", "next audit date test"),
auditDateChecker.CaptureNextAuditDate(s.T().Context(), "", "test-next-audit-date"),
),
},
{
Config: s.getFixture("access_list_next_audit_date_1_attempt_update.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(name, "spec.description", "updated next audit date test"),
auditDateChecker.TestNextAuditDateUnchanged(s.T().Context(), "", "test-next-audit-date"),
),
},
},
})
}
func (s *TerraformSuiteEnterpriseScopedResources) TestAccessListScopedAndUnscoped() {
require.True(s.T(),
s.teleportFeatures.GetAdvancedAccessWorkflows(),
"Test requires Advanced Access Workflows",
)
checkDestroyed := func(state *terraform.State) error {
for _, tc := range []struct {
scope string
name string
}{
{name: "test-unscoped"},
{scope: "/foo/bar", name: "test-scoped"},
} {
_, err := s.client.AccessListClient().GetAccessListV2(s.T().Context(), &accesslistv1.GetAccessListRequest{
Name: tc.name,
Scope: tc.scope,
})
if err != nil && !trace.IsNotFound(err) {
return err
}
}
return nil
}
resource.Test(s.T(), resource.TestCase{
ProtoV6ProviderFactories: s.terraformProviders,
CheckDestroy: checkDestroyed,
Steps: []resource.TestStep{
{
Config: s.getFixture("access_list_scoped_and_unscoped.tf"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("teleport_access_list.unscoped", "header.metadata.name", "test-unscoped"),
resource.TestCheckResourceAttr("teleport_access_list.unscoped", "spec.membership_requires.roles.0", "minion"),
resource.TestCheckResourceAttr("teleport_access_list.scoped", "header.metadata.name", "test-scoped"),
resource.TestCheckResourceAttr("teleport_access_list.scoped", "scope", "/foo/bar"),
checkAccessListExists(s.T().Context(), s.client.AccessListClient(), "", "test-unscoped"),
checkAccessListExists(s.T().Context(), s.client.AccessListClient(), "/foo/bar", "test-scoped"),
),
},
{
Config: s.getFixture("access_list_scoped_and_unscoped.tf"),
PlanOnly: true,
},
},
})
}
@@ -0,0 +1,39 @@
resource "teleport_access_list" "member_test" {
header = {
version = "v1"
metadata = {
name = "test-member-list"
}
}
spec = {
type = "static"
title = "Member test"
description = "Access list member Terraform test"
owners = [
{
name = "gru"
}
]
grants = {
roles = ["crane-operator"]
}
}
}
resource "teleport_access_list_member" "fighter" {
header = {
version = "v1"
metadata = {
name = "fighter"
labels = {
class = "fighter"
}
}
}
spec = {
access_list = teleport_access_list.member_test.id
membership_kind = 1
reason = "joined the party"
expires = "2038-01-01T00:00:00Z"
}
}
@@ -0,0 +1,39 @@
resource "teleport_access_list" "member_test" {
header = {
version = "v1"
metadata = {
name = "test-member-list"
}
}
spec = {
type = "static"
title = "Member test"
description = "Access list member Terraform test"
owners = [
{
name = "gru"
}
]
grants = {
roles = ["crane-operator"]
}
}
}
resource "teleport_access_list_member" "fighter" {
header = {
version = "v1"
metadata = {
name = "fighter"
labels = {
class = "veteran"
}
}
}
spec = {
access_list = teleport_access_list.member_test.id
membership_kind = 1
reason = "joined the party"
expires = "2038-02-01T00:00:00Z"
}
}
@@ -0,0 +1,118 @@
resource "teleport_access_list" "unscoped" {
header = {
version = "v1"
metadata = {
name = "test-unscoped-member-list"
}
}
spec = {
type = "static"
title = "Unscoped member list"
description = "unscoped access list member test"
owners = [
{
name = "gru"
}
]
grants = {
roles = ["crane-operator"]
}
}
}
resource "teleport_access_list" "scoped" {
depends_on = [teleport_access_list.unscoped]
header = {
version = "v1"
metadata = {
name = "test-scoped-member-list"
}
}
scope = "/foo/bar"
spec = {
type = "static"
title = "Scoped member list"
description = "scoped access list member test"
owners = [
{
name = "gru"
}
]
}
}
resource "teleport_access_list" "scoped_child" {
depends_on = [teleport_access_list.scoped]
header = {
version = "v1"
metadata = {
name = "test-scoped-child-member-list"
}
}
scope = "/foo/bar"
spec = {
type = "static"
title = "Scoped child member list"
description = "scoped child access list member test"
owners = [
{
name = "gru"
}
]
}
}
resource "teleport_access_list_member" "unscoped" {
depends_on = [teleport_access_list.scoped_child]
header = {
version = "v1"
metadata = {
name = "unscoped-fighter"
}
}
spec = {
access_list = teleport_access_list.unscoped.id
membership_kind = 1
reason = "unscoped member"
expires = "2038-03-01T00:00:00Z"
}
}
resource "teleport_access_list_member" "scoped" {
depends_on = [teleport_access_list_member.unscoped]
header = {
version = "v1"
metadata = {
name = "scoped-fighter"
}
}
scope = "/foo/bar"
spec = {
access_list = teleport_access_list.scoped.id
membership_kind = 1
reason = "scoped member"
expires = "2038-03-01T00:00:00Z"
}
}
resource "teleport_access_list_member" "scoped_list" {
depends_on = [teleport_access_list_member.scoped]
header = {
version = "v1"
metadata = {
name = teleport_access_list.scoped_child.id
}
}
scope = "/foo/bar"
spec = {
access_list = teleport_access_list.scoped.id
membership_kind = 3
reason = "scoped list member"
expires = "2038-03-01T00:00:00Z"
}
}
@@ -0,0 +1,35 @@
resource "teleport_access_list" "test" {
header = {
version = "v1"
metadata = {
name = "test-next-audit-date"
labels = {
example = "yes"
}
}
}
spec = {
description = "next audit date test"
owners = [
{
name = "gru"
description = "The supervillain."
}
]
membership_requires = {
roles = ["minion"]
}
ownership_requires = {
roles = ["supervillain"]
}
grants = {
roles = ["crane-operator"]
}
title = "Next Audit Date"
audit = {
recurrence = {
frequency = 3
}
}
}
}
@@ -0,0 +1,38 @@
resource "teleport_access_list" "test" {
header = {
version = "v1"
metadata = {
name = "test-next-audit-date"
labels = {
example = "yes"
}
}
}
spec = {
description = "updated next audit date test"
owners = [
{
name = "gru"
description = "The supervillain."
}
]
membership_requires = {
roles = ["minion"]
}
ownership_requires = {
roles = ["supervillain"]
}
grants = {
roles = ["crane-operator"]
}
title = "Next Audit Date"
audit = {
// Terraform should not clear or recompute the server-maintained next audit date during update.
next_audit_date = null
recurrence = {
frequency = 3
day_of_month = 15
}
}
}
}
@@ -0,0 +1,59 @@
resource "teleport_access_list" "unscoped" {
header = {
version = "v1"
metadata = {
name = "test-unscoped"
}
}
spec = {
description = "unscoped access list"
owners = [
{
name = "gru"
description = "The supervillain."
}
]
membership_requires = {
roles = ["minion"]
}
ownership_requires = {
roles = ["supervillain"]
}
grants = {
roles = ["crane-operator"]
}
title = "Unscoped"
audit = {
recurrence = {
frequency = 3
}
}
}
}
resource "teleport_access_list" "scoped" {
depends_on = [teleport_access_list.unscoped]
header = {
version = "v1"
metadata = {
name = "test-scoped"
}
}
scope = "/foo/bar"
spec = {
description = "scoped access list"
owners = [
{
name = "gru"
description = "The supervillain."
}
]
title = "Scoped"
audit = {
recurrence = {
frequency = 3
}
}
}
}
@@ -1,4 +1,5 @@
data "teleport_classifier" "test" {
kind = "classifier"
version = "v1"
metadata = {
name = "test-classifier"
@@ -78,6 +78,9 @@ type TerraformSuiteOSSScopedResources struct {
type TerraformSuiteEnterprise struct {
TerraformBaseSuite
}
type TerraformSuiteEnterpriseScopedResources struct {
TerraformBaseSuite
}
type TerraformSuiteEnterpriseWithCache struct {
TerraformBaseSuite
}
@@ -29,16 +29,19 @@ import (
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/plugin"
"github.com/gravitational/teleport/lib/scopes"
)
var testModules = &modulestest.Modules{
TestFeatures: modules.Features{
AdvancedAccessWorkflows: true,
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.OIDC: {Enabled: true},
entitlements.SAML: {Enabled: true},
entitlements.DeviceTrust: {Enabled: true},
entitlements.Policy: {Enabled: true},
entitlements.OIDC: {Enabled: true},
entitlements.SAML: {Enabled: true},
entitlements.DeviceTrust: {Enabled: true},
entitlements.Policy: {Enabled: true},
entitlements.SessionSummaries: {Enabled: true},
entitlements.AccessLists: {Enabled: true},
},
},
}
@@ -82,6 +85,28 @@ func TestTerraformEnterprise(t *testing.T) {
})
}
func TestTerraformEnterpriseScopedResources(t *testing.T) {
authPlugin, err := NewPlugin(testModules)
if trace.IsNotImplemented(err) {
t.Skip(entTestSkipMessage)
}
require.NoError(t, err)
registry := plugin.NewRegistry()
require.NoError(t, registry.Add(authPlugin))
suite.Run(t, &TerraformSuiteEnterpriseScopedResources{
TerraformBaseSuite: TerraformBaseSuite{
AuthHelper: &integration.MinimalAuthHelper{
PluginRegistry: registry,
AuthConfig: authtest.AuthServerConfig{
Modules: testModules,
ScopesFeatures: scopes.Features{Enabled: true},
},
},
},
})
}
func TestTerraformEnterpriseCloud(t *testing.T) {
authPlugin, err := NewPlugin(testModulesCloud)
if trace.IsNotImplemented(err) {
@@ -88,13 +88,6 @@ func GenSchemaAccessList(ctx context.Context) (github_com_hashicorp_terraform_pl
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"revision": {
Computed: true,
Description: "revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
}),
Description: "metadata is resource metadata.",
Optional: true,
@@ -121,9 +114,10 @@ func GenSchemaAccessList(ctx context.Context) (github_com_hashicorp_terraform_pl
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"scope": {
Description: "scope is the scope of the Access List.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
Description: "scope is the scope of the Access List.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.RequiresReplace()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"spec": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
@@ -390,13 +384,6 @@ func GenSchemaMember(ctx context.Context) (github_com_hashicorp_terraform_plugin
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"revision": {
Computed: true,
Description: "revision is an opaque identifier which tracks the versions of a resource over time. Clients should ignore and not alter its value but must return the revision in any updates of a resource.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
}),
Description: "metadata is resource metadata.",
Optional: true,
@@ -423,9 +410,10 @@ func GenSchemaMember(ctx context.Context) (github_com_hashicorp_terraform_plugin
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"scope": {
Description: "scope is the scope of the Access List member, it must be equal to the scope of the parent Access List.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
Description: "scope is the scope of the Access List member, it must be equal to the scope of the parent Access List.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.RequiresReplace()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"spec": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
@@ -644,23 +632,6 @@ func CopyAccessListFromTerraform(_ context.Context, tf github_com_hashicorp_terr
}
CopyFromTimestamp(diags, a, &obj.Expires)
}
{
a, ok := tf.Attrs["revision"]
if !ok {
diags.Append(attrReadMissingDiag{"AccessList.header.metadata.revision"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrReadConversionFailureDiag{"AccessList.header.metadata.revision", "github.com/hashicorp/terraform-plugin-framework/types.String"})
} else {
var t string
if !v.Null && !v.Unknown {
t = string(v.Value)
}
obj.Revision = t
}
}
}
}
}
}
@@ -1789,28 +1760,6 @@ func CopyAccessListToTerraform(ctx context.Context, obj *github_com_gravitationa
tf.Attrs["expires"] = v
}
}
{
t, ok := tf.AttrTypes["revision"]
if !ok {
diags.Append(attrWriteMissingDiag{"AccessList.header.metadata.revision"})
} else {
v, ok := tf.Attrs["revision"].(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"AccessList.header.metadata.revision", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"AccessList.header.metadata.revision", "github.com/hashicorp/terraform-plugin-framework/types.String"})
}
v.Null = string(obj.Revision) == ""
}
v.Value = string(obj.Revision)
v.Unknown = false
tf.Attrs["revision"] = v
}
}
}
v.Unknown = false
tf.Attrs["metadata"] = v
@@ -3471,23 +3420,6 @@ func CopyMemberFromTerraform(_ context.Context, tf github_com_hashicorp_terrafor
}
CopyFromTimestamp(diags, a, &obj.Expires)
}
{
a, ok := tf.Attrs["revision"]
if !ok {
diags.Append(attrReadMissingDiag{"Member.header.metadata.revision"})
} else {
v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrReadConversionFailureDiag{"Member.header.metadata.revision", "github.com/hashicorp/terraform-plugin-framework/types.String"})
} else {
var t string
if !v.Null && !v.Unknown {
t = string(v.Value)
}
obj.Revision = t
}
}
}
}
}
}
@@ -3884,28 +3816,6 @@ func CopyMemberToTerraform(ctx context.Context, obj *github_com_gravitational_te
tf.Attrs["expires"] = v
}
}
{
t, ok := tf.AttrTypes["revision"]
if !ok {
diags.Append(attrWriteMissingDiag{"Member.header.metadata.revision"})
} else {
v, ok := tf.Attrs["revision"].(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
i, err := t.ValueFromTerraform(ctx, github_com_hashicorp_terraform_plugin_go_tftypes.NewValue(t.TerraformType(ctx), nil))
if err != nil {
diags.Append(attrWriteGeneralError{"Member.header.metadata.revision", err})
}
v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String)
if !ok {
diags.Append(attrWriteConversionFailureDiag{"Member.header.metadata.revision", "github.com/hashicorp/terraform-plugin-framework/types.String"})
}
v.Null = string(obj.Revision) == ""
}
v.Value = string(obj.Revision)
v.Unknown = false
tf.Attrs["revision"] = v
}
}
}
v.Unknown = false
tf.Attrs["metadata"] = v
@@ -911,9 +911,10 @@ func GenSchemaAppV3(ctx context.Context) (github_com_hashicorp_terraform_plugin_
Optional: true,
},
"scope": {
Description: "The App's scope.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
Description: "The App's scope.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.RequiresReplace()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"spec": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
@@ -4467,9 +4468,10 @@ func GenSchemaKubernetesClusterV3(ctx context.Context) (github_com_hashicorp_ter
Optional: true,
},
"scope": {
Description: "The scope of the kube cluster.",
Optional: true,
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
Description: "The scope of the kube cluster.",
Optional: true,
PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.RequiresReplace()},
Type: github_com_hashicorp_terraform_plugin_framework_types.StringType,
},
"spec": {
Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{
+4 -2
View File
@@ -349,8 +349,10 @@ func NewAuthServer(cfg AuthServerConfig) (*AuthServer, error) {
}
accessLists, err := local.NewAccessListServiceV2(local.AccessListServiceConfig{
Backend: srv.Backend,
Modules: cfg.Modules,
Backend: srv.Backend,
Modules: cfg.Modules,
ScopesFeatures: cfg.ScopesFeatures,
RunWhileLockedRetryInterval: cfg.RunWhileLockedRetryInterval,
})
if err != nil {
return nil, trace.Wrap(err)