diff --git a/api/client/client.go b/api/client/client.go index 9cdc3408ffd50..03c661c3758cc 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -5674,3 +5674,35 @@ func (c *Client) ValidateTrustedCluster( } return resp, nil } + +// GetBot gets the bot with the given name. +func (c *Client) GetBot(ctx context.Context, name string) (*machineidv1pb.Bot, error) { + bot, err := c.BotServiceClient().GetBot(ctx, &machineidv1pb.GetBotRequest{ + BotName: name, + }) + return bot, trace.Wrap(err) +} + +// CreateBot creates the given bot. +func (c *Client) CreateBot(ctx context.Context, bot *machineidv1pb.Bot) (*machineidv1pb.Bot, error) { + bot, err := c.BotServiceClient().CreateBot(ctx, &machineidv1pb.CreateBotRequest{ + Bot: bot, + }) + return bot, trace.Wrap(err) +} + +// DeleteBot deletes the bot with the given name. +func (c *Client) DeleteBot(ctx context.Context, name string) error { + _, err := c.BotServiceClient().DeleteBot(ctx, &machineidv1pb.DeleteBotRequest{ + BotName: name, + }) + return trace.Wrap(err) +} + +// UpsertBot upserts the given bot. +func (c *Client) UpsertBot(ctx context.Context, bot *machineidv1pb.Bot) (*machineidv1pb.Bot, error) { + bot, err := c.BotServiceClient().UpsertBot(ctx, &machineidv1pb.UpsertBotRequest{ + Bot: bot, + }) + return bot, trace.Wrap(err) +} diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/bot.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/bot.mdx new file mode 100644 index 0000000000000..8d48d0e13dca9 --- /dev/null +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/bot.mdx @@ -0,0 +1,61 @@ +--- +title: Reference for the teleport_bot Terraform data-source +sidebar_label: bot +description: This page describes the supported values of the teleport_bot data-source of the Teleport Terraform provider. +tags: + - infrastructure-as-code + - reference + - platform-wide +--- + +{/*Auto-generated file. Do not edit.*/} +{/*To regenerate, navigate to integrations/terraform and run `make docs`.*/} + +{/* Disable the outdated name check since data source fields occasionally need +to refer to these. */} +{/* vale 3rd-party-products.former-names = NO */} + +This page describes the supported values of the `teleport_bot` data source of the +Teleport Terraform provider. + + + + + +{/* schema generated by tfplugindocs */} +## Schema + +### Optional + +- `metadata` (Attributes) Common metadata that all resources share. (see [below for nested schema](#nested-schema-for-metadata)) +- `spec` (Attributes) The configured properties of a Bot. (see [below for nested schema](#nested-schema-for-spec)) +- `status` (Attributes) Fields that are set by the server as results of operations. These should not be modified by users. (see [below for nested schema](#nested-schema-for-status)) +- `sub_kind` (String) Differentiates variations of the same kind. All resources should contain one, even if it is never populated. +- `version` (String) The version of the resource being represented. + +### Nested Schema for `metadata` + +Optional: + +- `description` (String) description is object description. +- `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. +- `name` (String) name is an object name. + + +### Nested Schema for `spec` + +Optional: + +- `max_session_ttl` (String) The max session TTL value for the bot's internal role. Unless specified, bots may not request a value beyond the default maximum TTL of 12 hours. This value may not be larger than 7 days (168 hours). +- `roles` (List of String) The roles that the bot should be able to impersonate. +- `traits` (Map of List of String) The traits that will be associated with the bot for the purposes of role templating. Where multiple specified with the same name, these will be merged by the server. + + +### Nested Schema for `status` + +Optional: + +- `role_name` (String) The name of the role associated with the bot. +- `user_name` (String) The name of the user associated with the bot. + diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/data-sources.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/data-sources.mdx index 4c0a3c46424b8..afa1a485c5812 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/data-sources.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/data-sources/data-sources.mdx @@ -24,6 +24,7 @@ The Teleport Terraform provider supports the following data-sources: - [`teleport_auth_preference`](./auth_preference.mdx) - [`teleport_autoupdate_config`](./autoupdate_config.mdx) - [`teleport_autoupdate_version`](./autoupdate_version.mdx) + - [`teleport_bot`](./bot.mdx) - [`teleport_cluster_maintenance_config`](./cluster_maintenance_config.mdx) - [`teleport_cluster_networking_config`](./cluster_networking_config.mdx) - [`teleport_database`](./database.mdx) diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/bot_v2.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/bot_v2.mdx new file mode 100644 index 0000000000000..32c3a17b9270a --- /dev/null +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/bot_v2.mdx @@ -0,0 +1,105 @@ +--- +title: Reference for the teleport_bot_v2 Terraform resource +sidebar_label: bot_v2 +description: This page describes the supported values of the teleport_bot_v2 resource of the Teleport Terraform provider. +tags: + - infrastructure-as-code + - reference + - platform-wide +--- + +{/*Auto-generated file. Do not edit.*/} +{/*To regenerate, navigate to integrations/terraform and run `make docs`.*/} + +{/* Disable the outdated name check since resource fields occasionally need +to refer to these. */} +{/* vale 3rd-party-products.former-names = NO */} + +This page describes the supported values of the teleport_bot_v2 resource of the Teleport Terraform provider. + + + + +## Example Usage + +```hcl +# Teleport Machine ID Bot creation example + +locals { + bot_name = "example" +} + +resource "random_password" "bot_token" { + length = 32 + special = false +} + +resource "time_offset" "bot_example_token_expiry" { + offset_hours = 1 +} + +resource "teleport_provision_token" "bot_example" { + metadata = { + expires = time_offset.bot_example_token_expiry.rfc3339 + description = "Bot join token for ${local.bot_name} generated by Terraform" + + name = random_password.bot_token.result + } + + spec = { + roles = ["Bot"] + bot_name = local.bot_name + join_method = "token" + } +} + +resource "teleport_bot_v2" "example" { + metadata = { + name = local.bot_name + } + + spec = { + roles = ["access"] + traits = { + "logins" = ["ubuntu", "root"] + } + } +} +``` + +{/* schema generated by tfplugindocs */} +## Schema + +### Optional + +- `metadata` (Attributes) Common metadata that all resources share. (see [below for nested schema](#nested-schema-for-metadata)) +- `spec` (Attributes) The configured properties of a Bot. (see [below for nested schema](#nested-schema-for-spec)) +- `status` (Attributes) Fields that are set by the server as results of operations. These should not be modified by users. (see [below for nested schema](#nested-schema-for-status)) +- `sub_kind` (String) Differentiates variations of the same kind. All resources should contain one, even if it is never populated. +- `version` (String) The version of the resource being represented. + +### Nested Schema for `metadata` + +Optional: + +- `description` (String) description is object description. +- `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. +- `name` (String) name is an object name. + + +### Nested Schema for `spec` + +Optional: + +- `max_session_ttl` (String) The max session TTL value for the bot's internal role. Unless specified, bots may not request a value beyond the default maximum TTL of 12 hours. This value may not be larger than 7 days (168 hours). +- `roles` (List of String) The roles that the bot should be able to impersonate. +- `traits` (Map of List of String) The traits that will be associated with the bot for the purposes of role templating. Where multiple specified with the same name, these will be merged by the server. + + +### Nested Schema for `status` + +Optional: + +- `role_name` (String) The name of the role associated with the bot. +- `user_name` (String) The name of the user associated with the bot. diff --git a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/resources.mdx b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/resources.mdx index 313d8760e9f7b..f5854f04691a3 100644 --- a/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/resources.mdx +++ b/docs/pages/reference/infrastructure-as-code/terraform-provider/resources/resources.mdx @@ -25,6 +25,7 @@ The Teleport Terraform provider supports the following resources: - [`teleport_autoupdate_config`](./autoupdate_config.mdx) - [`teleport_autoupdate_version`](./autoupdate_version.mdx) - [`teleport_bot`](./bot.mdx) + - [`teleport_bot_v2`](./bot_v2.mdx) - [`teleport_cluster_maintenance_config`](./cluster_maintenance_config.mdx) - [`teleport_cluster_networking_config`](./cluster_networking_config.mdx) - [`teleport_database`](./database.mdx) diff --git a/integrations/terraform/Makefile b/integrations/terraform/Makefile index bdefc6127ff6b..9296f35dd1369 100644 --- a/integrations/terraform/Makefile +++ b/integrations/terraform/Makefile @@ -142,6 +142,13 @@ endif --terraform_out=config=protoc-gen-terraform-healthcheckconfig.yaml:./tfschema \ teleport/healthcheckconfig/v1/health_check_config.proto + @protoc \ + -I=../../api/proto \ + -I=$(PROTOBUF_MOD_PATH) \ + --plugin=$(PROTOC_GEN_TERRAFORM) \ + --terraform_out=config=protoc-gen-terraform-machineid.yaml:./tfschema \ + teleport/machineid/v1/bot.proto + mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/loginrule/v1/loginrule_terraform.go ./tfschema/loginrule/v1/ mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/accesslist/v1/accesslist_terraform.go ./tfschema/accesslist/v1/ mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1/access_monitoring_rules_terraform.go ./tfschema/accessmonitoringrules/v1/ @@ -150,6 +157,7 @@ endif mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/autoupdate/v1/autoupdate_terraform.go ./tfschema/autoupdate/v1/ mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1/health_check_config_terraform.go ./tfschema/healthcheckconfig/v1/ mv ./tfschema/github.com/gravitational/teleport/api/types/device_terraform.go ./tfschema/devicetrust/v1/ + mv ./tfschema/github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1/bot_terraform.go ./tfschema/machineid/v1/ rm -r ./tfschema/github.com/ @go run ./gen/main.go diff --git a/integrations/terraform/examples/resources/teleport_bot_v2/resource.tf b/integrations/terraform/examples/resources/teleport_bot_v2/resource.tf new file mode 100644 index 0000000000000..6739bedc2db5d --- /dev/null +++ b/integrations/terraform/examples/resources/teleport_bot_v2/resource.tf @@ -0,0 +1,42 @@ +# Teleport Machine ID Bot creation example + +locals { + bot_name = "example" +} + +resource "random_password" "bot_token" { + length = 32 + special = false +} + +resource "time_offset" "bot_example_token_expiry" { + offset_hours = 1 +} + +resource "teleport_provision_token" "bot_example" { + metadata = { + expires = time_offset.bot_example_token_expiry.rfc3339 + description = "Bot join token for ${local.bot_name} generated by Terraform" + + name = random_password.bot_token.result + } + + spec = { + roles = ["Bot"] + bot_name = local.bot_name + join_method = "token" + } +} + +resource "teleport_bot_v2" "example" { + metadata = { + name = local.bot_name + } + + spec = { + roles = ["access"] + traits = { + "logins" = ["ubuntu", "root"] + } + } +} diff --git a/integrations/terraform/gen/main.go b/integrations/terraform/gen/main.go index 962e2f9032ae3..d74dd0f09875b 100644 --- a/integrations/terraform/gen/main.go +++ b/integrations/terraform/gen/main.go @@ -665,6 +665,31 @@ var ( ExtraImports: []string{"apitypes \"github.com/gravitational/teleport/api/types\""}, ForceSetKind: "apitypes.KindHealthCheckConfig", } + + bot = payload{ + Name: "Bot", + TypeName: "Bot", + VarName: "bot", + GetMethod: "GetBot", + CreateMethod: "CreateBot", + UpsertMethodArity: 2, + UpdateMethod: "UpsertBot", + DeleteMethod: "DeleteBot", + ID: "bot.Metadata.Name", + Kind: "bot", + HasStaticID: false, + ProtoPackage: "machineidv1", + ProtoPackagePath: "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1", + SchemaPackage: "schemav1", + SchemaPackagePath: "github.com/gravitational/teleport/integrations/terraform/tfschema/machineid/v1", + TerraformResourceType: "teleport_bot_v2", + // Since [RFD 153](https://github.com/gravitational/teleport/blob/master/rfd/0153-resource-guidelines.md) + // resources are plain structs + IsPlainStruct: true, + // As 153-style resources don't have CheckAndSetDefaults, we must set the Kind manually. + // We import the package containing kinds, then use ForceSetKind. + ForceSetKind: `"bot"`, + } ) func main() { @@ -726,6 +751,8 @@ func genTFSchema() { generateDataSource(autoUpdateConfig, singularDataSource) generateResource(healthCheckConfig, pluralResource) generateDataSource(healthCheckConfig, pluralDataSource) + generateResource(bot, pluralResource) + generateDataSource(bot, pluralDataSource) } func generateResource(p payload, tpl string) { diff --git a/integrations/terraform/protoc-gen-terraform-machineid.yaml b/integrations/terraform/protoc-gen-terraform-machineid.yaml new file mode 100644 index 0000000000000..58f8d0203fbb9 --- /dev/null +++ b/integrations/terraform/protoc-gen-terraform-machineid.yaml @@ -0,0 +1,69 @@ +--- +target_package_name: "v1" +default_package_name: "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" +duration_custom_type: Duration +use_state_for_unknown_by_default: true + +# Top-level type names to export +types: + - "Bot" + +# These import paths were not being automatically picked up by +# protoc-gen-terraform without these overrides +import_path_overrides: + "types": "github.com/gravitational/teleport/api/types" + "wrappers": "github.com/gravitational/teleport/api/types/wrappers" + "durationpb": "google.golang.org/protobuf/types/known/durationpb" + "timestamppb": "google.golang.org/protobuf/types/known/timestamppb" + "structpb": "google.golang.org/protobuf/types/known/structpb" + "v1": "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + "v11": "github.com/gravitational/teleport/api/gen/proto/go/teleport/label/v1" + "github_com_gravitational_teleport_integrations_terraform_tfschema": "github.com/gravitational/teleport/integrations/terraform/tfschema" + +# id field is required for integration tests. It is not used by provider. +# We have to add it manually (might be removed in the future versions). +injected_fields: + Bot: + - name: id + type: github.com/hashicorp/terraform-plugin-framework/types.StringType + computed: true + plan_modifiers: + - "github.com/hashicorp/terraform-plugin-framework/tfsdk.UseStateForUnknown()" + +# These fields will be excluded +exclude_fields: + # Metadata (we id resources by name on our side) + - "Bot.metadata.id" + +# These fields will be marked as Computed: true +computed_fields: + # Metadata + - "Bot.metadata.namespace" + - "Bot.kind" + - "Bot.status" + +# These fields will be marked as Required: true +required_fields: [] + +plan_modifiers: + # Force to recreate resource if it's name changes + Metadata.name: + - "github.com/hashicorp/terraform-plugin-framework/tfsdk.RequiresReplace()" + +# This must be defined for the generator to be happy, but in reality all time +# fields are overridden (because the protobuf timestamps contain locks and the +# linter gets mad if we use raw structs instead of pointers). +time_type: + type: "PlaceholderType" +duration_type: + type: "PlaceholderType" + +validators: + # Expires must be in the future + Metadata.expires: + - github_com_gravitational_teleport_integrations_terraform_tfschema.MustTimeBeInFuture() + +custom_types: + "Bot.metadata.expires": Timestamp + "Bot.spec.max_session_ttl": Duration + "Bot.spec.traits": TraitsMap diff --git a/integrations/terraform/provider/data_source_teleport_bot_v2.go b/integrations/terraform/provider/data_source_teleport_bot_v2.go new file mode 100755 index 0000000000000..038388cc80567 --- /dev/null +++ b/integrations/terraform/provider/data_source_teleport_bot_v2.go @@ -0,0 +1,96 @@ +// 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 provider + +import ( + "context" + + + "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/machineid/v1" +) + +// dataSourceTeleportBotType is the data source metadata type +type dataSourceTeleportBotType struct{} + +// dataSourceTeleportBot is the resource +type dataSourceTeleportBot struct { + p Provider +} + +// GetSchema returns the data source schema +func (r dataSourceTeleportBotType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return schemav1.GenSchemaBot(ctx) +} + +// NewDataSource creates the empty data source +func (r dataSourceTeleportBotType) NewDataSource(_ context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { + return dataSourceTeleportBot{ + p: *(p.(*Provider)), + }, nil +} + +// Read reads teleport Bot +func (r dataSourceTeleportBot) 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 + } + + botI, err := r.p.Client.GetBot(ctx, id.Value) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(err), "bot")) + 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 + } + + bot := botI + + diags = schemav1.CopyBotToTerraform(ctx, bot, &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 + } +} diff --git a/integrations/terraform/provider/provider.go b/integrations/terraform/provider/provider.go index 3cab362453fd7..d704ee58c5c83 100644 --- a/integrations/terraform/provider/provider.go +++ b/integrations/terraform/provider/provider.go @@ -543,7 +543,8 @@ func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp "teleport_session_recording_config": resourceTeleportSessionRecordingConfigType{}, "teleport_trusted_cluster": resourceTeleportTrustedClusterType{}, "teleport_user": resourceTeleportUserType{}, - "teleport_bot": resourceTeleportBotType{}, + "teleport_bot": resourceTeleportBotLegacyType{}, + "teleport_bot_v2": resourceTeleportBotType{}, "teleport_login_rule": resourceTeleportLoginRuleType{}, "teleport_trusted_device": resourceTeleportDeviceV1Type{}, "teleport_okta_import_rule": resourceTeleportOktaImportRuleType{}, @@ -565,6 +566,7 @@ func (p *Provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc return map[string]tfsdk.DataSourceType{ "teleport_app": dataSourceTeleportAppType{}, "teleport_auth_preference": dataSourceTeleportAuthPreferenceType{}, + "teleport_bot": dataSourceTeleportBotType{}, "teleport_cluster_maintenance_config": dataSourceTeleportClusterMaintenanceConfigType{}, "teleport_cluster_networking_config": dataSourceTeleportClusterNetworkingConfigType{}, "teleport_database": dataSourceTeleportDatabaseType{}, diff --git a/integrations/terraform/provider/resource_teleport_bot.go b/integrations/terraform/provider/resource_teleport_bot.go index 80910de555fa7..91a8159e48b51 100644 --- a/integrations/terraform/provider/resource_teleport_bot.go +++ b/integrations/terraform/provider/resource_teleport_bot.go @@ -34,8 +34,13 @@ import ( "github.com/gravitational/teleport/integrations/terraform/tfschema" ) -func GenSchemaBot(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +const legacyBotDeprecationMessage = "The `teleport_bot` resource is deprecated and has been replaced by `teleport_bot_v2`. " + + "To migrate to the new resource type, import your bot with `terraform import teleport_bot_v2.name {{bot-name}}` " + + "and remove the old resource from your state with `terraform state rm teleport_bot.name`." + +func GenSchemaBotLegacy(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ + DeprecationMessage: legacyBotDeprecationMessage, Attributes: map[string]tfsdk.Attribute{ "id": { Type: types.StringType, @@ -109,29 +114,29 @@ type Bot struct { RoleName types.String `tfsdk:"role_name"` } -// resourceTeleportBotType is the resource metadata type -type resourceTeleportBotType struct{} +// resourceTeleportBotLegacyType is the resource metadata type +type resourceTeleportBotLegacyType struct{} // GetSchema returns the resource schema -func (r resourceTeleportBotType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (r resourceTeleportBotLegacyType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { // It's unusual for this provider, but we'll hand-write the schema here as // bots do not have any server-side resources of their own. - return GenSchemaBot(ctx) + return GenSchemaBotLegacy(ctx) } // NewResource creates the empty resource -func (r resourceTeleportBotType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceTeleportBot{ +func (r resourceTeleportBotLegacyType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourceTeleportBotLegacy{ p: *(p.(*Provider)), }, nil } -// resourceTeleportBot is the resource -type resourceTeleportBot struct { +// resourceTeleportBotLegacy is the resource +type resourceTeleportBotLegacy struct { p Provider } -func (r resourceTeleportBot) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { +func (r resourceTeleportBotLegacy) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { if !r.p.IsConfigured(resp.Diagnostics) { return } @@ -205,7 +210,7 @@ func (r resourceTeleportBot) Create(ctx context.Context, req tfsdk.CreateResourc } } -func (r resourceTeleportBot) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r resourceTeleportBotLegacy) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { // Not much to do here: bots are currently immutable. We'll just check for // deletion. @@ -228,20 +233,20 @@ func (r resourceTeleportBot) Read(ctx context.Context, req tfsdk.ReadResourceReq } } -func (r resourceTeleportBot) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r resourceTeleportBotLegacy) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { // Nothing to do here: bots are currently immutable. In the future we'd // ideally want to add specific RPCs for desired mutable attributes, e.g. // UpdateBotRoles(), UpdateBotToken(), etc. } -func (r resourceTeleportBot) ModifyPlan(ctx context.Context, req tfsdk.ModifyResourcePlanRequest, resp *tfsdk.ModifyResourcePlanResponse) { +func (r resourceTeleportBotLegacy) ModifyPlan(ctx context.Context, req tfsdk.ModifyResourcePlanRequest, resp *tfsdk.ModifyResourcePlanResponse) { // Add .traits to RequiresReplace to ensure changes to this field trigger a // replacement. We can't set it in the schema as the attribute is generated // by a helper method. resp.RequiresReplace = append(resp.RequiresReplace, path.Root("traits")) } -func (r resourceTeleportBot) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r resourceTeleportBotLegacy) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { var name types.String diags := req.State.GetAttribute(ctx, path.Root("name"), &name) resp.Diagnostics.Append(diags...) diff --git a/integrations/terraform/provider/resource_teleport_bot_v2.go b/integrations/terraform/provider/resource_teleport_bot_v2.go new file mode 100755 index 0000000000000..340dff48182aa --- /dev/null +++ b/integrations/terraform/provider/resource_teleport_bot_v2.go @@ -0,0 +1,338 @@ +// 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 provider + +import ( + "context" + "fmt" + + machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" + + "github.com/gravitational/teleport/api/utils/retryutils" + "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/machineid/v1" +) + +// resourceTeleportBotType is the resource metadata type +type resourceTeleportBotType struct{} + +// resourceTeleportBot is the resource +type resourceTeleportBot struct { + p Provider +} + +// GetSchema returns the resource schema +func (r resourceTeleportBotType) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { + return schemav1.GenSchemaBot(ctx) +} + +// NewResource creates the empty resource +func (r resourceTeleportBotType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourceTeleportBot{ + p: *(p.(*Provider)), + }, nil +} + +// Create creates the Bot +func (r resourceTeleportBot) 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 + } + + bot := &machineidv1.Bot{} + diags = schemav1.CopyBotFromTerraform(ctx, plan, bot) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + + botResource := bot + + botResource.Kind = "bot" + + id := botResource.Metadata.Name + + _, err = r.p.Client.GetBot(ctx, id) + if !trace.IsNotFound(err) { + if err == nil { + existErr := fmt.Sprintf("Bot exists in Teleport. Either remove it (tctl rm bot/%v)"+ + " or import it to the existing state (terraform import teleport_bot_v2.%v %v)", id, id, id) + + resp.Diagnostics.Append(diagFromErr("Bot exists in Teleport", trace.Errorf(existErr))) + return + } + + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(err), "bot")) + return + } + + _, err = r.p.Client.CreateBot(ctx, botResource) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error creating Bot", trace.Wrap(err), "bot")) + return + } + var botI *machineidv1.Bot + // Try getting the resource until it exists. + tries := 0 + retry, err := retryutils.NewRetryV2(retryutils.RetryV2Config{ + Driver: retryutils.NewExponentialDriver(r.p.RetryConfig.Base), + First: r.p.RetryConfig.Base, + Max: r.p.RetryConfig.Cap, + Jitter: retryutils.HalfJitter, + }) + if err != nil { + return + } + for { + tries = tries + 1 + botI, err = r.p.Client.GetBot(ctx, id) + if trace.IsNotFound(err) { + select { + case <-ctx.Done(): + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(ctx.Err()), "bot")) + return + case <-retry.After(): + } + if tries >= r.p.RetryConfig.MaxTries { + diagMessage := fmt.Sprintf("Error reading Bot (tried %d times) - state outdated, please import resource", tries) + resp.Diagnostics.AddError(diagMessage, "bot") + return + } + continue + } + break + } + + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(err), "bot")) + return + } + + botResource = botI + + bot = botResource + + diags = schemav1.CopyBotToTerraform(ctx, bot, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + plan.Attrs["id"] = types.String{Value: bot.Metadata.Name} + + diags = resp.State.Set(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read reads teleport Bot +func (r resourceTeleportBot) 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 + } + + botI, err := r.p.Client.GetBot(ctx, id.Value) + if trace.IsNotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(err), "bot")) + return + } + bot := botI + diags = schemav1.CopyBotToTerraform(ctx, bot, &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 Bot +func (r resourceTeleportBot) 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 + } + + bot := &machineidv1.Bot{} + diags = schemav1.CopyBotFromTerraform(ctx, plan, bot) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + botResource := bot + + + + name := botResource.Metadata.Name + + botBefore, err := r.p.Client.GetBot(ctx, name) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", err, "bot")) + return + } + + _, err = r.p.Client.UpsertBot(ctx, botResource) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error updating Bot", err, "bot")) + return + } + var botI *machineidv1.Bot + + tries := 0 + retry, err := retryutils.NewRetryV2(retryutils.RetryV2Config{ + Driver: retryutils.NewExponentialDriver(r.p.RetryConfig.Base), + First: r.p.RetryConfig.Base, + Max: r.p.RetryConfig.Cap, + Jitter: retryutils.HalfJitter, + }) + if err != nil { + return + } + for { + tries = tries + 1 + botI, err = r.p.Client.GetBot(ctx, name) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", err, "bot")) + return + } + if botBefore.GetMetadata().Revision != botI.GetMetadata().Revision || false { + break + } + + select { + case <-ctx.Done(): + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(ctx.Err()), "bot")) + return + case <-retry.After(): + } + if tries >= r.p.RetryConfig.MaxTries { + diagMessage := fmt.Sprintf("Error reading Bot (tried %d times) - state outdated, please import resource", tries) + resp.Diagnostics.AddError(diagMessage, "bot") + return + } + } + + botResource = botI + + diags = schemav1.CopyBotToTerraform(ctx, bot, &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 Bot +func (r resourceTeleportBot) 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.DeleteBot(ctx, id.Value) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error deleting Bot", trace.Wrap(err), "bot")) + return + } + + resp.State.RemoveResource(ctx) +} + +// ImportState imports Bot state +func (r resourceTeleportBot) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + bot, err := r.p.Client.GetBot(ctx, req.ID) + if err != nil { + resp.Diagnostics.Append(diagFromWrappedErr("Error reading Bot", trace.Wrap(err), "bot")) + return + } + + botResource := bot + + + var state types.Object + + diags := resp.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = schemav1.CopyBotToTerraform(ctx, botResource, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + id := bot.Metadata.Name + + state.Attrs["id"] = types.String{Value: id} + + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/integrations/terraform/testlib/bot_test.go b/integrations/terraform/testlib/bot_test.go index f83b995feef3e..95878de0b31e6 100644 --- a/integrations/terraform/testlib/bot_test.go +++ b/integrations/terraform/testlib/bot_test.go @@ -22,6 +22,10 @@ import ( "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" + + headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" ) func (s *TerraformSuiteOSS) TestBot() { @@ -96,3 +100,121 @@ func (s *TerraformSuiteOSS) TestBot() { }) } + +func (s *TerraformSuiteOSS) TestBotV2() { + ctx, cancel := context.WithCancel(context.Background()) + s.T().Cleanup(cancel) + + checkResourcesDestroyed := func(state *terraform.State) error { + var errs []error + if _, err := s.client.GetToken(ctx, "bot-test"); err != nil { + if !trace.IsNotFound(err) { + errs = append(errs, err) + } + } + + if _, err := s.client.GetUser(ctx, "bot-test", false); err != nil { + if !trace.IsNotFound(err) { + errs = append(errs, err) + } + } + + return trace.NewAggregate(errs...) + } + + tokenName := "teleport_provision_token.bot_test" + botName := "teleport_bot_v2.test" + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + CheckDestroy: checkResourcesDestroyed, + IsUnitTest: true, + Steps: []resource.TestStep{ + { + Config: s.getFixture("bot_v2_0_create.tf"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(tokenName, "kind", "token"), + resource.TestCheckResourceAttr(tokenName, "metadata.name", "bot-test"), + resource.TestCheckResourceAttr(tokenName, "spec.roles.0", "Bot"), + resource.TestCheckResourceAttr(botName, "metadata.name", "test"), + resource.TestCheckResourceAttr(botName, "status.user_name", "bot-test"), + resource.TestCheckResourceAttr(botName, "status.role_name", "bot-test"), + resource.TestCheckResourceAttr(botName, "spec.roles.0", "terraform"), + resource.TestCheckNoResourceAttr(botName, "spec.traits.logins1"), + ), + }, + { + Config: s.getFixture("bot_v2_0_create.tf"), + PlanOnly: true, + }, + { + Config: s.getFixture("bot_v2_1_update.tf"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(tokenName, "kind", "token"), + resource.TestCheckResourceAttr(tokenName, "metadata.name", "bot-test"), + resource.TestCheckResourceAttr(tokenName, "spec.roles.0", "Bot"), + resource.TestCheckResourceAttr(botName, "metadata.name", "test"), + resource.TestCheckResourceAttr(botName, "status.user_name", "bot-test"), + resource.TestCheckResourceAttr(botName, "status.role_name", "bot-test"), + resource.TestCheckResourceAttr(botName, "spec.roles.0", "terraform"), + resource.TestCheckResourceAttr(botName, "spec.traits.logins1.0", "a"), + resource.TestCheckResourceAttr(botName, "spec.traits.logins1.1", "b"), + resource.TestCheckResourceAttr(botName, "spec.traits.logins2.0", "c"), + resource.TestCheckResourceAttr(botName, "spec.traits.logins2.1", "d"), + ), + }, + { + Config: s.getFixture("bot_v2_1_update.tf"), + PlanOnly: true, + }, + }, + }) +} + +func (s *TerraformSuiteOSS) TestImportBotV2() { + ctx, cancel := context.WithCancel(context.Background()) + s.T().Cleanup(cancel) + + resourceType := "teleport_bot_v2" + botName := "test" + name := resourceType + "." + botName + + bot := &machineidv1.Bot{ + Metadata: &headerv1.Metadata{ + Name: botName, + }, + Spec: &machineidv1.BotSpec{ + Roles: []string{"terraform"}, + Traits: []*machineidv1.Trait{ + { + Name: "logins1", + Values: []string{"a", "b"}, + }, + { + Name: "logins2", + Values: []string{"c", "d"}, + }, + }, + }, + } + + _, err := s.client.UpsertBot(ctx, bot) + require.NoError(s.T(), err) + + resource.Test(s.T(), resource.TestCase{ + ProtoV6ProviderFactories: s.terraformProviders, + Steps: []resource.TestStep{ + { + Config: s.terraformConfig + "\n" + `resource "` + resourceType + `" "` + botName + `" { }`, + ResourceName: name, + ImportState: true, + ImportStateId: botName, + ImportStateCheck: func(state []*terraform.InstanceState) error { + require.Equal(s.T(), "bot", state[0].Attributes["kind"]) + require.Equal(s.T(), botName, state[0].Attributes["metadata.name"]) + + return nil + }, + }, + }, + }) +} diff --git a/integrations/terraform/testlib/fixtures/bot_v2_0_create.tf b/integrations/terraform/testlib/fixtures/bot_v2_0_create.tf new file mode 100644 index 0000000000000..b2259d5efb26a --- /dev/null +++ b/integrations/terraform/testlib/fixtures/bot_v2_0_create.tf @@ -0,0 +1,31 @@ +locals { + bot_name = "test" +} + +resource "teleport_provision_token" "bot_test" { + version = "v2" + + metadata = { + expires = "2038-01-01T00:00:00Z" + name = "bot-test" + } + + spec = { + roles = ["Bot"] + bot_name = local.bot_name + join_method = "token" + } +} + +resource "teleport_bot_v2" "test" { + version = "v1" + + metadata = { + expires = "2038-01-01T00:00:00Z" + name = local.bot_name + } + + spec = { + roles = ["terraform"] + } +} diff --git a/integrations/terraform/testlib/fixtures/bot_v2_1_update.tf b/integrations/terraform/testlib/fixtures/bot_v2_1_update.tf new file mode 100644 index 0000000000000..26768a1e2ba27 --- /dev/null +++ b/integrations/terraform/testlib/fixtures/bot_v2_1_update.tf @@ -0,0 +1,35 @@ +locals { + bot_name = "test" +} + +resource "teleport_provision_token" "bot_test" { + version = "v2" + metadata = { + expires = "2038-01-01T00:00:00Z" + name = "bot-test" + } + + spec = { + roles = ["Bot"] + bot_name = local.bot_name + join_method = "token" + } +} + +resource "teleport_bot_v2" "test" { + version = "v1" + + metadata = { + expires = "2038-01-01T00:00:00Z" + name = local.bot_name + } + + spec = { + roles = ["terraform"] + + traits = { + "logins1" = ["a", "b"], + "logins2" = ["c", "d"], + } + } +} diff --git a/integrations/terraform/tfschema/machineid/v1/bot_terraform.go b/integrations/terraform/tfschema/machineid/v1/bot_terraform.go new file mode 100644 index 0000000000000..b03679f64442b --- /dev/null +++ b/integrations/terraform/tfschema/machineid/v1/bot_terraform.go @@ -0,0 +1,979 @@ +/* +Copyright 2015-2022 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. +*/ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: teleport/machineid/v1/bot.proto + +package v1 + +import ( + context "context" + fmt "fmt" + math "math" + + proto "github.com/gogo/protobuf/proto" + _ "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + github_com_gravitational_teleport_api_gen_proto_go_teleport_header_v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1" + github_com_gravitational_teleport_api_gen_proto_go_teleport_machineid_v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" + github_com_gravitational_teleport_integrations_terraform_tfschema "github.com/gravitational/teleport/integrations/terraform/tfschema" + github_com_hashicorp_terraform_plugin_framework_attr "github.com/hashicorp/terraform-plugin-framework/attr" + github_com_hashicorp_terraform_plugin_framework_diag "github.com/hashicorp/terraform-plugin-framework/diag" + github_com_hashicorp_terraform_plugin_framework_tfsdk "github.com/hashicorp/terraform-plugin-framework/tfsdk" + github_com_hashicorp_terraform_plugin_framework_types "github.com/hashicorp/terraform-plugin-framework/types" + github_com_hashicorp_terraform_plugin_go_tftypes "github.com/hashicorp/terraform-plugin-go/tftypes" + _ "google.golang.org/protobuf/types/known/durationpb" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// GenSchemaBot returns tfsdk.Schema definition for Bot +func GenSchemaBot(ctx context.Context) (github_com_hashicorp_terraform_plugin_framework_tfsdk.Schema, github_com_hashicorp_terraform_plugin_framework_diag.Diagnostics) { + return github_com_hashicorp_terraform_plugin_framework_tfsdk.Schema{Attributes: map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "id": { + Computed: true, + Optional: false, + PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()}, + Required: false, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "kind": { + Computed: true, + Description: "The kind of resource represented.", + 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, + }, + "metadata": { + Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "description": { + Description: "description is object description.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "expires": GenSchemaTimestamp(ctx, github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + Description: "expires is a global expiry time header can be set on any resource in the system.", + Optional: true, + Validators: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributeValidator{github_com_gravitational_teleport_integrations_terraform_tfschema.MustTimeBeInFuture()}, + }), + "labels": { + Description: "labels is a set of labels.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.MapType{ElemType: github_com_hashicorp_terraform_plugin_framework_types.StringType}, + }, + "name": { + Description: "name is an object name.", + 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, + }, + "namespace": { + Computed: true, + Description: "namespace is object namespace. The field should be called \"namespace\" when it returns in Teleport 2.4.", + 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, + }, + "revision": { + 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, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + }), + Description: "Common metadata that all resources share.", + Optional: true, + }, + "spec": { + Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "max_session_ttl": GenSchemaDuration(ctx, github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + Description: "The max session TTL value for the bot's internal role. Unless specified, bots may not request a value beyond the default maximum TTL of 12 hours. This value may not be larger than 7 days (168 hours).", + Optional: true, + }), + "roles": { + Description: "The roles that the bot should be able to impersonate.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.ListType{ElemType: github_com_hashicorp_terraform_plugin_framework_types.StringType}, + }, + "traits": GenSchemaTraitsMap(ctx, github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + Description: "The traits that will be associated with the bot for the purposes of role templating. Where multiple specified with the same name, these will be merged by the server.", + Optional: true, + }), + }), + Description: "The configured properties of a Bot.", + Optional: true, + }, + "status": { + Attributes: github_com_hashicorp_terraform_plugin_framework_tfsdk.SingleNestedAttributes(map[string]github_com_hashicorp_terraform_plugin_framework_tfsdk.Attribute{ + "role_name": { + Description: "The name of the role associated with the bot.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "user_name": { + Description: "The name of the user associated with the bot.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + }), + Computed: true, + Description: "Fields that are set by the server as results of operations. These should not be modified by users.", + Optional: true, + PlanModifiers: []github_com_hashicorp_terraform_plugin_framework_tfsdk.AttributePlanModifier{github_com_hashicorp_terraform_plugin_framework_tfsdk.UseStateForUnknown()}, + }, + "sub_kind": { + Description: "Differentiates variations of the same kind. All resources should contain one, even if it is never populated.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + "version": { + Description: "The version of the resource being represented.", + Optional: true, + Type: github_com_hashicorp_terraform_plugin_framework_types.StringType, + }, + }}, nil +} + +// CopyBotFromTerraform copies contents of the source Terraform object into a target struct +func CopyBotFromTerraform(_ context.Context, tf github_com_hashicorp_terraform_plugin_framework_types.Object, obj *github_com_gravitational_teleport_api_gen_proto_go_teleport_machineid_v1.Bot) github_com_hashicorp_terraform_plugin_framework_diag.Diagnostics { + var diags github_com_hashicorp_terraform_plugin_framework_diag.Diagnostics + { + a, ok := tf.Attrs["kind"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.kind"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.kind", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Kind = t + } + } + } + { + a, ok := tf.Attrs["sub_kind"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.sub_kind"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.sub_kind", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.SubKind = t + } + } + } + { + a, ok := tf.Attrs["version"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.version"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.version", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Version = t + } + } + } + { + a, ok := tf.Attrs["metadata"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata", "github.com/hashicorp/terraform-plugin-framework/types.Object"}) + } else { + obj.Metadata = nil + if !v.Null && !v.Unknown { + tf := v + obj.Metadata = &github_com_gravitational_teleport_api_gen_proto_go_teleport_header_v1.Metadata{} + obj := obj.Metadata + { + a, ok := tf.Attrs["name"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.name"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata.name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Name = t + } + } + } + { + a, ok := tf.Attrs["namespace"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.namespace"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata.namespace", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Namespace = t + } + } + } + { + a, ok := tf.Attrs["description"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.description"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata.description", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Description = t + } + } + } + { + a, ok := tf.Attrs["labels"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.labels"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Map) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata.labels", "github.com/hashicorp/terraform-plugin-framework/types.Map"}) + } else { + obj.Labels = make(map[string]string, len(v.Elems)) + if !v.Null && !v.Unknown { + for k, a := range v.Elems { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.metadata.labels", "github_com_hashicorp_terraform_plugin_framework_types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Labels[k] = t + } + } + } + } + } + } + { + a, ok := tf.Attrs["expires"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.expires"}) + } + CopyFromTimestamp(diags, a, &obj.Expires) + } + { + a, ok := tf.Attrs["revision"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.metadata.revision"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.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 + } + } + } + } + } + } + } + { + a, ok := tf.Attrs["spec"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.spec"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.spec", "github.com/hashicorp/terraform-plugin-framework/types.Object"}) + } else { + obj.Spec = nil + if !v.Null && !v.Unknown { + tf := v + obj.Spec = &github_com_gravitational_teleport_api_gen_proto_go_teleport_machineid_v1.BotSpec{} + obj := obj.Spec + { + a, ok := tf.Attrs["roles"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.spec.roles"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.List) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.spec.roles", "github.com/hashicorp/terraform-plugin-framework/types.List"}) + } else { + obj.Roles = make([]string, len(v.Elems)) + if !v.Null && !v.Unknown { + for k, a := range v.Elems { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.spec.roles", "github_com_hashicorp_terraform_plugin_framework_types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.Roles[k] = t + } + } + } + } + } + } + { + a, ok := tf.Attrs["traits"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.spec.traits"}) + } + CopyFromTraitsMap(diags, a, &obj.Traits) + } + { + a, ok := tf.Attrs["max_session_ttl"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.spec.max_session_ttl"}) + } + CopyFromDuration(diags, a, &obj.MaxSessionTtl) + } + } + } + } + } + { + a, ok := tf.Attrs["status"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.status"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.status", "github.com/hashicorp/terraform-plugin-framework/types.Object"}) + } else { + obj.Status = nil + if !v.Null && !v.Unknown { + tf := v + obj.Status = &github_com_gravitational_teleport_api_gen_proto_go_teleport_machineid_v1.BotStatus{} + obj := obj.Status + { + a, ok := tf.Attrs["user_name"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.status.user_name"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.status.user_name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.UserName = t + } + } + } + { + a, ok := tf.Attrs["role_name"] + if !ok { + diags.Append(attrReadMissingDiag{"Bot.status.role_name"}) + } else { + v, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrReadConversionFailureDiag{"Bot.status.role_name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } else { + var t string + if !v.Null && !v.Unknown { + t = string(v.Value) + } + obj.RoleName = t + } + } + } + } + } + } + } + return diags +} + +// CopyBotToTerraform copies contents of the source Terraform object into a target struct +func CopyBotToTerraform(ctx context.Context, obj *github_com_gravitational_teleport_api_gen_proto_go_teleport_machineid_v1.Bot, tf *github_com_hashicorp_terraform_plugin_framework_types.Object) github_com_hashicorp_terraform_plugin_framework_diag.Diagnostics { + var diags github_com_hashicorp_terraform_plugin_framework_diag.Diagnostics + tf.Null = false + tf.Unknown = false + if tf.Attrs == nil { + tf.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value) + } + { + t, ok := tf.AttrTypes["kind"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.kind"}) + } else { + v, ok := tf.Attrs["kind"].(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{"Bot.kind", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.kind", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Kind) == "" + } + v.Value = string(obj.Kind) + v.Unknown = false + tf.Attrs["kind"] = v + } + } + { + t, ok := tf.AttrTypes["sub_kind"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.sub_kind"}) + } else { + v, ok := tf.Attrs["sub_kind"].(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{"Bot.sub_kind", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.sub_kind", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.SubKind) == "" + } + v.Value = string(obj.SubKind) + v.Unknown = false + tf.Attrs["sub_kind"] = v + } + } + { + t, ok := tf.AttrTypes["version"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.version"}) + } else { + v, ok := tf.Attrs["version"].(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{"Bot.version", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.version", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Version) == "" + } + v.Value = string(obj.Version) + v.Unknown = false + tf.Attrs["version"] = v + } + } + { + a, ok := tf.AttrTypes["metadata"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata"}) + } else { + o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"}) + } else { + v, ok := tf.Attrs["metadata"].(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + v = github_com_hashicorp_terraform_plugin_framework_types.Object{ + + AttrTypes: o.AttrTypes, + Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)), + } + } else { + if v.Attrs == nil { + v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes)) + } + } + if obj.Metadata == nil { + v.Null = true + } else { + obj := obj.Metadata + tf := &v + { + t, ok := tf.AttrTypes["name"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata.name"}) + } else { + v, ok := tf.Attrs["name"].(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{"Bot.metadata.name", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata.name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Name) == "" + } + v.Value = string(obj.Name) + v.Unknown = false + tf.Attrs["name"] = v + } + } + { + t, ok := tf.AttrTypes["namespace"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata.namespace"}) + } else { + v, ok := tf.Attrs["namespace"].(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{"Bot.metadata.namespace", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata.namespace", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Namespace) == "" + } + v.Value = string(obj.Namespace) + v.Unknown = false + tf.Attrs["namespace"] = v + } + } + { + t, ok := tf.AttrTypes["description"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata.description"}) + } else { + v, ok := tf.Attrs["description"].(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{"Bot.metadata.description", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata.description", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.Description) == "" + } + v.Value = string(obj.Description) + v.Unknown = false + tf.Attrs["description"] = v + } + } + { + a, ok := tf.AttrTypes["labels"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata.labels"}) + } else { + o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.MapType) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata.labels", "github.com/hashicorp/terraform-plugin-framework/types.MapType"}) + } else { + c, ok := tf.Attrs["labels"].(github_com_hashicorp_terraform_plugin_framework_types.Map) + if !ok { + c = github_com_hashicorp_terraform_plugin_framework_types.Map{ + + ElemType: o.ElemType, + Elems: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.Labels)), + Null: true, + } + } else { + if c.Elems == nil { + c.Elems = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.Labels)) + } + } + if obj.Labels != nil { + t := o.ElemType + for k, a := range obj.Labels { + v, ok := tf.Attrs["labels"].(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{"Bot.metadata.labels", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.metadata.labels", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = false + } + v.Value = string(a) + v.Unknown = false + c.Elems[k] = v + } + if len(obj.Labels) > 0 { + c.Null = false + } + } + c.Unknown = false + tf.Attrs["labels"] = c + } + } + } + { + t, ok := tf.AttrTypes["expires"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.metadata.expires"}) + } else { + v := CopyToTimestamp(diags, obj.Expires, t, tf.Attrs["expires"]) + tf.Attrs["expires"] = v + } + } + { + t, ok := tf.AttrTypes["revision"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.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{"Bot.metadata.revision", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.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 + } + } + } + { + a, ok := tf.AttrTypes["spec"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.spec"}) + } else { + o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.spec", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"}) + } else { + v, ok := tf.Attrs["spec"].(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + v = github_com_hashicorp_terraform_plugin_framework_types.Object{ + + AttrTypes: o.AttrTypes, + Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)), + } + } else { + if v.Attrs == nil { + v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes)) + } + } + if obj.Spec == nil { + v.Null = true + } else { + obj := obj.Spec + tf := &v + { + a, ok := tf.AttrTypes["roles"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.spec.roles"}) + } else { + o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ListType) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.spec.roles", "github.com/hashicorp/terraform-plugin-framework/types.ListType"}) + } else { + c, ok := tf.Attrs["roles"].(github_com_hashicorp_terraform_plugin_framework_types.List) + if !ok { + c = github_com_hashicorp_terraform_plugin_framework_types.List{ + + ElemType: o.ElemType, + Elems: make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.Roles)), + Null: true, + } + } else { + if c.Elems == nil { + c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.Roles)) + } + } + if obj.Roles != nil { + t := o.ElemType + if len(obj.Roles) != len(c.Elems) { + c.Elems = make([]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(obj.Roles)) + } + for k, a := range obj.Roles { + v, ok := tf.Attrs["roles"].(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{"Bot.spec.roles", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.spec.roles", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(a) == "" + } + v.Value = string(a) + v.Unknown = false + c.Elems[k] = v + } + if len(obj.Roles) > 0 { + c.Null = false + } + } + c.Unknown = false + tf.Attrs["roles"] = c + } + } + } + { + t, ok := tf.AttrTypes["traits"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.spec.traits"}) + } else { + v := CopyToTraitsMap(diags, obj.Traits, t, tf.Attrs["traits"]) + tf.Attrs["traits"] = v + } + } + { + t, ok := tf.AttrTypes["max_session_ttl"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.spec.max_session_ttl"}) + } else { + v := CopyToDuration(diags, obj.MaxSessionTtl, t, tf.Attrs["max_session_ttl"]) + tf.Attrs["max_session_ttl"] = v + } + } + } + v.Unknown = false + tf.Attrs["spec"] = v + } + } + } + { + a, ok := tf.AttrTypes["status"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.status"}) + } else { + o, ok := a.(github_com_hashicorp_terraform_plugin_framework_types.ObjectType) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.status", "github.com/hashicorp/terraform-plugin-framework/types.ObjectType"}) + } else { + v, ok := tf.Attrs["status"].(github_com_hashicorp_terraform_plugin_framework_types.Object) + if !ok { + v = github_com_hashicorp_terraform_plugin_framework_types.Object{ + + AttrTypes: o.AttrTypes, + Attrs: make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(o.AttrTypes)), + } + } else { + if v.Attrs == nil { + v.Attrs = make(map[string]github_com_hashicorp_terraform_plugin_framework_attr.Value, len(tf.AttrTypes)) + } + } + if obj.Status == nil { + v.Null = true + } else { + obj := obj.Status + tf := &v + { + t, ok := tf.AttrTypes["user_name"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.status.user_name"}) + } else { + v, ok := tf.Attrs["user_name"].(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{"Bot.status.user_name", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.status.user_name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.UserName) == "" + } + v.Value = string(obj.UserName) + v.Unknown = false + tf.Attrs["user_name"] = v + } + } + { + t, ok := tf.AttrTypes["role_name"] + if !ok { + diags.Append(attrWriteMissingDiag{"Bot.status.role_name"}) + } else { + v, ok := tf.Attrs["role_name"].(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{"Bot.status.role_name", err}) + } + v, ok = i.(github_com_hashicorp_terraform_plugin_framework_types.String) + if !ok { + diags.Append(attrWriteConversionFailureDiag{"Bot.status.role_name", "github.com/hashicorp/terraform-plugin-framework/types.String"}) + } + v.Null = string(obj.RoleName) == "" + } + v.Value = string(obj.RoleName) + v.Unknown = false + tf.Attrs["role_name"] = v + } + } + } + v.Unknown = false + tf.Attrs["status"] = v + } + } + } + return diags +} + +// attrReadMissingDiag represents diagnostic message on an attribute missing in the source object +type attrReadMissingDiag struct { + Path string +} + +func (d attrReadMissingDiag) Severity() github_com_hashicorp_terraform_plugin_framework_diag.Severity { + return github_com_hashicorp_terraform_plugin_framework_diag.SeverityError +} + +func (d attrReadMissingDiag) Summary() string { + return "Error reading from Terraform object" +} + +func (d attrReadMissingDiag) Detail() string { + return fmt.Sprintf("A value for %v is missing in the source Terraform object Attrs", d.Path) +} + +func (d attrReadMissingDiag) Equal(o github_com_hashicorp_terraform_plugin_framework_diag.Diagnostic) bool { + return (d.Severity() == o.Severity()) && (d.Summary() == o.Summary()) && (d.Detail() == o.Detail()) +} + +// attrReadConversionFailureDiag represents diagnostic message on a failed type conversion on read +type attrReadConversionFailureDiag struct { + Path string + Type string +} + +func (d attrReadConversionFailureDiag) Severity() github_com_hashicorp_terraform_plugin_framework_diag.Severity { + return github_com_hashicorp_terraform_plugin_framework_diag.SeverityError +} + +func (d attrReadConversionFailureDiag) Summary() string { + return "Error reading from Terraform object" +} + +func (d attrReadConversionFailureDiag) Detail() string { + return fmt.Sprintf("A value for %v can not be converted to %v", d.Path, d.Type) +} + +func (d attrReadConversionFailureDiag) Equal(o github_com_hashicorp_terraform_plugin_framework_diag.Diagnostic) bool { + return (d.Severity() == o.Severity()) && (d.Summary() == o.Summary()) && (d.Detail() == o.Detail()) +} + +// attrWriteMissingDiag represents diagnostic message on an attribute missing in the target object +type attrWriteMissingDiag struct { + Path string +} + +func (d attrWriteMissingDiag) Severity() github_com_hashicorp_terraform_plugin_framework_diag.Severity { + return github_com_hashicorp_terraform_plugin_framework_diag.SeverityError +} + +func (d attrWriteMissingDiag) Summary() string { + return "Error writing to Terraform object" +} + +func (d attrWriteMissingDiag) Detail() string { + return fmt.Sprintf("A value for %v is missing in the source Terraform object AttrTypes", d.Path) +} + +func (d attrWriteMissingDiag) Equal(o github_com_hashicorp_terraform_plugin_framework_diag.Diagnostic) bool { + return (d.Severity() == o.Severity()) && (d.Summary() == o.Summary()) && (d.Detail() == o.Detail()) +} + +// attrWriteConversionFailureDiag represents diagnostic message on a failed type conversion on write +type attrWriteConversionFailureDiag struct { + Path string + Type string +} + +func (d attrWriteConversionFailureDiag) Severity() github_com_hashicorp_terraform_plugin_framework_diag.Severity { + return github_com_hashicorp_terraform_plugin_framework_diag.SeverityError +} + +func (d attrWriteConversionFailureDiag) Summary() string { + return "Error writing to Terraform object" +} + +func (d attrWriteConversionFailureDiag) Detail() string { + return fmt.Sprintf("A value for %v can not be converted to %v", d.Path, d.Type) +} + +func (d attrWriteConversionFailureDiag) Equal(o github_com_hashicorp_terraform_plugin_framework_diag.Diagnostic) bool { + return (d.Severity() == o.Severity()) && (d.Summary() == o.Summary()) && (d.Detail() == o.Detail()) +} + +// attrWriteGeneralError represents diagnostic message on a generic error on write +type attrWriteGeneralError struct { + Path string + Err error +} + +func (d attrWriteGeneralError) Severity() github_com_hashicorp_terraform_plugin_framework_diag.Severity { + return github_com_hashicorp_terraform_plugin_framework_diag.SeverityError +} + +func (d attrWriteGeneralError) Summary() string { + return "Error writing to Terraform object" +} + +func (d attrWriteGeneralError) Detail() string { + return fmt.Sprintf("%s: %s", d.Path, d.Err.Error()) +} + +func (d attrWriteGeneralError) Equal(o github_com_hashicorp_terraform_plugin_framework_diag.Diagnostic) bool { + return (d.Severity() == o.Severity()) && (d.Summary() == o.Summary()) && (d.Detail() == o.Detail()) +} diff --git a/integrations/terraform/tfschema/machineid/v1/custom_types.go b/integrations/terraform/tfschema/machineid/v1/custom_types.go new file mode 100644 index 0000000000000..ccc8a8e41326c --- /dev/null +++ b/integrations/terraform/tfschema/machineid/v1/custom_types.go @@ -0,0 +1,115 @@ +/* +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 v1 + +import ( + context "context" + fmt "fmt" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + + machineidv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/machineid/v1" + + "github.com/gravitational/teleport/integrations/terraform/tfschema/resource153" +) + +var ( + GenSchemaDuration = resource153.GenSchemaDuration + CopyToDuration = resource153.CopyToDuration + CopyFromDuration = resource153.CopyFromDuration + + GenSchemaTimestamp = resource153.GenSchemaTimestamp + CopyToTimestamp = resource153.CopyToTimestamp + CopyFromTimestamp = resource153.CopyFromTimestamp +) + +func GenSchemaTraitsMap(_ context.Context, attr tfsdk.Attribute) tfsdk.Attribute { + return tfsdk.Attribute{ + Optional: attr.Optional, + Type: types.MapType{ + ElemType: types.ListType{ + ElemType: types.StringType, + }, + }, + Description: attr.Description, + } +} + +func CopyFromTraitsMap(diags diag.Diagnostics, v attr.Value, o *[]*machineidv1.Trait) { + value, ok := v.(types.Map) + if !ok { + diags.AddError("Error reading from Terraform object", fmt.Sprintf("Can not convert %T to Map", v)) + return + } + + traits := make([]*machineidv1.Trait, 0, len(value.Elems)) + for name, elem := range value.Elems { + list, ok := elem.(types.List) + if !ok { + diags.AddError("Error reading from Terraform object", fmt.Sprintf("Can not convert %T to List", elem)) + return + } + + trait := &machineidv1.Trait{ + Name: name, + Values: make([]string, len(list.Elems)), + } + + for idx, elem := range list.Elems { + str, ok := elem.(types.String) + if !ok { + diags.AddError("Error reading from Terraform object", fmt.Sprintf("Can not convert %T to String", elem)) + return + } + trait.Values[idx] = str.Value + } + + traits = append(traits, trait) + } + + *o = traits +} + +func CopyToTraitsMap(_ diag.Diagnostics, traits []*machineidv1.Trait, _ attr.Type, _ attr.Value) attr.Value { + mapValue := &types.Map{ + Elems: make(map[string]attr.Value, len(traits)), + ElemType: types.ListType{ + ElemType: types.StringType, + }, + } + + if traits == nil { + mapValue.Null = true + return mapValue + } + + for _, trait := range traits { + listValue := types.List{ + Elems: make([]attr.Value, len(trait.Values)), + ElemType: types.StringType, + } + for idx, value := range trait.GetValues() { + listValue.Elems[idx] = types.String{Value: value} + } + mapValue.Elems[trait.Name] = listValue + } + + return mapValue +} diff --git a/lib/auth/machineid/machineidv1/bot_service.go b/lib/auth/machineid/machineidv1/bot_service.go index 5d319bf881067..e63a49321087f 100644 --- a/lib/auth/machineid/machineidv1/bot_service.go +++ b/lib/auth/machineid/machineidv1/bot_service.go @@ -727,6 +727,12 @@ func botFromUserAndRole(user types.User, role types.Role) (*pb.Bot, error) { expiry := botExpiryFromUser(user) + // We need to return a revision because the Terraform provider uses it to + // determine when the change has been applied to the cache. We do not use + // the revision for conditional updates, though, so the concatenation of + // the user and role's revisions is sufficient. + revision := fmt.Sprintf("%s/%s", user.GetMetadata().Revision, role.GetMetadata().Revision) + b := &pb.Bot{ Kind: types.KindBot, Version: types.V1, @@ -734,6 +740,7 @@ func botFromUserAndRole(user types.User, role types.Role) (*pb.Bot, error) { Name: botName, Expires: expiry, Description: user.GetMetadata().Description, + Revision: revision, }, Status: &pb.BotStatus{ UserName: user.GetName(), diff --git a/lib/auth/machineid/machineidv1/machineidv1_test.go b/lib/auth/machineid/machineidv1/machineidv1_test.go index 8b6b310ea022d..25070b48b7677 100644 --- a/lib/auth/machineid/machineidv1/machineidv1_test.go +++ b/lib/auth/machineid/machineidv1/machineidv1_test.go @@ -48,6 +48,8 @@ import ( "github.com/gravitational/teleport/lib/modules" ) +var ignoreRevision = protocmp.IgnoreFields(&headerv1.Metadata{}, "revision") + func TestMain(m *testing.M) { modules.SetInsecureTestMode(true) os.Exit(m.Run()) @@ -605,7 +607,7 @@ func TestCreateBot(t *testing.T) { tt.assertError(t, err) if tt.want != nil { // Check that the returned bot matches - require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform())) + require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform(), ignoreRevision)) } if tt.wantUser != nil { gotUser, err := srv.Auth().GetUser(ctx, tt.wantUser.GetName(), false) @@ -1008,6 +1010,7 @@ func TestUpdateBot(t *testing.T) { &machineidv1pb.BotSpec{}, "traits", ), + ignoreRevision, ), ) } @@ -1596,7 +1599,7 @@ func TestUpsertBot(t *testing.T) { tt.assertError(t, err) if tt.want != nil { // Check that the returned bot matches - require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform())) + require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform(), ignoreRevision)) } if tt.wantUser != nil { gotUser, err := srv.Auth().GetUser(ctx, tt.wantUser.GetName(), false) @@ -1781,7 +1784,7 @@ func TestGetBot(t *testing.T) { tt.assertError(t, err) if tt.want != nil { // Check that the returned bot matches - require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform())) + require.Empty(t, cmp.Diff(tt.want, bot, protocmp.Transform(), ignoreRevision)) } }) } @@ -1935,6 +1938,7 @@ func TestListBots(t *testing.T) { res, protocmp.Transform(), protocmp.SortRepeatedFields(&machineidv1pb.ListBotsResponse{}, "bots"), + ignoreRevision, ), ) }