diff --git a/blueprints/thousandeyes-kiota/resources/account_group.blueprint.json b/blueprints/thousandeyes-kiota/resources/account_group.blueprint.json new file mode 100644 index 00000000..6e93c8fb --- /dev/null +++ b/blueprints/thousandeyes-kiota/resources/account_group.blueprint.json @@ -0,0 +1,529 @@ +{ + "formatVersion": "4", + "resources": [ + { + "key": "account_group", + "name": "account_group", + "goPackage": "account_group", + "goPackageAlias": "v7AccountGroup", + "goTypeName": "AccountGroupResource", + "modelTypeName": "AccountGroupResourceModel", + "serviceGroup": "account_groups", + "apiVersionDir": "v7", + "schema": { + "attributes": [ + { + "name": "account_group_name", + "goField": "AccountGroupName", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Account group name\n\nRequired-by-API by curation: the sweeper's field, unprobeable by omission.", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true + }, + "wire": { + "jsonPath": "accountGroupName", + "sdkField": "AccountGroupName", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "id", + "goField": "ID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "A unique identifier associated with your account group. You can retrieve your `AccountGroupId` from the `/account-groups` endpoint.\n\nNamed `id` per the import convention; the wire field stays aid.", + "wire": { + "jsonPath": "aid", + "sdkField": "Aid", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "is_current_account_group", + "goField": "IsCurrentAccountGroup", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates whether the requested aid is the context of the current account.", + "wire": { + "jsonPath": "isCurrentAccountGroup", + "sdkField": "IsCurrentAccountGroup", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "is_default_account_group", + "goField": "IsDefaultAccountGroup", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates whether the aid is the default one for the requesting user.", + "wire": { + "jsonPath": "isDefaultAccountGroup", + "sdkField": "IsDefaultAccountGroup", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "org_id", + "goField": "OrgID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "(Optional) The ID for the organization associated with the account group.", + "wire": { + "jsonPath": "orgId", + "sdkField": "OrgId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "organization_name", + "goField": "OrganizationName", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "(Optional) The name of the organization associated with the account group.", + "wire": { + "jsonPath": "organizationName", + "sdkField": "OrganizationName", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "users", + "goField": "Users", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "AccountGroupUserAccountGroupModel", + "sdkType": "models.Administrative_API_UserAccountGroupable", + "attrTypesVar": "accountGroupUserAccountGroupAttrTypes", + "objectTypeVar": "accountGroupUserAccountGroupObjectType", + "expandFunc": "expandAccountGroupUserAccountGroups", + "flattenFunc": "flattenAccountGroupUserAccountGroups", + "attributes": [ + { + "name": "date_registered", + "goField": "DateRegistered", + "type": { + "kind": "string", + "constraints": { + "format": "date-time" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User's UTC registration date (ISO date-time format).", + "wire": { + "jsonPath": "dateRegistered", + "sdkField": "DateRegistered", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "email", + "goField": "Email", + "type": { + "kind": "string", + "constraints": { + "format": "email" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User's email address.", + "wire": { + "jsonPath": "email", + "sdkField": "Email", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "last_login", + "goField": "LastLogin", + "type": { + "kind": "string", + "constraints": { + "format": "date-time" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User's UTC last login date (ISO date-time format).", + "wire": { + "jsonPath": "lastLogin", + "sdkField": "LastLogin", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User's display name.", + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "roles", + "goField": "Roles", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "AccountGroupRoleModel", + "sdkType": "models.Administrative_API_Roleable", + "attrTypesVar": "accountGroupRoleAttrTypes", + "objectTypeVar": "accountGroupRoleObjectType", + "expandFunc": "expandAccountGroupRoles", + "flattenFunc": "flattenAccountGroupRoles", + "attributes": [ + { + "name": "has_management_permissions", + "goField": "HasManagementPermissions", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Flag indicating whether the user has management permissions.", + "wire": { + "jsonPath": "hasManagementPermissions", + "sdkField": "HasManagementPermissions", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "is_builtin", + "goField": "IsBuiltin", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User).", + "wire": { + "jsonPath": "isBuiltin", + "sdkField": "IsBuiltin", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Name of the role.", + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "role_id", + "goField": "RoleID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID representing the role.", + "wire": { + "jsonPath": "roleId", + "sdkField": "RoleId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "constructorExpr": "models.NewAdministrative_API_Role()" + } + }, + "computedOptionalRequired": "computed", + "wire": { + "jsonPath": "roles", + "sdkField": "Roles", + "sdkGoType": "[]models.Administrative_API_Roleable", + "flatten": { + "func": "flattenAccountGroupRoles", + "needsCtx": true, + "returnsError": true + }, + "skipExpand": true + } + }, + { + "name": "uid", + "goField": "Uid", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID representing the user.", + "wire": { + "jsonPath": "uid", + "sdkField": "Uid", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "constructorExpr": "models.NewAdministrative_API_UserAccountGroup()" + } + }, + "computedOptionalRequired": "computed", + "wire": { + "jsonPath": "users", + "sdkField": "Users", + "sdkGoType": "[]models.Administrative_API_UserAccountGroupable", + "flatten": { + "func": "flattenAccountGroupUserAccountGroups", + "needsCtx": true, + "returnsError": true + }, + "skipExpand": true + } + } + ], + "markdownDescription": "Retrieve account group." + }, + "binding": { + "service": { + "importPath": "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models", + "alias": "models", + "accessor": "r.client" + }, + "create": { + "style": "fluent", + "chain": [ + { + "method": "AccountGroups" + }, + { + "method": "Post", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_CreatedAccountGroupable", + "httpMethod": "POST", + "pathTemplate": "/account-groups" + }, + "read": { + "style": "fluent", + "chain": [ + { + "method": "AccountGroups" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Get", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_AccountGroupDetailable", + "httpMethod": "GET", + "pathTemplate": "/account-groups/{id}" + }, + "update": { + "style": "fluent", + "chain": [ + { + "method": "AccountGroups" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Put", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_AccountGroupDetailable", + "httpMethod": "PUT", + "pathTemplate": "/account-groups/{id}" + }, + "delete": { + "style": "fluent", + "chain": [ + { + "method": "AccountGroups" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Delete", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "error", + "httpMethod": "DELETE", + "pathTemplate": "/account-groups/{id}" + }, + "id": { + "attribute": "id", + "goField": "ID", + "fromCreate": "created.GetAid()", + "fromCreateIsPointer": true + }, + "body": { + "requestType": "models.Administrative_API_AccountGroupRequest", + "responseType": "models.Administrative_API_AccountGroupDetailable", + "constructorExpr": "models.NewAdministrative_API_AccountGroupRequest()", + "accessStyle": "method" + } + }, + "policy": { + "updateStyle": "putFull", + "delete": { + "notFoundIsSuccess": true + } + }, + "import": { + "style": "passthroughId", + "attribute": "id" + }, + "sweep": { + "nameField": "accountGroupName" + }, + "accTest": { + "skipUnlessEnv": "TFPFGEN_ACC_ADMIN" + } + } + ] +} \ No newline at end of file diff --git a/blueprints/thousandeyes-kiota/resources/alert_suppression_window.blueprint.json b/blueprints/thousandeyes-kiota/resources/alert_suppression_window.blueprint.json new file mode 100644 index 00000000..56d11077 --- /dev/null +++ b/blueprints/thousandeyes-kiota/resources/alert_suppression_window.blueprint.json @@ -0,0 +1,969 @@ +{ + "formatVersion": "4", + "resources": [ + { + "key": "alert_suppression_window", + "name": "alert_suppression_window", + "goPackage": "alert_suppression_window", + "goPackageAlias": "v7AlertSuppressionWindow", + "goTypeName": "AlertSuppressionWindowResource", + "modelTypeName": "AlertSuppressionWindowResourceModel", + "serviceGroup": "alert_suppression_windows", + "apiVersionDir": "v7", + "schema": { + "attributes": [ + { + "name": "id", + "goField": "ID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID of the alert suppression window.\n\nNamed `id` because terraform-plugin-testing populates import state only from an attribute literally called id; the wire field stays alertSuppressionWindowId.", + "wire": { + "jsonPath": "alertSuppressionWindowId", + "sdkField": "AlertSuppressionWindowId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "duration", + "goField": "Duration", + "type": { + "kind": "int64" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Duration in seconds the suppression window is active.\n\nThe API refuses a window without a duration (live probe, 2026-08-03).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "duration", + "sdkField": "Duration", + "sdkGoType": "*int32", + "expand": { + "func": "convert.FrameworkInt64ToPtrInt32" + }, + "flatten": { + "func": "convert.PtrInt32ToFrameworkInt64" + } + } + }, + { + "name": "end_repeat", + "goField": "EndRepeat", + "type": { + "kind": "single_nested", + "nestedObject": { + "goTypeName": "AlertSuppressionWindowEndRepeatModel", + "sdkType": "models.Alerts_API_EndRepeatable", + "attrTypesVar": "alertSuppressionWindowEndRepeatAttrTypes", + "objectTypeVar": "alertSuppressionWindowEndRepeatObjectType", + "expandFunc": "expandAlertSuppressionWindowEndRepeat", + "flattenFunc": "flattenAlertSuppressionWindowEndRepeat", + "attributes": [ + { + "name": "count", + "goField": "Count", + "type": { + "kind": "int64" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "End repeat after number of occurrences, only valid with count type option.", + "wire": { + "jsonPath": "count", + "sdkField": "Count", + "sdkGoType": "*int32", + "expand": { + "func": "convert.FrameworkInt64ToPtrInt32" + }, + "flatten": { + "func": "convert.PtrInt32ToFrameworkInt64" + } + } + }, + { + "name": "date", + "goField": "Date", + "type": { + "kind": "string", + "constraints": { + "format": "date" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "End repeat after specific date, only valid with date type option (ISO date format).", + "wire": { + "jsonPath": "date", + "sdkField": "Date", + "sdkGoType": "*serialization.DateOnly", + "expand": { + "func": "convert.FrameworkToPtrDateOnly", + "returnsError": true + }, + "flatten": { + "func": "convert.PtrDateOnlyToFramework" + } + } + }, + { + "name": "type", + "goField": "Type", + "type": { + "kind": "string", + "allowedValues": [ + "count", + "never", + "date" + ] + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "End repeat options type.\n\n\nValues accepted here: `count`, `never`, `date`.\nObserved: the API assigns \"never\" when this is omitted.\nThe API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute.\n", + "behaviour": { + "serverDefault": { + "kind": "", + "raw": "\"never\"" + }, + "acceptedValues": [ + "count", + "never", + "date" + ], + "valuesClosed": false + }, + "wire": { + "jsonPath": "type", + "sdkField": "TypeEscaped", + "sdkGoType": "*models.Alerts_API_EndRepeatType", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Alerts_API_EndRepeatType" + ], + "extraArgs": [ + "models.ParseAlerts_API_EndRepeatType" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + } + ], + "constructorExpr": "models.NewAlerts_API_EndRepeat()" + } + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "End repeat options.\n\nThe API assigns { type = \"never\" } when omitted (live acceptance, 2026-08-03).", + "wire": { + "jsonPath": "endRepeat", + "sdkField": "EndRepeat", + "sdkGoType": "models.Alerts_API_EndRepeatable", + "expand": { + "func": "expandAlertSuppressionWindowEndRepeat", + "needsCtx": true, + "returnsError": true + }, + "flatten": { + "func": "flattenAlertSuppressionWindowEndRepeat", + "needsCtx": true, + "returnsError": true + } + } + }, + { + "name": "is_enabled", + "goField": "IsEnabled", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "Set to `false` for `disabled`, `true` for `enabled`.", + "behaviour": { + "requiredByApi": false, + "serverDefault": { + "kind": "bool", + "raw": "true" + }, + "returnedOnRead": true, + "returnedOnCreate": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "isEnabled", + "sdkField": "IsEnabled", + "sdkGoType": "*bool", + "expand": { + "func": "convert.FrameworkToPtrBool" + }, + "flatten": { + "func": "convert.PtrBoolToFramework" + } + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Name of the alert suppression window.\n\nThe API refuses a window without a name (live acceptance, 2026-08-03); the probe cannot test this itself because name carries the sweeper's prefix.", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true + }, + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "repeat", + "goField": "Repeat", + "type": { + "kind": "single_nested", + "nestedObject": { + "goTypeName": "AlertSuppressionWindowRepeatModel", + "sdkType": "models.Alerts_API_Repeatable", + "attrTypesVar": "alertSuppressionWindowRepeatAttrTypes", + "objectTypeVar": "alertSuppressionWindowRepeatObjectType", + "expandFunc": "expandAlertSuppressionWindowRepeat", + "flattenFunc": "flattenAlertSuppressionWindowRepeat", + "attributes": [ + { + "name": "days_of_week", + "goField": "DaysOfWeek", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "wire": { + "jsonPath": "daysOfWeek", + "sdkField": "DaysOfWeek", + "sdkGoType": "[]models.Alerts_API_DaysOfWeek", + "expand": { + "func": "convert.FrameworkSetToKiotaEnumSlice", + "typeArgs": [ + "models.Alerts_API_DaysOfWeek" + ], + "extraArgs": [ + "models.ParseAlerts_API_DaysOfWeek" + ], + "needsCtx": true, + "returnsError": true + }, + "flatten": { + "func": "convert.KiotaEnumSliceToFrameworkSet", + "needsCtx": true, + "returnsError": true + } + } + }, + { + "name": "interval_length", + "goField": "IntervalLength", + "type": { + "kind": "int64" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Number of `intervalTypes` to wait before reactivating the alert suppression window.", + "wire": { + "jsonPath": "intervalLength", + "sdkField": "IntervalLength", + "sdkGoType": "*int32", + "expand": { + "func": "convert.FrameworkInt64ToPtrInt32" + }, + "flatten": { + "func": "convert.PtrInt32ToFrameworkInt64" + } + } + }, + { + "name": "interval_type", + "goField": "IntervalType", + "type": { + "kind": "string", + "allowedValues": [ + "day", + "week", + "month" + ] + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Repeat options interval type\n\n\nValues accepted here: `day`, `week`, `month`.\nThe API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute.\n", + "behaviour": { + "acceptedValues": [ + "day", + "week", + "month" + ], + "valuesClosed": false + }, + "wire": { + "jsonPath": "intervalType", + "sdkField": "IntervalType", + "sdkGoType": "*models.Alerts_API_IntervalType", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Alerts_API_IntervalType" + ], + "extraArgs": [ + "models.ParseAlerts_API_IntervalType" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + }, + { + "name": "type", + "goField": "Type", + "type": { + "kind": "string", + "allowedValues": [ + "day", + "week", + "month", + "custom" + ] + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "Repeat options type.\n\n\nValues accepted here: `day`, `week`, `month`, `custom`.\nObserved: the API assigns \"none\" when this is omitted.\nThe API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute.\n", + "behaviour": { + "serverDefault": { + "kind": "", + "raw": "\"none\"" + }, + "returnedOnRead": true, + "returnedOnCreate": true, + "acceptedValues": [ + "day", + "week", + "month", + "custom" + ], + "valuesClosed": false + }, + "wire": { + "jsonPath": "type", + "sdkField": "TypeEscaped", + "sdkGoType": "*models.Alerts_API_RepeatType", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Alerts_API_RepeatType" + ], + "extraArgs": [ + "models.ParseAlerts_API_RepeatType" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + } + ], + "constructorExpr": "models.NewAlerts_API_Repeat()" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Repeat options.\n\nThe API requires repeat conditions on every window; a one-off window carries `type = \"none\"` (live probe, 2026-08-03).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "repeat", + "sdkField": "Repeat", + "sdkGoType": "models.Alerts_API_Repeatable", + "expand": { + "func": "expandAlertSuppressionWindowRepeat", + "needsCtx": true, + "returnsError": true + }, + "flatten": { + "func": "flattenAlertSuppressionWindowRepeat", + "needsCtx": true, + "returnsError": true + } + } + }, + { + "name": "start_date", + "goField": "StartDate", + "type": { + "kind": "string", + "constraints": { + "format": "date-time" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "The date/time when the alert suppression window starts (ISO date-time format).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "startDate", + "sdkField": "StartDate", + "sdkGoType": "*time.Time", + "expand": { + "func": "convert.FrameworkToPtrTime", + "returnsError": true + }, + "flatten": { + "func": "convert.PtrTimeToFramework" + } + } + }, + { + "name": "status", + "goField": "Status", + "type": { + "kind": "string", + "allowedValues": [ + "active", + "inactive", + "ended" + ] + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates the current status of the suppression window.", + "wire": { + "jsonPath": "status", + "sdkField": "Status", + "sdkGoType": "*models.Alerts_API_AlertSuppressionWindowState", + "flatten": { + "func": "convert.PtrStringerToFramework" + }, + "skipExpand": true + } + }, + { + "name": "test_ids", + "goField": "TestIDs", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "List of test IDs to assign to the alert suppression window.\n\nThe response never carries testIds -- it echoes the linked tests as a nested collection instead -- so state carries the configured value.\n\n\nNeither the update response nor a read returns this field: it is write-only in practice, and state carries the configured value.\n", + "behaviour": { + "returnedOnRead": false, + "returnedOnUpdate": false + }, + "wire": { + "jsonPath": "testIds", + "sdkField": "TestIds", + "sdkGoType": "[]string", + "expand": { + "func": "convert.FrameworkSetToStringSlice", + "needsCtx": true, + "returnsError": true + }, + "skipFlatten": true + } + }, + { + "name": "tests", + "goField": "Tests", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "AlertSuppressionWindowSimpleTestModel", + "sdkType": "models.Alerts_API_SimpleTestable", + "attrTypesVar": "alertSuppressionWindowSimpleTestAttrTypes", + "objectTypeVar": "alertSuppressionWindowSimpleTestObjectType", + "expandFunc": "expandAlertSuppressionWindowSimpleTests", + "flattenFunc": "flattenAlertSuppressionWindowSimpleTests", + "attributes": [ + { + "name": "alerts_enabled", + "goField": "AlertsEnabled", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates if alerts are enabled.", + "wire": { + "jsonPath": "alertsEnabled", + "sdkField": "AlertsEnabled", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "created_by", + "goField": "CreatedBy", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User that created the test.", + "wire": { + "jsonPath": "createdBy", + "sdkField": "CreatedBy", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "created_date", + "goField": "CreatedDate", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "UTC created date (ISO date-time format).", + "wire": { + "jsonPath": "createdDate", + "sdkField": "CreatedDate", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "description", + "goField": "Description", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "A description of the test.", + "wire": { + "jsonPath": "description", + "sdkField": "Description", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "enabled", + "goField": "Enabled", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Test is enabled.", + "wire": { + "jsonPath": "enabled", + "sdkField": "Enabled", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "interval", + "goField": "Interval", + "type": { + "kind": "string", + "allowedValues": [ + "60", + "120", + "300", + "600", + "900", + "1800", + "3600" + ] + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Interval between test runs in seconds.", + "wire": { + "jsonPath": "interval", + "sdkField": "Interval", + "sdkGoType": "*int32", + "flatten": { + "func": "convert.PtrInt32ToFrameworkString" + }, + "skipExpand": true + } + }, + { + "name": "live_share", + "goField": "LiveShare", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates if the test is shared with the account group.", + "wire": { + "jsonPath": "liveShare", + "sdkField": "LiveShare", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "modified_by", + "goField": "ModifiedBy", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "User that modified the test.", + "wire": { + "jsonPath": "modifiedBy", + "sdkField": "ModifiedBy", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "modified_date", + "goField": "ModifiedDate", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "UTC last modification date (ISO date-time format).", + "wire": { + "jsonPath": "modifiedDate", + "sdkField": "ModifiedDate", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "saved_event", + "goField": "SavedEvent", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Indicates if the test is a saved event.\n\n**Note**: **Saved Events** are now called **Private Snapshots** in the user interface. This change does not affect API.", + "wire": { + "jsonPath": "savedEvent", + "sdkField": "SavedEvent", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "test_id", + "goField": "TestID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Each test is assigned an unique ID; this is used to access test information and results from other endpoints.", + "wire": { + "jsonPath": "testId", + "sdkField": "TestId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "test_name", + "goField": "TestName", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "The name of the test. Test name must be unique.", + "wire": { + "jsonPath": "testName", + "sdkField": "TestName", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "type", + "goField": "Type", + "type": { + "kind": "string", + "allowedValues": [ + "api", + "agent-to-agent", + "agent-to-server", + "bgp", + "http-server", + "page-load", + "web-transactions", + "ftp-server", + "dns-trace", + "dns-server", + "dnssec", + "sip-server", + "voice" + ] + }, + "computedOptionalRequired": "computed", + "markdownDescription": "This is a read only value, as test type is implicit in the test creation url.", + "wire": { + "jsonPath": "type", + "sdkField": "TypeEscaped", + "sdkGoType": "*models.Alerts_API_TestType", + "flatten": { + "func": "convert.PtrStringerToFramework" + }, + "skipExpand": true + } + } + ], + "constructorExpr": "models.NewAlerts_API_SimpleTest()" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "List of tests assigned to the alert suppression window.\n\nDropped in v1: Read-only echo of the suppressed tests; testIds is the writable half and the echo would drift against it.", + "wire": { + "jsonPath": "tests", + "sdkField": "Tests", + "sdkGoType": "[]models.Alerts_API_SimpleTestable", + "flatten": { + "func": "flattenAlertSuppressionWindowSimpleTests", + "needsCtx": true, + "returnsError": true + }, + "skipExpand": true + }, + "drop": true + } + ], + "markdownDescription": "Retrieve alert suppression window." + }, + "binding": { + "service": { + "importPath": "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models", + "alias": "models", + "accessor": "r.client" + }, + "create": { + "style": "fluent", + "chain": [ + { + "method": "AlertSuppressionWindows" + }, + { + "method": "Post", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Alerts_API_AlertSuppressionWindowDetailable", + "httpMethod": "POST", + "pathTemplate": "/alert-suppression-windows" + }, + "read": { + "style": "fluent", + "chain": [ + { + "method": "AlertSuppressionWindows" + }, + { + "method": "ByWindowId", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Get", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Alerts_API_AlertSuppressionWindowDetailable", + "httpMethod": "GET", + "pathTemplate": "/alert-suppression-windows/{windowId}" + }, + "update": { + "style": "fluent", + "chain": [ + { + "method": "AlertSuppressionWindows" + }, + { + "method": "ByWindowId", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Put", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Alerts_API_AlertSuppressionWindowDetailable", + "httpMethod": "PUT", + "pathTemplate": "/alert-suppression-windows/{windowId}" + }, + "delete": { + "style": "fluent", + "chain": [ + { + "method": "AlertSuppressionWindows" + }, + { + "method": "ByWindowId", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Delete", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "error", + "httpMethod": "DELETE", + "pathTemplate": "/alert-suppression-windows/{windowId}" + }, + "id": { + "attribute": "id", + "goField": "ID", + "fromCreate": "created.GetAlertSuppressionWindowId()", + "fromCreateIsPointer": true + }, + "body": { + "requestType": "models.Alerts_API_AlertSuppressionWindowRequest", + "responseType": "models.Alerts_API_AlertSuppressionWindowDetailable", + "constructorExpr": "models.NewAlerts_API_AlertSuppressionWindowRequest()", + "accessStyle": "method" + } + }, + "policy": { + "updateStyle": "putFull", + "delete": { + "notFoundIsSuccess": true + } + }, + "import": { + "style": "passthroughId", + "attribute": "id" + }, + "accFixture": { + "values": [ + { + "attr": "duration", + "hcl": "3600", + "wire": 3600 + }, + { + "attr": "repeat", + "hcl": "{ type = \"none\" }", + "wire": { + "type": "none" + } + }, + { + "attr": "test_ids", + "hcl": "[]", + "wire": [] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/blueprints/thousandeyes-kiota/resources/endpoint_label.blueprint.json b/blueprints/thousandeyes-kiota/resources/endpoint_label.blueprint.json new file mode 100644 index 00000000..47ef93d1 --- /dev/null +++ b/blueprints/thousandeyes-kiota/resources/endpoint_label.blueprint.json @@ -0,0 +1,498 @@ +{ + "formatVersion": "4", + "resources": [ + { + "key": "endpoint_label", + "name": "endpoint_label", + "goPackage": "endpoint_label", + "goPackageAlias": "v7EndpointLabel", + "goTypeName": "EndpointLabelResource", + "modelTypeName": "EndpointLabelResourceModel", + "serviceGroup": "endpoint_agent_labels", + "apiVersionDir": "v7", + "schema": { + "attributes": [ + { + "name": "color", + "goField": "Color", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "UI color\n\n\nObserved: the API assigns \"#d9d9d9\" when this is omitted.\n", + "behaviour": { + "serverDefault": { + "kind": "", + "raw": "\"#d9d9d9\"" + }, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "color", + "sdkField": "Color", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "filters", + "goField": "Filters", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "EndpointLabelFilterModel", + "sdkType": "models.Endpoint_Agent_Labels_API_Filterable", + "attrTypesVar": "endpointLabelFilterAttrTypes", + "objectTypeVar": "endpointLabelFilterObjectType", + "expandFunc": "expandEndpointLabelFilters", + "flattenFunc": "flattenEndpointLabelFilters", + "attributes": [ + { + "name": "key", + "goField": "Key", + "type": { + "kind": "string", + "allowedValues": [ + "agent-id", + "public-network", + "local-network", + "connection", + "gateway", + "platform", + "agent-type", + "vpn-vendor", + "vpn-gateway-address", + "vpn-client-network", + "vpn-client-address", + "ip-address-family", + "ssid", + "bssid", + "hostname", + "username", + "asn" + ] + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "Type of filter - the data that will be used to filter.\n\n\nValues accepted here: `agent-id`, `public-network`, `local-network`, `connection`, `gateway`, `platform`, `agent-type`, `vpn-vendor`, `vpn-gateway-address`, `vpn-client-network`, `vpn-client-address`, `ip-address-family`, `ssid`, `bssid`, `hostname`, `username`, `asn`.\nObserved: the API assigns \"platform\" when this is omitted.\nThe API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute.\n", + "behaviour": { + "serverDefault": { + "kind": "", + "raw": "\"platform\"" + }, + "acceptedValues": [ + "agent-id", + "public-network", + "local-network", + "connection", + "gateway", + "platform", + "agent-type", + "vpn-vendor", + "vpn-gateway-address", + "vpn-client-network", + "vpn-client-address", + "ip-address-family", + "ssid", + "bssid", + "hostname", + "username", + "asn" + ], + "valuesClosed": false + }, + "wire": { + "jsonPath": "key", + "sdkField": "Key", + "sdkGoType": "*models.Endpoint_Agent_Labels_API_FilterType", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Endpoint_Agent_Labels_API_FilterType" + ], + "extraArgs": [ + "models.ParseEndpoint_Agent_Labels_API_FilterType" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + }, + { + "name": "mode", + "goField": "Mode", + "type": { + "kind": "string", + "allowedValues": [ + "in", + "not-in" + ] + }, + "computedOptionalRequired": "computed_optional", + "markdownDescription": "Type of matching to be applied for the values:\n\n* `in`: The value on the agent must match one of the list of values provided.\n* `not-in`: The value on the agent must not match any of the list of values provided.\n\n\nValues accepted here: `in`, `not-in`.\nObserved: the API assigns \"in\" when this is omitted.\nThe API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute.\n", + "behaviour": { + "serverDefault": { + "kind": "", + "raw": "\"in\"" + }, + "acceptedValues": [ + "in", + "not-in" + ], + "valuesClosed": false + }, + "wire": { + "jsonPath": "mode", + "sdkField": "Mode", + "sdkGoType": "*models.Endpoint_Agent_Labels_API_LabelFilterMode", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Endpoint_Agent_Labels_API_LabelFilterMode" + ], + "extraArgs": [ + "models.ParseEndpoint_Agent_Labels_API_LabelFilterMode" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + }, + { + "name": "values", + "goField": "Values", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "wire": { + "jsonPath": "values", + "sdkField": "Values", + "sdkGoType": "[]string", + "expand": { + "func": "convert.FrameworkSetToStringSlice", + "needsCtx": true, + "returnsError": true + }, + "flatten": { + "func": "convert.StringSliceToFrameworkSet", + "needsCtx": true, + "returnsError": true + } + } + } + ], + "constructorExpr": "models.NewEndpoint_Agent_Labels_API_Filter()" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "The filters combined using the matchType to determine the label's match.\n\nThe API refuses a label without at least one filter (live probe, 2026-08-04).\n\nOnly returned when the read asks for expand=filters; the generated read always does (live probe, 2026-08-04).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "filters", + "sdkField": "Filters", + "sdkGoType": "[]models.Endpoint_Agent_Labels_API_Filterable", + "expand": { + "func": "expandEndpointLabelFilters", + "needsCtx": true, + "returnsError": true + }, + "flatten": { + "func": "flattenEndpointLabelFilters", + "needsCtx": true, + "returnsError": true + } + } + }, + { + "name": "id", + "goField": "ID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Label identifier.", + "wire": { + "jsonPath": "id", + "sdkField": "Id", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "match_type", + "goField": "MatchType", + "type": { + "kind": "string", + "allowedValues": [ + "and", + "or" + ] + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Specifies how the filters are combined for matching this label.\n\n* `and`: All filters must be matched.\n* `or`: Any of the filters should match.\n\n\nValues accepted here: `and`, `or`.\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnUpdate": true, + "acceptedValues": [ + "and", + "or" + ], + "valuesClosed": true + }, + "wire": { + "jsonPath": "matchType", + "sdkField": "MatchType", + "sdkGoType": "*models.Endpoint_Agent_Labels_API_MatchType", + "expand": { + "func": "convert.FrameworkToKiotaEnum", + "typeArgs": [ + "models.Endpoint_Agent_Labels_API_MatchType" + ], + "extraArgs": [ + "models.ParseEndpoint_Agent_Labels_API_MatchType" + ], + "returnsError": true + }, + "flatten": { + "func": "convert.PtrStringerToFramework" + } + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "The label name.\n\nRequired-by-API by curation: the sweeper's field, unprobeable by omission.", + "behaviour": { + "requiredByApi": true + }, + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "updateExpand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + } + ], + "markdownDescription": "Retrieve label." + }, + "binding": { + "service": { + "importPath": "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models", + "alias": "models", + "accessor": "r.client" + }, + "create": { + "style": "fluent", + "chain": [ + { + "method": "Endpoint" + }, + { + "method": "Labels" + }, + { + "method": "Post", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Endpoint_Agent_Labels_API_LabelResponseable", + "httpMethod": "POST", + "pathTemplate": "/endpoint/labels" + }, + "read": { + "style": "fluent", + "chain": [ + { + "method": "Endpoint" + }, + { + "method": "Labels" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Get", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Endpoint_Agent_Labels_API_LabelResponseable", + "httpMethod": "GET", + "pathTemplate": "/endpoint/labels/{id}" + }, + "update": { + "style": "fluent", + "chain": [ + { + "method": "Endpoint" + }, + { + "method": "Labels" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Patch", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Endpoint_Agent_Labels_API_LabelResponseable", + "httpMethod": "PATCH", + "pathTemplate": "/endpoint/labels/{id}" + }, + "delete": { + "style": "fluent", + "chain": [ + { + "method": "Endpoint" + }, + { + "method": "Labels" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Delete", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "error", + "httpMethod": "DELETE", + "pathTemplate": "/endpoint/labels/{id}" + }, + "id": { + "attribute": "id", + "goField": "ID", + "fromCreate": "created.GetId()", + "fromCreateIsPointer": true + }, + "body": { + "requestType": "models.Endpoint_Agent_Labels_API_LabelRequest", + "responseType": "models.Endpoint_Agent_Labels_API_LabelResponseable", + "constructorExpr": "models.NewEndpoint_Agent_Labels_API_LabelRequest()", + "accessStyle": "method", + "updateRequestType": "models.Endpoint_Agent_Labels_API_Label", + "updateConstructorExpr": "models.NewEndpoint_Agent_Labels_API_Label()" + } + }, + "policy": { + "updateStyle": "patchMerge", + "delete": { + "notFoundIsSuccess": true + } + }, + "import": { + "style": "passthroughId", + "attribute": "id" + }, + "accFixture": { + "values": [ + { + "attr": "filters", + "hcl": "[{ key = \"platform\", mode = \"in\", values = [\"windows\"] }]", + "wire": [ + { + "key": "platform", + "mode": "in", + "values": [ + "windows" + ] + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/blueprints/thousandeyes-kiota/resources/role.blueprint.json b/blueprints/thousandeyes-kiota/resources/role.blueprint.json new file mode 100644 index 00000000..763ee892 --- /dev/null +++ b/blueprints/thousandeyes-kiota/resources/role.blueprint.json @@ -0,0 +1,282 @@ +{ + "formatVersion": "4", + "resources": [ + { + "key": "role", + "name": "role", + "goPackage": "role", + "goPackageAlias": "v7Role", + "goTypeName": "RoleResource", + "modelTypeName": "RoleResourceModel", + "serviceGroup": "roles", + "apiVersionDir": "v7", + "schema": { + "attributes": [ + { + "name": "is_builtin", + "goField": "IsBuiltin", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User).", + "wire": { + "jsonPath": "isBuiltin", + "sdkField": "IsBuiltin", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Name of the role.\n\nRequired-by-API by curation: the sweeper's field, unprobeable by omission.", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true + }, + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "permissions", + "goField": "Permissions", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "List of permission ids to grant (permissionId values from the permissions API). The response echoes permissions as expanded objects, never this id list, so state carries the configured value.\n\nEffectively required: a permission-less role create answers 500 (live probe, 2026-08-03).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": true, + "returnedOnCreate": true, + "returnedOnUpdate": true + }, + "wire": { + "jsonPath": "permissions", + "sdkField": "Permissions", + "sdkGoType": "[]string", + "expand": { + "func": "convert.FrameworkSetToStringSlice", + "needsCtx": true, + "returnsError": true + }, + "skipFlatten": true + } + }, + { + "name": "id", + "goField": "ID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID representing the role.\n\nNamed `id` per the import convention; the wire field stays roleId.", + "wire": { + "jsonPath": "roleId", + "sdkField": "RoleId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "markdownDescription": "Retrieve role." + }, + "binding": { + "service": { + "importPath": "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models", + "alias": "models", + "accessor": "r.client" + }, + "create": { + "style": "fluent", + "chain": [ + { + "method": "Roles" + }, + { + "method": "Post", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_RoleDetailable", + "httpMethod": "POST", + "pathTemplate": "/roles" + }, + "read": { + "style": "fluent", + "chain": [ + { + "method": "Roles" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Get", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_RoleDetailable", + "httpMethod": "GET", + "pathTemplate": "/roles/{id}" + }, + "update": { + "style": "fluent", + "chain": [ + { + "method": "Roles" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Put", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_RoleDetailable", + "httpMethod": "PUT", + "pathTemplate": "/roles/{id}" + }, + "delete": { + "style": "fluent", + "chain": [ + { + "method": "Roles" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Delete", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "error", + "httpMethod": "DELETE", + "pathTemplate": "/roles/{id}" + }, + "id": { + "attribute": "id", + "goField": "ID", + "fromCreate": "created.GetRoleId()", + "fromCreateIsPointer": true + }, + "body": { + "requestType": "models.Administrative_API_RoleRequestBody", + "responseType": "models.Administrative_API_RoleDetailable", + "constructorExpr": "models.NewAdministrative_API_RoleRequestBody()", + "accessStyle": "method" + } + }, + "policy": { + "updateStyle": "putFull", + "delete": { + "notFoundIsSuccess": true + } + }, + "import": { + "style": "passthroughId", + "attribute": "id" + }, + "accFixture": { + "values": [ + { + "attr": "permissions", + "hcl": "[\"1\", \"2\"]", + "wire": [ + "1", + "2" + ] + } + ] + }, + "accTest": { + "skipUnlessEnv": "TFPFGEN_ACC_ADMIN" + } + } + ] +} \ No newline at end of file diff --git a/blueprints/thousandeyes-kiota/resources/user.blueprint.json b/blueprints/thousandeyes-kiota/resources/user.blueprint.json new file mode 100644 index 00000000..e4b273d8 --- /dev/null +++ b/blueprints/thousandeyes-kiota/resources/user.blueprint.json @@ -0,0 +1,616 @@ +{ + "formatVersion": "4", + "resources": [ + { + "key": "user", + "name": "user", + "goPackage": "user", + "goPackageAlias": "v7User", + "goTypeName": "UserResource", + "modelTypeName": "UserResourceModel", + "serviceGroup": "users", + "apiVersionDir": "v7", + "schema": { + "attributes": [ + { + "name": "account_group_roles", + "goField": "AccountGroupRoles", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "UserAccountGroupRoleModel", + "sdkType": "models.Administrative_API_UserAccountGroupRoleable", + "attrTypesVar": "userAccountGroupRoleAttrTypes", + "objectTypeVar": "userAccountGroupRoleObjectType", + "expandFunc": "expandUserAccountGroupRoles", + "flattenFunc": "flattenUserAccountGroupRoles", + "attributes": [ + { + "name": "account_group_id", + "goField": "AccountGroupID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Unique ID of the account group.", + "wire": { + "jsonPath": "accountGroupId", + "sdkField": "AccountGroupId", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "skipFlatten": true + } + }, + { + "name": "role_ids", + "goField": "RoleIds", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Unique role IDs to grant in the account group.", + "wire": { + "jsonPath": "roleIds", + "sdkField": "RoleIds", + "sdkGoType": "[]string", + "expand": { + "func": "convert.FrameworkSetToStringSlice", + "needsCtx": true, + "returnsError": true + }, + "skipFlatten": true + } + } + ], + "constructorExpr": "models.NewAdministrative_API_UserAccountGroupRole()" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Role assignments to write: account group id plus role ids. The response echoes assignments as expanded objects under allAccountGroupRoles, never in this shape, so state carries the configured value.\n\nThe API refuses a user without at least one role (live probe, 2026-08-03).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": false + }, + "wire": { + "jsonPath": "accountGroupRoles", + "sdkField": "AccountGroupRoles", + "sdkGoType": "[]models.Administrative_API_UserAccountGroupRoleable", + "expand": { + "func": "expandUserAccountGroupRoles", + "needsCtx": true, + "returnsError": true + }, + "skipFlatten": true + } + }, + { + "name": "all_account_group_role_ids", + "goField": "AllAccountGroupRoleIDs", + "type": { + "kind": "set", + "elementType": { + "kind": "string" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Unique IDs representing the roles.\n\nThe response echoes roles as nested objects, never as this flat id list; state carries the configured value.\n\nOmitted from generated fixtures: its members are live role ids, and account_group_roles already carries the assignment the fixtures exercise.", + "behaviour": { + "returnedOnRead": false + }, + "wire": { + "jsonPath": "allAccountGroupRoleIds", + "sdkField": "AllAccountGroupRoleIds", + "sdkGoType": "[]string", + "expand": { + "func": "convert.FrameworkSetToStringSlice", + "needsCtx": true, + "returnsError": true + }, + "skipFlatten": true + } + }, + { + "name": "all_account_group_roles", + "goField": "AllAccountGroupRoles", + "type": { + "kind": "set_nested", + "nestedObject": { + "goTypeName": "UserRoleModel2", + "sdkType": "models.Administrative_API_Roleable", + "attrTypesVar": "userRoleAttrTypes2", + "objectTypeVar": "userRoleObjectType2", + "expandFunc": "expandUserRoles2", + "flattenFunc": "flattenUserRoles2", + "attributes": [ + { + "name": "has_management_permissions", + "goField": "HasManagementPermissions", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Flag indicating whether the user has management permissions.", + "wire": { + "jsonPath": "hasManagementPermissions", + "sdkField": "HasManagementPermissions", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "is_builtin", + "goField": "IsBuiltin", + "type": { + "kind": "bool" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User).", + "wire": { + "jsonPath": "isBuiltin", + "sdkField": "IsBuiltin", + "sdkGoType": "*bool", + "flatten": { + "func": "convert.PtrBoolToFramework" + }, + "skipExpand": true + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Name of the role.", + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "role_id", + "goField": "RoleID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID representing the role.", + "wire": { + "jsonPath": "roleId", + "sdkField": "RoleId", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "constructorExpr": "models.NewAdministrative_API_Role()" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Dropped in v1: read-only echo of accountGroupRoles in a different shape (expanded objects with value-typed nesting); mirroring it would drift against the writable half.", + "wire": { + "jsonPath": "allAccountGroupRoles", + "sdkField": "AllAccountGroupRoles", + "sdkGoType": "[]models.Administrative_API_Roleable", + "flatten": { + "func": "flattenUserRoles2", + "needsCtx": true, + "returnsError": true + }, + "skipExpand": true + }, + "drop": true + }, + { + "name": "date_registered", + "goField": "DateRegistered", + "type": { + "kind": "string", + "constraints": { + "format": "date-time" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "UTC date the user registered their account (ISO date-time format).", + "wire": { + "jsonPath": "dateRegistered", + "sdkField": "DateRegistered", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "email", + "goField": "Email", + "type": { + "kind": "string", + "constraints": { + "format": "email" + } + }, + "computedOptionalRequired": "optional", + "markdownDescription": "User's email address.\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true + }, + "wire": { + "jsonPath": "email", + "sdkField": "Email", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "last_login", + "goField": "LastLogin", + "type": { + "kind": "string", + "constraints": { + "format": "date-time" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "UTC last login of the user (ISO date-time format).", + "wire": { + "jsonPath": "lastLogin", + "sdkField": "LastLogin", + "sdkGoType": "*time.Time", + "flatten": { + "func": "convert.PtrTimeToFramework" + }, + "skipExpand": true + } + }, + { + "name": "login_account_group", + "goField": "LoginAccountGroup", + "type": { + "kind": "single_nested", + "nestedObject": { + "goTypeName": "UserAccountGroupModel2", + "sdkType": "models.Administrative_API_AccountGroupable", + "attrTypesVar": "userAccountGroupAttrTypes2", + "objectTypeVar": "userAccountGroupObjectType2", + "expandFunc": "expandUserAccountGroup2", + "flattenFunc": "flattenUserAccountGroup2", + "attributes": [ + { + "name": "account_group_name", + "goField": "AccountGroupName", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Account group name", + "wire": { + "jsonPath": "accountGroupName", + "sdkField": "AccountGroupName", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + }, + { + "name": "aid", + "goField": "AID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "A unique identifier associated with your account group. You can retrieve your `AccountGroupId` from the `/account-groups` endpoint.", + "wire": { + "jsonPath": "aid", + "sdkField": "Aid", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "constructorExpr": "models.NewAdministrative_API_AccountGroup()" + } + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Dropped in v1: read-only echo of loginAccountGroupId as a value-typed nested object.", + "wire": { + "jsonPath": "loginAccountGroup", + "sdkField": "LoginAccountGroup", + "sdkGoType": "models.Administrative_API_AccountGroupable", + "flatten": { + "func": "flattenUserAccountGroup2", + "needsCtx": true, + "returnsError": true + }, + "skipExpand": true + }, + "drop": true + }, + { + "name": "login_account_group_id", + "goField": "LoginAccountGroupID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "Unique ID of the login account group.\n\nThe response echoes loginAccountGroup as a nested object, never this id; state carries the configured value.\n\nThe API refuses a user without a login account group (live probe, 2026-08-03).\n\n\nThe API enforces this field's presence, which the specification does not declare.\n", + "behaviour": { + "requiredByApi": true, + "returnedOnRead": false + }, + "wire": { + "jsonPath": "loginAccountGroupId", + "sdkField": "LoginAccountGroupId", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "skipFlatten": true + } + }, + { + "name": "name", + "goField": "Name", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "optional", + "markdownDescription": "User's display name.\n\nRequired-by-API by curation: the sweeper's field, unprobeable by omission.", + "behaviour": { + "requiredByApi": true + }, + "wire": { + "jsonPath": "name", + "sdkField": "Name", + "sdkGoType": "*string", + "expand": { + "func": "convert.FrameworkToPtrString" + }, + "flatten": { + "func": "convert.PtrStringToFramework" + } + } + }, + { + "name": "id", + "goField": "ID", + "type": { + "kind": "string" + }, + "computedOptionalRequired": "computed", + "markdownDescription": "Unique ID of the user.\n\nNamed `id` per the import convention; the wire field stays uid.", + "wire": { + "jsonPath": "uid", + "sdkField": "Uid", + "sdkGoType": "*string", + "flatten": { + "func": "convert.PtrStringToFramework" + }, + "skipExpand": true + } + } + ], + "markdownDescription": "Retrieve user." + }, + "binding": { + "service": { + "importPath": "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models", + "alias": "models", + "accessor": "r.client" + }, + "create": { + "style": "fluent", + "chain": [ + { + "method": "Users" + }, + { + "method": "Post", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_CreatedUserable", + "httpMethod": "POST", + "pathTemplate": "/users" + }, + "read": { + "style": "fluent", + "chain": [ + { + "method": "Users" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Get", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_UserDetailable", + "httpMethod": "GET", + "pathTemplate": "/users/{id}" + }, + "update": { + "style": "fluent", + "chain": [ + { + "method": "Users" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Put", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "body" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "resultError", + "resultType": "models.Administrative_API_UserDetailable", + "httpMethod": "PUT", + "pathTemplate": "/users/{id}" + }, + "delete": { + "style": "fluent", + "chain": [ + { + "method": "Users" + }, + { + "method": "ById", + "args": [ + { + "kind": "stateField", + "field": "ID" + } + ] + }, + { + "method": "Delete", + "args": [ + { + "kind": "ctx" + }, + { + "kind": "literal", + "expr": "nil" + } + ] + } + ], + "return": "error", + "httpMethod": "DELETE", + "pathTemplate": "/users/{id}" + }, + "id": { + "attribute": "id", + "goField": "ID", + "fromCreate": "created.GetUid()", + "fromCreateIsPointer": true + }, + "body": { + "requestType": "models.Administrative_API_UserRequest", + "responseType": "models.Administrative_API_UserDetailable", + "constructorExpr": "models.NewAdministrative_API_UserRequest()", + "accessStyle": "method" + } + }, + "policy": { + "updateStyle": "putFull", + "delete": { + "notFoundIsSuccess": true + } + }, + "import": { + "style": "passthroughId", + "attribute": "id" + }, + "accFixture": { + "values": [ + { + "attr": "account_group_roles", + "hcl": "[{ account_group_id = \"281474976742769\", role_ids = [\"281474976759598\"] }]", + "wire": [ + { + "accountGroupId": "281474976742769", + "roleIds": [ + "281474976759598" + ] + } + ] + }, + { + "attr": "all_account_group_role_ids", + "hcl": "", + "omit": true + }, + { + "attr": "email", + "hcl": "\"dafydd.watkins+tfacc@deploymenttheory.com\"", + "wire": "dafydd.watkins+tfacc@deploymenttheory.com" + }, + { + "attr": "login_account_group_id", + "hcl": "\"281474976742769\"", + "wire": "281474976742769" + } + ] + }, + "accTest": { + "skipUnlessEnv": "TFPFGEN_ACC_ADMIN" + } + } + ] +} \ No newline at end of file diff --git a/internal/generate/fluent_test.go b/internal/generate/fluent_test.go index 72791aa5..69a1c2f0 100644 --- a/internal/generate/fluent_test.go +++ b/internal/generate/fluent_test.go @@ -175,6 +175,19 @@ func TestUnit_Generate_FluentSingleNestedConstructs(t *testing.T) { if !strings.Contains(joined, "item.SetNote(") { t.Errorf("single-object assignments must go through setters:\n%s", joined) } + + var flatten *NestedFuncView + for i := range v.State.NestedObject { + if v.State.NestedObject[i].FuncName == "flattenTagDetails" { + flatten = &v.State.NestedObject[i] + } + } + if flatten == nil { + t.Fatal("the single-object flatten helper was not built") + } + if flatten.InRef != "in" { + t.Errorf("InRef = %q, want in (an interface cannot be dereferenced)", flatten.InRef) + } } func TestUnit_Generate_FluentTestHelperRebasesTheChain(t *testing.T) { diff --git a/internal/generate/nested.go b/internal/generate/nested.go index 2621bbf4..c09878cb 100644 --- a/internal/generate/nested.go +++ b/internal/generate/nested.go @@ -384,11 +384,23 @@ func nestedItemRef(s nestedShape) string { return "&item" } +// nestedInRef is the flatten twin of nestedItemRef: how the single-object +// parameter becomes the local the assignments read. A struct-access helper +// receives *Model and dereferences it; a method-access helper receives the +// interface, which cannot be dereferenced and does not need to be. +func nestedInRef(s nestedShape) string { + if s.access == blueprint.AccessMethod { + return "in" + } + return "*in" +} + // nestedFlattenView builds the helper converting SDK structs into a framework // collection. func nestedFlattenView(s nestedShape) NestedFuncView { v := NestedFuncView{ SDKSingleType: nestedSingleType(s), + InRef: nestedInRef(s), FuncName: s.nested.FlattenFunc, FrameworkType: frameworkModelType[s.attr.Type.Kind], SDKType: s.nested.SDKType, diff --git a/internal/generate/render.go b/internal/generate/render.go index 429318a8..7837e02e 100644 --- a/internal/generate/render.go +++ b/internal/generate/render.go @@ -285,6 +285,9 @@ type NestedFuncView struct { // ItemRef is how a finished single element is returned: "&item" when item // is a value, "item" when the constructor already yielded the interface. ItemRef string + // InRef is how a single-object flatten reads its parameter: "*in" when the + // helper receives *Model, "in" when it receives the bare interface. + InRef string // SharedDiag is true when an assignment uses the enclosing shared d, which // is what makes declaring `var d` outside a loop body load-bearing; the // temp forms declare their own. diff --git a/internal/templates/_nested_flatten.go.tmpl b/internal/templates/_nested_flatten.go.tmpl index 3e1df7d8..db53a7e7 100644 --- a/internal/templates/_nested_flatten.go.tmpl +++ b/internal/templates/_nested_flatten.go.tmpl @@ -49,7 +49,7 @@ func {{ .FuncName }}(ctx context.Context, in {{ .SDKSingleType }}) ({{ .Framewor return types.ObjectNull({{ .ObjectTypeVar }}.AttrTypes), diags } - item := *in + item := {{ .InRef }} var m {{ .ModelType }} {{ range .Assignments }} diff --git a/pilot/thousandeyes-kiota/.tfpfgen/manifest.json b/pilot/thousandeyes-kiota/.tfpfgen/manifest.json index 46c17279..ba1d5529 100644 --- a/pilot/thousandeyes-kiota/.tfpfgen/manifest.json +++ b/pilot/thousandeyes-kiota/.tfpfgen/manifest.json @@ -4,272 +4,497 @@ "files": [ { "path": "internal/provider/actions.go", - "sha256": "f93b411834bc1eac6eb562032eebc37b1be29cc2ae9de7e4de4f1a10b25c19ec", + "sha256": "4684bff4fc6034aebf9c0de62e54419e74036924800ee479a0c9f83bc066cd2d", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/provider/datasources.go", - "sha256": "045c5fcabd27221824e90315d06544cfd7732cce0fb3de0f21c7666bb2a4348b", + "sha256": "c13c7c1f4911b7a2cf287b90aa383631e876b201c583ced8d43b28c0841c8d47", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/provider/ephemerals.go", - "sha256": "76b6dcb214300117db7d0e991d5f40364c8df5d96f13520725b22804641c4142", + "sha256": "5d92bd688df59680b1834e762a3f8200bc6da20fc26706131d006d1abb5e02db", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/provider/list_resources.go", - "sha256": "20658b43631011b9c12b847a1caf70d111159928f4cc04757006815675fec810", + "sha256": "07803a621f6f313a2e15d0fe0abc7d1a3e848e85611e4e8736832072b0a0ce1e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/provider/resources.go", - "sha256": "f5fc87f9bd232ab154ae48213faa36ea89a892282bb0d0be798d7d4b9e868f47", + "sha256": "dee0f9151fdd8e8ec6bc07b6431539d531078d0688724a5e3db714c17e8f5d7f", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action.go", - "sha256": "1d780007ab1192aa03d938b77b7e483d1fdb3b2a34e98718ab4bbe086d063261", + "sha256": "5d1908fac415256485686b4c4067b369cd16bd2113ade7ac8476e086e4b1015e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action_acceptance_test.go", - "sha256": "8bea8690a76989ffcde993129ed5681a4628d43366f43fcfe3d870a81e271a6d", + "sha256": "9e3dfaa3f39f77f240bd63693fe6b20245d252dfd827f46c61a8948a0f0ddc6d", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/invoke.go", - "sha256": "81cccc5882471dda322c3581f9dfacdfd9b2c6b10ccd6de95cfc0ea805af2f3b", + "sha256": "20bd516026c3b3ac78c9624fa6b8329ead5c79abd7352feb762ac3b29a884bf0", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/model.go", - "sha256": "38012938b519360d5189009416855224891c0bb363380c8d9bed33bf94719d59", + "sha256": "973fa43f9738f631d5b8589546517d099f3b954e6ef5033c0e50b528a8ed29ac", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/testdata/action.tf", - "sha256": "2b55944dbfb8d4a8eea5ac28e48d970cd5f5bb65c812dae835b1a00aca5c325a", + "sha256": "98703121080e548137584f6fbdab1c30e10fd63b3487db12a418e0be270a6d73", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/datasource.go", - "sha256": "c0dc61370be23690e790ad3333281717e2f34b51321238f9588770a07ff55eca", + "sha256": "50351bc465baa420a7731956bc4f64c45c4d18cbdb7bfc97b62536df14215826", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/datasource_acceptance_test.go", - "sha256": "1d535baac88ce31fedd552bc47d848d2fb668eb3a44fd5822169ccfb709f4e9c", + "sha256": "038ac9cac80e69efd5802ec07c53c52e4bcd2ccc333284124c12cc8b68bcb32f", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/model.go", - "sha256": "255d17450a623cc949cb2b6e761ea1b4a3fdb08b15a2fe38765c7c8d4b940b2a", + "sha256": "d8d95b3ada77bef8746a902c68df665f66f59800eb222187970f09d5d921e875", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/read.go", - "sha256": "b467ab6a89571bdf9dedde4e0bedf9ce16869532864105a0d90ec93612add899", + "sha256": "c860b8b85943bf14a57f69ffe26aa5fc12d366a34889704999e0bf2735df1821", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/seed_helper_test.go", - "sha256": "8a7de4a5702469c284035dde0e79d82bc3cb5ce9b7d3b65ca149c5f3d19b70a0", + "sha256": "5c0296dbb434a676b867d259d345c3ae7f8155d944d5ce7ed14ed20268ba9d29", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/state.go", - "sha256": "a5106e29b16790eeeaa9afa831e85f38e4df4a1b031364e39f93e9877d01f798", + "sha256": "cf697f3c3a343082d16dd8da0340d9149e070ece607c6e51f6a1b7668e3c50ae", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tag/testdata/datasource.tf", - "sha256": "8c559685a909f2ae481fa283561d9936db29a131168dd83e93ae50a348ae1b43", + "sha256": "1c2d89c4d71939ec0ab5836e7bc40fddbf859a5ef6bb37ed1ac37024aad46f9e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/datasource.go", - "sha256": "c1d18ef08add74715dc1fda31360c5531b1ea9f6b7b7c8f41462ff5729145553", + "sha256": "2b6cbff71b6860c7830de1978080f524a0059e21659951820bc231d249e93825", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/datasource_acceptance_test.go", - "sha256": "222ae6b9725d463522d268920ca32f4a8bda85b6439576617b76fd31c72605ba", + "sha256": "2a05a4e3189aea619cc4f77a41891e4500f657ebb4e362ce8bb8e22565a08776", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/model.go", - "sha256": "f1c3f87863e429b8a305089e4699fcd103f663f8d63635c531ea30b8cd761d3b", + "sha256": "25064023dce9eacfae42b7c6ce052302ea0011a17d1aa9bd8f4f9de68ab2b0fb", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/read.go", - "sha256": "e797084cb105ece0dd126f6002c09c7aabe51d9c4abe79107bfc64283e706141", + "sha256": "6957911bc9d562d4dafd8f9427cba603e59dabf4e658ff444739113de675945f", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/seed_helper_test.go", - "sha256": "dc4f744f8381c5456c9f5de9e476fff84b8bc6424fb2335817c7d2a40443eb75", + "sha256": "852158bc95b5e4cad51f3adba34702ae8f0ceffbdc5ddb961306802dfedc5b41", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/state.go", - "sha256": "d1bb32859bba8d61218140ed1f2c4f6e74311d1028ac2f4f08e5d41a6571c1b1", + "sha256": "8394b42abbca70937bb52731bf6ac2591c2a9330a3e0affbf40cd9f6a470709e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/datasources/tags/v7/tags/testdata/datasource.tf", - "sha256": "4432fc63955ff622ad867b7e97d89154be1ea8ec8816c198908b937e40318ca1", + "sha256": "5776e5f74b9b006a01b0f86206f145eb7aa242aea2dc487a3c89bef8006784e1", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/ephemeral.go", - "sha256": "159baf87b45e1d36ffcbefb82eac008674826b8bfed34badb1a7a9c52acf6e94", + "sha256": "701d8c8c68f8775149d1e028e98aa0d4a615e52c3a94dce7ae3d5937ed978a30", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/ephemeral_acceptance_test.go", - "sha256": "579f76bf9ec83a363babf0142e44a15d9e6fe17f1a96a05619b076fcf889cb9f", + "sha256": "4d118e168481004585a6cc477f150258efc57d9971245fbbb023c9dba1e2e780", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/model.go", - "sha256": "7e2e3701c171f224c9cab1ec2e7318f1b516ffd48c76debcde5cfd29f3b3a299", + "sha256": "07dfe33733a7ad1fc367611d1c831e2985cc149692529ecd27f9107e81e52f20", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/open.go", - "sha256": "e3a1524ccaf6638e827407e09e4aca58e93da7171cbe3813138c8df48682e7b5", + "sha256": "c22a3671a79f1a3bf3213d3c79bbe2e5ee00214e05797c532684a6ca494969ce", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/seed_helper_test.go", - "sha256": "7006d05ffe3c7264fb64d64e8892a911e53e462e69f307168ad3b1a8fc05d1b2", + "sha256": "eb568c7ddeab523bd97c1f51b3804e743bac9ef150bdb6a16bae16a542c0009e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/state.go", - "sha256": "51027435f53ee949f9d44c5bc57aa75e48aba037bb47aa70d18d05e542af5d3d", + "sha256": "b2bdb24be0371764803415299ef41b47b3e633e6d63ffd47f460203aef0ac23b", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/ephemerals/credentials/v7/credential/testdata/ephemeral.tf", - "sha256": "f058c5294c9853b5ef309ce8ca4427743d62e56147192b42ea9d1c86c60c5e15", + "sha256": "6526e5466a886ec0844166779669d66093ba08de3a590f43e8594858d40ff496", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/construct.go", + "sha256": "c7957c8b78eb4185ea883cd509b2f4893688414d15d5dcc1f78830217ef01367", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/crud.go", + "sha256": "9322acf337164c888d942a8649626f7f0b0ed12a2d4c4f81113f09b326c36acd", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/model.go", + "sha256": "d56679bc39b76c918fd293b4502c2eace841fe08c47009f46240d769955eb34b", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/resource.go", + "sha256": "0548b8051bfde8ecc0ebadec0924b87ed7633fc263880f4345501086ec0d75a0", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/resource_acceptance_test.go", + "sha256": "adf6f7c32b9218b101ec2b0a90c270130ada21311216459e726ca4ed721e2b23", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/state.go", + "sha256": "0c876c3d6e7ae421ebf18bd7567e13b7fc7a7fed0443bb46799aac59313cab6b", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/test_helper_test.go", + "sha256": "1ebb56ef29a7a24a5054bd63e35bf82366c57682fc4e43561af16ddb77577acf", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/testdata/maximal.tf", + "sha256": "5f8f05f6e79a9f2304c32502924aaaa1eeca5c78b74c42f78341db25c1864da5", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/account_groups/v7/account_group/testdata/minimal.tf", + "sha256": "d6b60b5d8b19bc6f33e83117043078ae271656bf318df0a19a0568000e249f55", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/construct.go", + "sha256": "80917c5e36caf7e562e8ee7a531bcfd47665891306759254b8ee26e586b74497", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/crud.go", + "sha256": "62d64eaed24af7ae754dad524e6a3e787359bfb311a21f5adfd45e738441f715", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/model.go", + "sha256": "0bdcb28a56911bc8576d71c76ad98d405e985d29e695ba0359cc03076e167e42", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource.go", + "sha256": "925c24ca1d947de4516dfdf93162db1c18ed77997e8b03de91aa7910c6a464bd", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource_acceptance_test.go", + "sha256": "d9ef09c1b3c6b89df3df43ee418a22279c09a0e75042184e2ba8c07718b5c955", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/state.go", + "sha256": "a02c416ae059e842576dfff1c49afcce080a9b2531691fe1bc5e860039e041dc", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/test_helper_test.go", + "sha256": "c8e45dcb774246f4acdfbe84a13d4616d61e3308348ecca2d5a8c6e131125402", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/maximal.tf", + "sha256": "0aec7b5dbd0d92d78d4dc07ec21590c783fa79e4bea6534d5a1e924bc7aba3d3", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/minimal.tf", + "sha256": "11170b8d1de5842fedfab3281d10591241f5f8e8615d3d54916f781acc4da612", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/construct.go", - "sha256": "a4761b45813ab58bbac28157e83ae0ad3d6502edbeaf05e0aa18ad364d2332ca", + "sha256": "ccf47074266009bf48395eedba54a717d51ebd4d9bd2387526a088cc6ff9ac07", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/crud.go", - "sha256": "4f20da4d48ec2f88a3642d4e8b8985300bacf49d3093369d1bcffa2d2cbdf2ea", + "sha256": "0bf3a615ff848eb1e28ba195ea38059c20d41a4489873f08534e19abb7ae8904", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/model.go", - "sha256": "d902a12ffc2a29f6eac3f05abe5d7bf689cea96cc376d62fdf9966d6c6d2bbdf", + "sha256": "e945a5df1a2ca596d8489e1d56fda4248f6615ee5f2db6bcb41e0b1c1e363e8c", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/resource.go", - "sha256": "e06992b1f1ed4fa5d85b57ef005dcf467241e9776e56efba0ed35444a4bef547", + "sha256": "2eb068c055eff0a65bb2e8880d0e7bb53f4eb8d58320dc6139114e2225df38b8", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/resource_acceptance_test.go", - "sha256": "cb566bb29488a70fe4e59dee47d36dd5173e51a46a48562ae6176c2dc181878a", + "sha256": "6b3ad98f3b5623443b83e11d10ebf236e67a36787bdc17f005c370356b1646f7", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/state.go", - "sha256": "e7ea8e952ed8376a6e727dafbc60ccc4f39cd26519e46b4f92a3d904268ab851", + "sha256": "10d534fb061bddf9d42eda05fe4e9d2546ca3424c4d6e81b3342fa11856e3ffe", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/test_helper_test.go", - "sha256": "7006d05ffe3c7264fb64d64e8892a911e53e462e69f307168ad3b1a8fc05d1b2", + "sha256": "eb568c7ddeab523bd97c1f51b3804e743bac9ef150bdb6a16bae16a542c0009e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/testdata/maximal.tf", - "sha256": "c3bf18d11becf67c946612686f10d6d33f12bfaf9a0563d9e8a694684ff5e442", + "sha256": "1c1b548fc53473ae83f51a923b8604204c8ed273b59e45f368003b48bbb9c7eb", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/credentials/v7/credential/testdata/minimal.tf", - "sha256": "3774300d163ddaef9585a6dfec23717105683634e706e5c6034b48b21a5b8d69", + "sha256": "1cacf7f40622e2cb36e49f48c64e076dab3c7da4dad30c11fd7a5e5e00ad13fb", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/construct.go", + "sha256": "38da38d9d89b481c8c9cb6b59de8a8ab9979c122617caf8e2437d655e0d74ac9", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/crud.go", + "sha256": "914bf51e458ee3b8eed5e757544e4f69ce77ba60b36f1099447642db74701ccf", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/model.go", + "sha256": "bcc8101704a32bf1d428f766fffd240f083c03de990fd56b975132fd52e1416d", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource.go", + "sha256": "32b5ef0e37177d256b5fc727493723185433df054f5de497118094ab1730c675", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource_acceptance_test.go", + "sha256": "9f7b198705de32c9050867084df5aa2174a7dda4ae32e9b5c183602fe2574a1f", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/state.go", + "sha256": "f85567d0e61bd74cdce379e42333ab8618fa0fc995cdc817d197b03dbd56194b", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/test_helper_test.go", + "sha256": "c72d89806e8b8330a9b78ae47ed1e294ba28a427126b7aa7da767829c1935c41", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/maximal.tf", + "sha256": "83aebd72fd42b172c0b818c616bce7b0b450f82b437ec941da9276d7252e09fe", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/minimal.tf", + "sha256": "bc628b26af42a9f004f01bf4381c9fbee779f21ee5d8917b5a2268f8c7d46a10", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/construct.go", + "sha256": "19339e435322e206ca5ebadcf0d3ae0034b0f2e7f3bcc07c6b7689c89fec21d8", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/crud.go", + "sha256": "245a05858881cea6c50c64d576e71b85b3d5dedb1e385fae69d21db44d99cc67", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/model.go", + "sha256": "3af4bdcd03a64a92b453f8e7179a2b5c04e764ab0dabcd52b215928652701612", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/resource.go", + "sha256": "44b6cec6bde2508d759e313678cbc7ddab1fc47659d43c8028ddec7ce57ec501", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/resource_acceptance_test.go", + "sha256": "6951191e2f4258f7679c83da798985612693e47f38c2a1b1ab102c6bec164994", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/state.go", + "sha256": "518124451e928da31fa99481446c30923a5d070b6d927e47a2d25d70e8386ad2", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/test_helper_test.go", + "sha256": "8f9ac667b626b3773e95802a23590f96870b859d0352032e20f6784acda9b056", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/testdata/maximal.tf", + "sha256": "8115abb8831a35bd80ba4ac36d026f56bdca9d10b1f9abc85fee7cfb93cf4ad0", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/roles/v7/role/testdata/minimal.tf", + "sha256": "1c09328f2a43170f22b5a23ee3898842c38323c3b5d5c67723a540958823388d", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/conditional_validators.go", - "sha256": "04849f04315f211f09c2af06c14eb674273b5069d46c9b47eb8e2fc9d725a0f9", + "sha256": "83c3edfea90a79f10bec80bdf48012811f8680c2e4213c36252b0d469f16caa5", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/construct.go", - "sha256": "77fbea94f37047fb6aadc1a31192f89946b03a418601061f23a0c4bd1f8293d7", + "sha256": "8b5ffb30dc3fa999ad8c89a20fd23e664f43f8ecde046333039cf5930bed0f3d", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/crud.go", - "sha256": "3746d0788aabb50cc3e7993ee307cad28afd51f0d09b6a11419f8e7e1389fa45", + "sha256": "e3368e6008031264bfe4643f1dd3d21eb8d15a0b229df314890dbe56563ef730", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/list_acceptance_test.go", - "sha256": "3a74d84cf51b6e132ce4320b376609ea96e4ac81e88f777dfdb44f57f2438b76", + "sha256": "ccd9fbf7145ef0f6195ad8889af44c268dae9e8ea3ab0763ed0881595b39b08e", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/list_resource.go", - "sha256": "cbdc47d0fac378b055923c248fbdea255e21f930ea455701968f6bf3da1f5286", + "sha256": "32fc65f3f48a975a086cd00abc1328143d3a99663a2d36df396bb82f20b9abab", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/model.go", - "sha256": "2e4761077da6f39284f994523b0c73d5503da77983d269427e2a8b9e05afe317", + "sha256": "1ce290e0a5ebd4b506eceab9bca0ecd028741f46bb8328a54101901864be4f97", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/resource.go", - "sha256": "4cba7bce84251c1e3eef35cea27ec4b5158af785161a6b61964eef3d5d8f4a47", + "sha256": "3abd117fcba7ce660a239ede9a4a209d40d71a91a9d60fbe13819c3f327a82cd", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/resource_acceptance_test.go", - "sha256": "f75f0f1c76bd78885b2ea4acaf3c693792d1f07bdefc5e57b66eb34d6b619421", + "sha256": "72b371b043bd355716d57385308897ee4adbbfbd0be352e5a98b198aea3eb978", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/state.go", - "sha256": "c400efddb3b54ba4f12c60d8103ec3ed50355ac0420dcd463af48cb81d29dd5b", + "sha256": "33850bcd2e14ddedbe1ad9b728bbf683a2d7545947f533962f4c7e10dbe75525", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/test_helper_test.go", - "sha256": "8a7de4a5702469c284035dde0e79d82bc3cb5ce9b7d3b65ca149c5f3d19b70a0", + "sha256": "5c0296dbb434a676b867d259d345c3ae7f8155d944d5ce7ed14ed20268ba9d29", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/testdata/maximal.tf", - "sha256": "6a2c9f8699d0538db1a64e7ea8618f96e8f85d72203245eb6a329bea3253b10b", + "sha256": "99a7cbf9f52b65b4414e1293d212eabcaa7055b4988793214df99f08491548ac", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/testdata/minimal.tf", - "sha256": "3615f0fd07006619258a6035c19b8cf73b34bb2044d564fe9a1d6953bf3f9316", + "sha256": "84be75c5dfdb93b4555ae22eb5b2e6cb350f1f81868ba2afbd58ca7cf9921a92", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/testdata/query.tf", - "sha256": "69aa217c0d67226eee78f0b218ceb99402f3695421872835b04541910c0a92fe", + "sha256": "290ba63ce609d7553a8f30167d020c422fc936355acaa043bc26c745a2002474", "blueprint": "blueprints/thousandeyes-kiota" }, { "path": "internal/services/resources/tags/v7/tag/testdata/replace.tf", - "sha256": "d48b074259fdae92f1fdca4ef3a67ace072fe32b6d05f0323f1039d5c87d629f", + "sha256": "d8c651b8812e8a1143785226c7fa6c175caca8fbeb959292e1cdd6adc4219443", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/construct.go", + "sha256": "157239f2404b54a6e436c107c9f27ebd831bedb7133de82b8c83ec774b1e0dd1", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/crud.go", + "sha256": "6e6f6138cf00d72f90386991ad8ce1218ab7d795927fe5ff403d5e651497d603", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/model.go", + "sha256": "d826256297e05c497a02f290c0889e2b21557d6f85f9316c8e15bf09bab391b8", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/resource.go", + "sha256": "994c42d9baa140a70ea3d6dfa0a751b68bf1f597cf4e100beccbcf4ca5ab35cb", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/resource_acceptance_test.go", + "sha256": "cb4c04d3fac8b1179b6f4e4d84ec6897e0465ca1a16d89709e2eebe234f8df90", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/state.go", + "sha256": "6d83ce660a403b5863f3a02c70bb22f6ef5f5303985b4081d191bee1b2b1863a", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/test_helper_test.go", + "sha256": "0b553bb471fe9aee03df7324ec7aa3ba57dfbc071c1f0c07635f70d4e3a693be", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/testdata/maximal.tf", + "sha256": "a913243a9907e032698dd2c7089ce11e7d7f4e8748fab9b92104cfe7c5aaded5", + "blueprint": "blueprints/thousandeyes-kiota" + }, + { + "path": "internal/services/resources/users/v7/user/testdata/minimal.tf", + "sha256": "bae0ecaeba4da9b97d5731fccbcd98e032c5855b492c1dc02681b751f927ac8c", "blueprint": "blueprints/thousandeyes-kiota" } ] diff --git a/pilot/thousandeyes-kiota/docs/resources/account_group.md b/pilot/thousandeyes-kiota/docs/resources/account_group.md new file mode 100644 index 00000000..b31669d2 --- /dev/null +++ b/pilot/thousandeyes-kiota/docs/resources/account_group.md @@ -0,0 +1,69 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "thousandeyes_account_group Resource - ThousandEyes" +subcategory: "" +description: |- + Retrieve account group. +--- + +# thousandeyes_account_group (Resource) + +Retrieve account group. + +## Example Usage + +```terraform +resource "thousandeyes_account_group" "example" { + account_group_name = "tfacc-account-group-name" +} +``` + + +## Schema + +### Optional + +- `account_group_name` (String) Account group name Required-by-API by curation: the sweeper's field, unprobeable by omission. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) A unique identifier associated with your account group. You can retrieve your `AccountGroupId` from the `/account-groups` endpoint. Named `id` per the import convention; the wire field stays aid. +- `is_current_account_group` (Boolean) Indicates whether the requested aid is the context of the current account. +- `is_default_account_group` (Boolean) Indicates whether the aid is the default one for the requesting user. +- `org_id` (String) (Optional) The ID for the organization associated with the account group. +- `organization_name` (String) (Optional) The name of the organization associated with the account group. +- `users` (Attributes Set) (see [below for nested schema](#nestedatt--users)) + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `users` + +Read-Only: + +- `date_registered` (String) User's UTC registration date (ISO date-time format). +- `email` (String) User's email address. +- `last_login` (String) User's UTC last login date (ISO date-time format). +- `name` (String) User's display name. +- `roles` (Attributes Set) (see [below for nested schema](#nestedatt--users--roles)) +- `uid` (String) Unique ID representing the user. + + +### Nested Schema for `users.roles` + +Read-Only: + +- `has_management_permissions` (Boolean) Flag indicating whether the user has management permissions. +- `is_builtin` (Boolean) Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User). +- `name` (String) Name of the role. +- `role_id` (String) Unique ID representing the role. diff --git a/pilot/thousandeyes-kiota/docs/resources/alert_suppression_window.md b/pilot/thousandeyes-kiota/docs/resources/alert_suppression_window.md new file mode 100644 index 00000000..b4ce5468 --- /dev/null +++ b/pilot/thousandeyes-kiota/docs/resources/alert_suppression_window.md @@ -0,0 +1,72 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "thousandeyes_alert_suppression_window Resource - ThousandEyes" +subcategory: "" +description: |- + Retrieve alert suppression window. +--- + +# thousandeyes_alert_suppression_window (Resource) + +Retrieve alert suppression window. + +## Example Usage + +```terraform +resource "thousandeyes_alert_suppression_window" "example" { + duration = 3600 + name = "tfacc-name" + repeat = { type = "none" } + start_date = "2027-06-01T00:00:00Z" +} +``` + + +## Schema + +### Optional + +- `duration` (Number) Duration in seconds the suppression window is active. The API refuses a window without a duration (live probe, 2026-08-03). The API enforces this field's presence, which the specification does not declare. +- `end_repeat` (Attributes) End repeat options. The API assigns { type = "never" } when omitted (live acceptance, 2026-08-03). (see [below for nested schema](#nestedatt--end_repeat)) +- `is_enabled` (Boolean) Set to `false` for `disabled`, `true` for `enabled`. +- `name` (String) Name of the alert suppression window. The API refuses a window without a name (live acceptance, 2026-08-03); the probe cannot test this itself because name carries the sweeper's prefix. +- `repeat` (Attributes) Repeat options. The API requires repeat conditions on every window; a one-off window carries `type = "none"` (live probe, 2026-08-03). The API enforces this field's presence, which the specification does not declare. (see [below for nested schema](#nestedatt--repeat)) +- `start_date` (String) The date/time when the alert suppression window starts (ISO date-time format). The API enforces this field's presence, which the specification does not declare. +- `test_ids` (Set of String) List of test IDs to assign to the alert suppression window. The response never carries testIds -- it echoes the linked tests as a nested collection instead -- so state carries the configured value. Neither the update response nor a read returns this field: it is write-only in practice, and state carries the configured value. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) Unique ID of the alert suppression window. Named `id` because terraform-plugin-testing populates import state only from an attribute literally called id; the wire field stays alertSuppressionWindowId. +- `status` (String) Indicates the current status of the suppression window. + + +### Nested Schema for `end_repeat` + +Optional: + +- `count` (Number) End repeat after number of occurrences, only valid with count type option. +- `date` (String) End repeat after specific date, only valid with date type option (ISO date format). +- `type` (String) End repeat options type. Values accepted here: `count`, `never`, `date`. Observed: the API assigns "never" when this is omitted. The API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute. + + + +### Nested Schema for `repeat` + +Optional: + +- `days_of_week` (Set of String) +- `interval_length` (Number) Number of `intervalTypes` to wait before reactivating the alert suppression window. +- `interval_type` (String) Repeat options interval type Values accepted here: `day`, `week`, `month`. The API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute. +- `type` (String) Repeat options type. Values accepted here: `day`, `week`, `month`, `custom`. Observed: the API assigns "none" when this is omitted. The API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/pilot/thousandeyes-kiota/docs/resources/endpoint_label.md b/pilot/thousandeyes-kiota/docs/resources/endpoint_label.md new file mode 100644 index 00000000..08b1cf5b --- /dev/null +++ b/pilot/thousandeyes-kiota/docs/resources/endpoint_label.md @@ -0,0 +1,56 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "thousandeyes_endpoint_label Resource - ThousandEyes" +subcategory: "" +description: |- + Retrieve label. +--- + +# thousandeyes_endpoint_label (Resource) + +Retrieve label. + +## Example Usage + +```terraform +resource "thousandeyes_endpoint_label" "example" { + filters = [{ key = "platform", mode = "in", values = ["windows"] }] + match_type = "and" + name = "tfacc-name" +} +``` + + +## Schema + +### Optional + +- `color` (String) UI color Observed: the API assigns "#d9d9d9" when this is omitted. +- `filters` (Attributes Set) The filters combined using the matchType to determine the label's match. The API refuses a label without at least one filter (live probe, 2026-08-04). Only returned when the read asks for expand=filters; the generated read always does (live probe, 2026-08-04). The API enforces this field's presence, which the specification does not declare. (see [below for nested schema](#nestedatt--filters)) +- `match_type` (String) Specifies how the filters are combined for matching this label. * `and`: All filters must be matched. * `or`: Any of the filters should match. Values accepted here: `and`, `or`. The API enforces this field's presence, which the specification does not declare. +- `name` (String) The label name. Required-by-API by curation: the sweeper's field, unprobeable by omission. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) Label identifier. + + +### Nested Schema for `filters` + +Optional: + +- `key` (String) Type of filter - the data that will be used to filter. Values accepted here: `agent-id`, `public-network`, `local-network`, `connection`, `gateway`, `platform`, `agent-type`, `vpn-vendor`, `vpn-gateway-address`, `vpn-client-network`, `vpn-client-address`, `ip-address-family`, `ssid`, `bssid`, `hostname`, `username`, `asn`. Observed: the API assigns "platform" when this is omitted. The API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute. +- `mode` (String) Type of matching to be applied for the values: * `in`: The value on the agent must match one of the list of values provided. * `not-in`: The value on the agent must not match any of the list of values provided. Values accepted here: `in`, `not-in`. Observed: the API assigns "in" when this is omitted. The API accepted a value from outside the documented set, so no OneOf validator is generated for this attribute. +- `values` (Set of String) + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/pilot/thousandeyes-kiota/docs/resources/role.md b/pilot/thousandeyes-kiota/docs/resources/role.md new file mode 100644 index 00000000..c724eacf --- /dev/null +++ b/pilot/thousandeyes-kiota/docs/resources/role.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "thousandeyes_role Resource - ThousandEyes" +subcategory: "" +description: |- + Retrieve role. +--- + +# thousandeyes_role (Resource) + +Retrieve role. + +## Example Usage + +```terraform +resource "thousandeyes_role" "example" { + name = "tfacc-name" + permissions = ["1", "2"] +} +``` + + +## Schema + +### Optional + +- `name` (String) Name of the role. Required-by-API by curation: the sweeper's field, unprobeable by omission. +- `permissions` (Set of String) List of permission ids to grant (permissionId values from the permissions API). The response echoes permissions as expanded objects, never this id list, so state carries the configured value. Effectively required: a permission-less role create answers 500 (live probe, 2026-08-03). The API enforces this field's presence, which the specification does not declare. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) Unique ID representing the role. Named `id` per the import convention; the wire field stays roleId. +- `is_builtin` (Boolean) Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User). + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/pilot/thousandeyes-kiota/docs/resources/user.md b/pilot/thousandeyes-kiota/docs/resources/user.md new file mode 100644 index 00000000..18f237aa --- /dev/null +++ b/pilot/thousandeyes-kiota/docs/resources/user.md @@ -0,0 +1,61 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "thousandeyes_user Resource - ThousandEyes" +subcategory: "" +description: |- + Retrieve user. +--- + +# thousandeyes_user (Resource) + +Retrieve user. + +## Example Usage + +```terraform +resource "thousandeyes_user" "example" { + account_group_roles = [{ account_group_id = "281474976742769", role_ids = ["281474976759598"] }] + email = "dafydd.watkins+tfacc@deploymenttheory.com" + login_account_group_id = "281474976742769" + name = "tfacc-name" +} +``` + + +## Schema + +### Optional + +- `account_group_roles` (Attributes Set) Role assignments to write: account group id plus role ids. The response echoes assignments as expanded objects under allAccountGroupRoles, never in this shape, so state carries the configured value. The API refuses a user without at least one role (live probe, 2026-08-03). The API enforces this field's presence, which the specification does not declare. (see [below for nested schema](#nestedatt--account_group_roles)) +- `all_account_group_role_ids` (Set of String) Unique IDs representing the roles. The response echoes roles as nested objects, never as this flat id list; state carries the configured value. Omitted from generated fixtures: its members are live role ids, and account_group_roles already carries the assignment the fixtures exercise. +- `email` (String) User's email address. The API enforces this field's presence, which the specification does not declare. +- `login_account_group_id` (String) Unique ID of the login account group. The response echoes loginAccountGroup as a nested object, never this id; state carries the configured value. The API refuses a user without a login account group (live probe, 2026-08-03). The API enforces this field's presence, which the specification does not declare. +- `name` (String) User's display name. Required-by-API by curation: the sweeper's field, unprobeable by omission. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `date_registered` (String) UTC date the user registered their account (ISO date-time format). +- `id` (String) Unique ID of the user. + +Named `id` per the import convention; the wire field stays uid. +- `last_login` (String) UTC last login of the user (ISO date-time format). + + +### Nested Schema for `account_group_roles` + +Optional: + +- `account_group_id` (String) Unique ID of the account group. +- `role_ids` (Set of String) Unique role IDs to grant in the account group. + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). diff --git a/pilot/thousandeyes-kiota/examples/resources/thousandeyes_account_group/resource.tf b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_account_group/resource.tf new file mode 100644 index 00000000..7012b3e7 --- /dev/null +++ b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_account_group/resource.tf @@ -0,0 +1,3 @@ +resource "thousandeyes_account_group" "example" { + account_group_name = "tfacc-account-group-name" +} diff --git a/pilot/thousandeyes-kiota/examples/resources/thousandeyes_alert_suppression_window/resource.tf b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_alert_suppression_window/resource.tf new file mode 100644 index 00000000..3e663aad --- /dev/null +++ b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_alert_suppression_window/resource.tf @@ -0,0 +1,6 @@ +resource "thousandeyes_alert_suppression_window" "example" { + duration = 3600 + name = "tfacc-name" + repeat = { type = "none" } + start_date = "2027-06-01T00:00:00Z" +} diff --git a/pilot/thousandeyes-kiota/examples/resources/thousandeyes_endpoint_label/resource.tf b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_endpoint_label/resource.tf new file mode 100644 index 00000000..535b8c2c --- /dev/null +++ b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_endpoint_label/resource.tf @@ -0,0 +1,5 @@ +resource "thousandeyes_endpoint_label" "example" { + filters = [{ key = "platform", mode = "in", values = ["windows"] }] + match_type = "and" + name = "tfacc-name" +} diff --git a/pilot/thousandeyes-kiota/examples/resources/thousandeyes_role/resource.tf b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_role/resource.tf new file mode 100644 index 00000000..cf1af8d5 --- /dev/null +++ b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_role/resource.tf @@ -0,0 +1,4 @@ +resource "thousandeyes_role" "example" { + name = "tfacc-name" + permissions = ["1", "2"] +} diff --git a/pilot/thousandeyes-kiota/examples/resources/thousandeyes_user/resource.tf b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_user/resource.tf new file mode 100644 index 00000000..20de1e6f --- /dev/null +++ b/pilot/thousandeyes-kiota/examples/resources/thousandeyes_user/resource.tf @@ -0,0 +1,6 @@ +resource "thousandeyes_user" "example" { + account_group_roles = [{ account_group_id = "281474976742769", role_ids = ["281474976759598"] }] + email = "dafydd.watkins+tfacc@deploymenttheory.com" + login_account_group_id = "281474976742769" + name = "tfacc-name" +} diff --git a/pilot/thousandeyes-kiota/internal/provider/actions.go b/pilot/thousandeyes-kiota/internal/provider/actions.go index f2c55c4a..7fd30e6d 100644 --- a/pilot/thousandeyes-kiota/internal/provider/actions.go +++ b/pilot/thousandeyes-kiota/internal/provider/actions.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package provider diff --git a/pilot/thousandeyes-kiota/internal/provider/datasources.go b/pilot/thousandeyes-kiota/internal/provider/datasources.go index 1e956c95..ed9e4324 100644 --- a/pilot/thousandeyes-kiota/internal/provider/datasources.go +++ b/pilot/thousandeyes-kiota/internal/provider/datasources.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package provider diff --git a/pilot/thousandeyes-kiota/internal/provider/ephemerals.go b/pilot/thousandeyes-kiota/internal/provider/ephemerals.go index afa32b59..d6af8f88 100644 --- a/pilot/thousandeyes-kiota/internal/provider/ephemerals.go +++ b/pilot/thousandeyes-kiota/internal/provider/ephemerals.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package provider diff --git a/pilot/thousandeyes-kiota/internal/provider/list_resources.go b/pilot/thousandeyes-kiota/internal/provider/list_resources.go index 3c89a917..2a095935 100644 --- a/pilot/thousandeyes-kiota/internal/provider/list_resources.go +++ b/pilot/thousandeyes-kiota/internal/provider/list_resources.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package provider diff --git a/pilot/thousandeyes-kiota/internal/provider/resources.go b/pilot/thousandeyes-kiota/internal/provider/resources.go index 8a598fa2..7ec5f091 100644 --- a/pilot/thousandeyes-kiota/internal/provider/resources.go +++ b/pilot/thousandeyes-kiota/internal/provider/resources.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package provider @@ -8,8 +8,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" + v7AccountGroup "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group" + v7AlertSuppressionWindow "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window" v7Credential "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential" + v7EndpointLabel "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label" + v7Role "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role" v7Tag "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag" + v7User "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user" ) // Resources returns every managed resource this provider serves. @@ -18,7 +23,12 @@ import ( // regenerating, not by editing here. func (p *Provider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ + v7AccountGroup.NewAccountGroupResource, + v7AlertSuppressionWindow.NewAlertSuppressionWindowResource, v7Credential.NewCredentialResource, + v7EndpointLabel.NewEndpointLabelResource, + v7Role.NewRoleResource, v7Tag.NewTagResource, + v7User.NewUserResource, } } diff --git a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action.go b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action.go index f3168c62..68e000c8 100644 --- a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action.go +++ b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/disable-endpoint-agent/ package disable_endpoint_agent diff --git a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action_acceptance_test.go index 250741c9..88f718d8 100644 --- a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/action_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package disable_endpoint_agent_test diff --git a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/invoke.go b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/invoke.go index dd55301e..5d4c89a6 100644 --- a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/invoke.go +++ b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/invoke.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package disable_endpoint_agent diff --git a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/model.go b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/model.go index f4c21e0b..042285c2 100644 --- a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/model.go +++ b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package disable_endpoint_agent diff --git a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/testdata/action.tf b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/testdata/action.tf index 1ec90b41..5e1b58ea 100644 --- a/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/testdata/action.tf +++ b/pilot/thousandeyes-kiota/internal/services/actions/endpoint_agents/v7/disable_endpoint_agent/testdata/action.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The action under test, invoked once by a terraform_data trigger on create. The # subject's identifier arrives as a variable, supplied by the generated test from the diff --git a/pilot/thousandeyes-kiota/internal/services/common/convert/convert.go b/pilot/thousandeyes-kiota/internal/services/common/convert/convert.go index 4435b94e..c360a521 100644 --- a/pilot/thousandeyes-kiota/internal/services/common/convert/convert.go +++ b/pilot/thousandeyes-kiota/internal/services/common/convert/convert.go @@ -29,12 +29,14 @@ package convert import ( "context" "fmt" + "strconv" "time" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/microsoft/kiota-abstractions-go/serialization" ) // ----------------------------------------------------------------------------- @@ -461,3 +463,119 @@ func FrameworkToKiotaEnum[T any](v types.String, parse func(string) (any, error) return typed, diags } + +// PtrInt32ToFrameworkInt64 widens an optional int32 -- the Kiota SDK types +// every plain integer property as *int32 -- into the types.Int64 the curated +// schema declares, so the schema does not have to change shape with the SDK. +func PtrInt32ToFrameworkInt64[T ~int32](p *T) types.Int64 { + if p == nil { + return types.Int64Null() + } + return types.Int64Value(int64(*p)) +} + +// FrameworkInt64ToPtrInt32 is the write-side mirror of PtrInt32ToFrameworkInt64. +// Not generic: a setter parameter gives Go no way to infer a type argument, and +// the Kiota SDK types every plain integer as exactly *int32. +func FrameworkInt64ToPtrInt32(v types.Int64) *int32 { + if v.IsNull() || v.IsUnknown() { + return nil + } + out := int32(v.ValueInt64()) + return &out +} + +// PtrInt32ToFrameworkString renders an optional int32 as a string attribute -- +// for a property the curated schema models as a string (a test interval) where +// the Kiota SDK carries the raw integer. +func PtrInt32ToFrameworkString[T ~int32](p *T) types.String { + if p == nil { + return types.StringNull() + } + return types.StringValue(strconv.FormatInt(int64(*p), 10)) +} + +// FrameworkToPtrTime parses an RFC 3339 string attribute into the *time.Time a +// Kiota write model wants. RFC 3339 is the only accepted spelling because it is +// the shape the API echoes back -- accepting a looser format here would create +// a value the read side can never render identically, which Terraform reports +// as a permanent diff. +func FrameworkToPtrTime(v types.String) (*time.Time, diag.Diagnostics) { + var diags diag.Diagnostics + + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + t, err := time.Parse(time.RFC3339, v.ValueString()) + if err != nil { + diags.AddError( + "Invalid timestamp", + fmt.Sprintf("%q is not an RFC 3339 timestamp (2027-06-01T00:00:00Z): %s", v.ValueString(), err), + ) + return nil, diags + } + return &t, diags +} + +// PtrDateOnlyToFramework converts an optional date-only value to types.String +// in the yyyy-mm-dd shape it arrived in on the wire. +func PtrDateOnlyToFramework(p *serialization.DateOnly) types.String { + if p == nil { + return types.StringNull() + } + return types.StringValue(p.String()) +} + +// FrameworkToPtrDateOnly parses a yyyy-mm-dd string attribute into the +// *serialization.DateOnly a Kiota write model wants. +func FrameworkToPtrDateOnly(v types.String) (*serialization.DateOnly, diag.Diagnostics) { + var diags diag.Diagnostics + + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + d, err := serialization.ParseDateOnly(v.ValueString()) + if err != nil { + diags.AddError( + "Invalid date", + fmt.Sprintf("%q is not a yyyy-mm-dd date: %s", v.ValueString(), err), + ) + return nil, diags + } + return d, diags +} + +// KiotaEnumSliceToFrameworkSet renders a slice of Kiota enumeration values as +// a set of their wire strings -- a suppression window's daysOfWeek. +func KiotaEnumSliceToFrameworkSet[T fmt.Stringer](ctx context.Context, v []T) (types.Set, diag.Diagnostics) { + if v == nil { + return StringSliceToFrameworkSet(ctx, nil) + } + raw := make([]string, len(v)) + for i, e := range v { + raw[i] = e.String() + } + return StringSliceToFrameworkSet(ctx, raw) +} + +// FrameworkSetToKiotaEnumSlice converts a set of strings into a slice of Kiota +// enumeration values through the SDK's ParseX companion, refusing any string +// outside the closed set for the same reason FrameworkToKiotaEnum does. +func FrameworkSetToKiotaEnumSlice[T any](ctx context.Context, v types.Set, parse func(string) (any, error)) ([]T, diag.Diagnostics) { + raw, diags := FrameworkSetToStringSlice(ctx, v) + if raw == nil { + return nil, diags + } + + out := make([]T, 0, len(raw)) + for _, s := range raw { + one, oneDiags := FrameworkToKiotaEnum[T](types.StringValue(s), parse) + diags.Append(oneDiags...) + if one != nil { + out = append(out, *one) + } + } + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource.go index aba3428b..3b89658e 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource_acceptance_test.go index 8dd9d68e..86d6cd11 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/datasource_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag_test diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/model.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/model.go index 39bdff8f..3dbb5bef 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/model.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/get-tag/ package tag diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/read.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/read.go index f70219ed..1cb69dde 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/read.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/read.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/seed_helper_test.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/seed_helper_test.go index 8d7ea434..3282cda6 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/seed_helper_test.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/seed_helper_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/state.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/state.go index a709cb98..fab6f871 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/state.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/state.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/testdata/datasource.tf b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/testdata/datasource.tf index b768882c..d61f6134 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/testdata/datasource.tf +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tag/testdata/datasource.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The seed resource this data source reads back, then the data block itself. One file, # because the reference below only resolves against the resource above it. diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource.go index cfafe3ab..ece952ac 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tags diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource_acceptance_test.go index 978df4f8..6547d1db 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/datasource_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tags_test diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/model.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/model.go index 12959a39..5db7bb82 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/model.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/list-tags/ package tags diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/read.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/read.go index ac1ff099..389804bf 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/read.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/read.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tags diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/seed_helper_test.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/seed_helper_test.go index d9922ae7..b776e523 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/seed_helper_test.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/seed_helper_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tags diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/state.go b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/state.go index 90b98c1c..14d485a9 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/state.go +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/state.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tags diff --git a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/testdata/datasource.tf b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/testdata/datasource.tf index 0f1b81cf..7c2fd320 100644 --- a/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/testdata/datasource.tf +++ b/pilot/thousandeyes-kiota/internal/services/datasources/tags/v7/tags/testdata/datasource.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The seed resource this data source reads back, then the data block itself. One file, # because the reference below only resolves against the resource above it. diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral.go index 109a2a19..143d122b 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral_acceptance_test.go index b04e5539..62b37c01 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/ephemeral_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential_test diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/model.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/model.go index f33225dc..74c45479 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/model.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/get-credential/ package credential diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/open.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/open.go index c3127a78..7ab517ca 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/open.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/open.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/seed_helper_test.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/seed_helper_test.go index d996850f..30185449 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/seed_helper_test.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/seed_helper_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/state.go b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/state.go index bca4db07..7feda052 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/state.go +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/state.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/testdata/ephemeral.tf b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/testdata/ephemeral.tf index ad0742a7..30369de9 100644 --- a/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/testdata/ephemeral.tf +++ b/pilot/thousandeyes-kiota/internal/services/ephemerals/credentials/v7/credential/testdata/ephemeral.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The seed resource, the ephemeral that opens it, and the echo that makes the opened # value observable. One file, because each reference only resolves against the block diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/construct.go new file mode 100644 index 00000000..0a6c6859 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/construct.go @@ -0,0 +1,27 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// constructResource maps Terraform configuration onto the API's request body. +// +// Only writable attributes appear here. A computed attribute has no expand +// conversion, so it cannot be sent by construction rather than by discipline. +func constructResource(ctx context.Context, data *AccountGroupResourceModel) *models.Administrative_API_AccountGroupRequest { + tflog.Debug(ctx, "constructing request body", map[string]any{"resource": TypeName}) + + body := models.NewAdministrative_API_AccountGroupRequest() + + body.SetAccountGroupName(convert.FrameworkToPtrString(data.AccountGroupName)) + + return body +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/crud.go new file mode 100644 index 00000000..60eb81e1 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/crud.go @@ -0,0 +1,258 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/crud" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/errors" +) + +// Create provisions a new thousandeyes_account_group. +func (r *AccountGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan AccountGroupResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "creating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseCreate, CreateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body := constructResource(ctx, &plan) + + created, err := r.client.AccountGroups().Post(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, err) + return + } + + // An empty success body cannot be recorded: without a response there is no + // identifier, and inventing one would orphan whatever the API just created. + if created == nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, errors.ErrEmptyResponse) + return + } + + // The identifier is assigned first, so that a partially-successful create still + // leaves a resource Terraform can find and delete rather than an orphan it has no + // record of. + plan.ID = convert.PtrStringToFramework(created.GetAid()) + + // State comes from a read, not from the create response. The two are not + // interchangeable: a field the API only returns when asked to expand it is absent + // from a create response, so mapping that response would write it null and the next + // read would contradict it -- a diff no configuration change can resolve. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpCreate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Read refreshes thousandeyes_account_group state from the API. +func (r *AccountGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AccountGroupResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "reading resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseRead, ReadTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + found, diags := r.readState(ctx, &state, errors.OpRead) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Something deleted outside Terraform must produce a plan that recreates it, not an + // error the practitioner has no way to clear. + if !found { + tflog.Debug(ctx, "resource no longer exists, removing it from state", map[string]any{ + "resource": TypeName, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// readState fetches thousandeyes_account_group and maps it onto state. +// +// The single call site of mapRemoteStateToTerraform. Create, Update and Read all arrive +// here, so a change to state mapping is one edit rather than three that have to agree. +// +// The bool distinguishes "not there" from "the request failed", which need different +// handling and used to be conflated: Read turns absence into a recreate, while a read +// after a write treats it as not-yet-consistent and tries again. +func (r *AccountGroupResource) readState( + ctx context.Context, + state *AccountGroupResourceModel, + op errors.Operation, +) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + remote, err := r.client.AccountGroups().ById(state.ID.ValueString()).Get(ctx, nil) + if err != nil { + if errors.IsNotFound(err) { + return false, diags + } + + errors.Handle(&diags, TypeName, op, err) + + return false, diags + } + + // An empty success body means the object is gone as far as this provider can + // tell; treat it exactly like a not-found rather than mapping nothing. + if remote == nil { + return false, diags + } + diags.Append(mapRemoteStateToTerraform(ctx, state, remote)...) + + return true, diags +} + +// readAfterWrite refreshes state from the API after a create or an update, retrying while +// the object is not yet readable. +// +// Why a read at all, rather than mapping the write's own response: the two are not +// interchangeable. A field the API only returns when asked to expand it is absent from a +// write response, so mapping that response writes it null and the next read contradicts +// it. The pilot's own probe evidence says so. +// +// Why a retry: an API that is not read-your-writes consistent returns 404 for an object it +// has just created, and a create that reports success on state the next plan disagrees with +// is worse than one that waits. +// +// The budget is the built-in default: this API's consistency has not been measured, so +// there is no observation to size it from. An API that is consistent succeeds on the first +// attempt and pays nothing for the loop. +func (r *AccountGroupResource) readAfterWrite( + ctx context.Context, + state *AccountGroupResourceModel, + op errors.Operation, +) diag.Diagnostics { + var diags diag.Diagnostics + + opts := crud.ReadBackOptions{ + MaxRetries: 10, + Interval: 2000 * time.Millisecond, + ResourceType: TypeName, + Operation: string(op), + } + + err := crud.ReadBack(ctx, opts, func(ctx context.Context) (bool, error) { + found, d := r.readState(ctx, state, op) + diags.Append(d...) + + if d.HasError() { + return false, crud.ErrStopRetrying + } + + return found, nil + }) + + // A stop signal means readState has already produced the diagnostic, so reporting + // here as well would say the same thing twice. + if err != nil && !crud.IsStopRetrying(err) { + errors.Handle(&diags, TypeName, op, err) + } + + return diags +} + +// Update applies configuration changes to an existing thousandeyes_account_group. +func (r *AccountGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state AccountGroupResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "updating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseUpdate, UpdateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body := constructResource(ctx, &plan) + + _, err := r.client.AccountGroups().ById(state.ID.ValueString()).Put(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpUpdate, err) + return + } + + // The plan carries no identifier, since it is computed. Carrying it over from + // prior state keeps it in the model in case the response omits it. + plan.ID = state.ID + + // As with create: state comes from a read rather than from the update response. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpUpdate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete removes a thousandeyes_account_group. +func (r *AccountGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AccountGroupResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseDelete, DeleteTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.client.AccountGroups().ById(state.ID.ValueString()).Delete(ctx, nil) + if err != nil { + // Already gone is the outcome delete was asking for. + if errors.IsNotFound(err) { + return + } + errors.Handle(&resp.Diagnostics, TypeName, errors.OpDelete, err) + return + } + + // State is removed by the framework once Delete returns without error. +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/model.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/model.go new file mode 100644 index 00000000..ea0adba4 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/model.go @@ -0,0 +1,76 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// AccountGroupResourceModel is the Terraform state for a thousandeyes_account_group. +// +// Field order follows the schema, and the timeouts value is last. +type AccountGroupResourceModel struct { + AccountGroupName types.String `tfsdk:"account_group_name"` + ID types.String `tfsdk:"id"` + IsCurrentAccountGroup types.Bool `tfsdk:"is_current_account_group"` + IsDefaultAccountGroup types.Bool `tfsdk:"is_default_account_group"` + OrgID types.String `tfsdk:"org_id"` + OrganizationName types.String `tfsdk:"organization_name"` + Users types.Set `tfsdk:"users"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// AccountGroupUserAccountGroupModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type AccountGroupUserAccountGroupModel struct { + DateRegistered types.String `tfsdk:"date_registered"` + Email types.String `tfsdk:"email"` + LastLogin types.String `tfsdk:"last_login"` + Name types.String `tfsdk:"name"` + Roles types.Set `tfsdk:"roles"` + Uid types.String `tfsdk:"uid"` +} + +// accountGroupUserAccountGroupAttrTypes describes AccountGroupUserAccountGroupModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var accountGroupUserAccountGroupAttrTypes = map[string]attr.Type{ + "date_registered": types.StringType, + "email": types.StringType, + "last_login": types.StringType, + "name": types.StringType, + "roles": types.SetType{ElemType: accountGroupRoleObjectType}, + "uid": types.StringType, +} + +var accountGroupUserAccountGroupObjectType = types.ObjectType{AttrTypes: accountGroupUserAccountGroupAttrTypes} + +// AccountGroupRoleModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type AccountGroupRoleModel struct { + HasManagementPermissions types.Bool `tfsdk:"has_management_permissions"` + IsBuiltin types.Bool `tfsdk:"is_builtin"` + Name types.String `tfsdk:"name"` + RoleID types.String `tfsdk:"role_id"` +} + +// accountGroupRoleAttrTypes describes AccountGroupRoleModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var accountGroupRoleAttrTypes = map[string]attr.Type{ + "has_management_permissions": types.BoolType, + "is_builtin": types.BoolType, + "name": types.StringType, + "role_id": types.StringType, +} + +var accountGroupRoleObjectType = types.ObjectType{AttrTypes: accountGroupRoleAttrTypes} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource.go new file mode 100644 index 00000000..d29701cc --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource.go @@ -0,0 +1,155 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/client" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" + commonschema "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/schema" +) + +const ( + // TypeName is the Terraform type this resource registers as. + TypeName = "thousandeyes_account_group" + + // Default per-operation timeouts, in seconds. A practitioner overrides them + // with the resource's timeouts block. + CreateTimeout = 180 + ReadTimeout = 180 + UpdateTimeout = 180 + DeleteTimeout = 180 +) + +// Compile-time assertions that this resource implements what it claims to. They +// turn a missing method into a build failure rather than a runtime surprise. +var ( + _ resource.Resource = (*AccountGroupResource)(nil) + _ resource.ResourceWithConfigure = (*AccountGroupResource)(nil) + _ resource.ResourceWithImportState = (*AccountGroupResource)(nil) +) + +// NewAccountGroupResource returns the thousandeyes_account_group resource. +func NewAccountGroupResource() resource.Resource { + return &AccountGroupResource{} +} + +// AccountGroupResource implements the thousandeyes_account_group resource. +type AccountGroupResource struct { + client *sdk.ThousandEyesClient +} + +// Metadata returns the resource type name. +func (r *AccountGroupResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TypeName +} + +// Configure receives the configured SDK client from the provider. +func (r *AccountGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = client.ForResource(ctx, req, resp, TypeName) +} + +// ImportState maps a terraform import into state. +func (r *AccountGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Schema returns the resource schema. +func (r *AccountGroupResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve account group.", + Attributes: map[string]schema.Attribute{ + "account_group_name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Account group name Required-by-API by curation: the sweeper's field, unprobeable by " + + "omission.", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "A unique identifier associated with your account group. You can retrieve your " + + "`AccountGroupId` from the `/account-groups` endpoint. Named `id` per the import " + + "convention; the wire field stays aid.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "is_current_account_group": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Indicates whether the requested aid is the context of the current account.", + }, + "is_default_account_group": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Indicates whether the aid is the default one for the requesting user.", + }, + "org_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "(Optional) The ID for the organization associated with the account group.", + }, + "organization_name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "(Optional) The name of the organization associated with the account group.", + }, + "users": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "date_registered": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "User's UTC registration date (ISO date-time format).", + }, + "email": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "User's email address.", + }, + "last_login": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "User's UTC last login date (ISO date-time format).", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "User's display name.", + }, + "roles": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "has_management_permissions": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Flag indicating whether the user has management permissions.", + }, + "is_builtin": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User).", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Name of the role.", + }, + "role_id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique ID representing the role.", + }, + }, + }, + Computed: true, + }, + "uid": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique ID representing the user.", + }, + }, + }, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": commonschema.ResourceTimeouts(ctx), + }, + } +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource_acceptance_test.go new file mode 100644 index 00000000..81f63cf4 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/resource_acceptance_test.go @@ -0,0 +1,87 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group_test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/check" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/destroy" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group" +) + +// The resource under test, and the helper that asks the API whether it is really there. +const address = "thousandeyes_account_group.test" + +var testResource = account_group.AccountGroupTestResource{} + +// TestAccResourceAccountGroup_01_Lifecycle exercises the whole lifecycle against a live API. +// +// Guarded by TF_ACC, which terraform-plugin-testing requires, so `go test ./...` never reaches a +// live tenant by accident. The credential comes from the environment; see internal/acceptance. +// +// Every step ends with an implicit plan, and terraform-plugin-testing fails the step if that plan +// is not empty. That is the single strongest signal available that state mapping is right: an +// attribute the provider stores differently from how the API reports it shows up here as a diff on +// a resource nobody touched, and no amount of compiling or schema checking can see it. +func TestAccResourceAccountGroup_01_Lifecycle(t *testing.T) { + // This resource needs more privilege than the ordinary credential carries, so the + // gate is its own variable: a run against a lesser token skips it by name here, + // stated, instead of failing red for a reason that is about the token. + if os.Getenv("TFPFGEN_ACC_ADMIN") == "" { + t.Skipf("set TFPFGEN_ACC_ADMIN to run this test; it needs a more privileged credential") + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + // Destroy is verified against the API rather than against state, because Terraform + // considers a destroy done when the provider's Delete returns no error -- which is a + // statement about the request, not about the object. + // + // The wait is the built-in default; this API's consistency after delete was never measured, + // so this is a budget rather than an observation + CheckDestroy: destroy.CheckDestroyed(testResource, "thousandeyes_account_group", 20*time.Second), + Steps: []resource.TestStep{ + // Create, from the smallest configuration the schema permits. + { + Config: config(t, "minimal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("account_group_name").HasValue("tfacc-account-group-name"), + ), + }, + // Import, then compare every attribute against the state the apply produced. + // + // This is what catches a read that populates state differently from a create, which + // is the most common way a generated provider is subtly wrong. + { + ResourceName: address, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +// config reads one of the committed fixtures. +// +// From testdata rather than from a string constant in this file, so the configuration can be +// edited and re-run without recompiling, and so `terraform fmt` can police it. +func config(t *testing.T, name string) string { + t.Helper() + + body, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading fixture %s: %v", name, err) + } + + return string(body) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/state.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/state.go new file mode 100644 index 00000000..6222da10 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/state.go @@ -0,0 +1,105 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// mapRemoteStateToTerraform maps the API's response onto Terraform state. +// +// This function replaces what a reflection-based provider does at runtime. Being +// generated code, a field whose conversion is wrong is a compile error rather +// than a surprise during apply. +func mapRemoteStateToTerraform(ctx context.Context, data *AccountGroupResourceModel, remote models.Administrative_API_AccountGroupDetailable) diag.Diagnostics { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + + if remote == nil { + tflog.Debug(ctx, "remote resource is nil, leaving state untouched", map[string]any{ + "resource": TypeName, + }) + return diags + } + + data.AccountGroupName = convert.PtrStringToFramework(remote.GetAccountGroupName()) + data.ID = convert.PtrStringToFramework(remote.GetAid()) + data.IsCurrentAccountGroup = convert.PtrBoolToFramework(remote.GetIsCurrentAccountGroup()) + data.IsDefaultAccountGroup = convert.PtrBoolToFramework(remote.GetIsDefaultAccountGroup()) + data.OrgID = convert.PtrStringToFramework(remote.GetOrgId()) + data.OrganizationName = convert.PtrStringToFramework(remote.GetOrganizationName()) + data.Users, d = flattenAccountGroupUserAccountGroups(ctx, remote.GetUsers()) + diags.Append(d...) + + tflog.Debug(ctx, "mapped remote state", map[string]any{"resource": TypeName}) + + return diags +} + +// flattenAccountGroupUserAccountGroups converts SDK structs into the framework value state holds. +func flattenAccountGroupUserAccountGroups(ctx context.Context, in []models.Administrative_API_UserAccountGroupable) (types.Set, diag.Diagnostics) { + var diags diag.Diagnostics + var d diag.Diagnostics + + // A nil slice is null and an empty slice is empty. Conflating them produces a + // diff no configuration change can resolve. + if in == nil { + return types.SetNull(accountGroupUserAccountGroupObjectType), diags + } + + elements := make([]AccountGroupUserAccountGroupModel, 0, len(in)) + for _, item := range in { + var m AccountGroupUserAccountGroupModel + + m.DateRegistered = convert.PtrTimeToFramework(item.GetDateRegistered()) + m.Email = convert.PtrStringToFramework(item.GetEmail()) + m.LastLogin = convert.PtrTimeToFramework(item.GetLastLogin()) + m.Name = convert.PtrStringToFramework(item.GetName()) + m.Roles, d = flattenAccountGroupRoles(ctx, item.GetRoles()) + diags.Append(d...) + m.Uid = convert.PtrStringToFramework(item.GetUid()) + elements = append(elements, m) + } + + out, d2 := types.SetValueFrom(ctx, accountGroupUserAccountGroupObjectType, elements) + diags.Append(d2...) + + return out, diags +} + +// flattenAccountGroupRoles converts SDK structs into the framework value state holds. +func flattenAccountGroupRoles(ctx context.Context, in []models.Administrative_API_Roleable) (types.Set, diag.Diagnostics) { + var diags diag.Diagnostics + + // A nil slice is null and an empty slice is empty. Conflating them produces a + // diff no configuration change can resolve. + if in == nil { + return types.SetNull(accountGroupRoleObjectType), diags + } + + elements := make([]AccountGroupRoleModel, 0, len(in)) + for _, item := range in { + var m AccountGroupRoleModel + + m.HasManagementPermissions = convert.PtrBoolToFramework(item.GetHasManagementPermissions()) + m.IsBuiltin = convert.PtrBoolToFramework(item.GetIsBuiltin()) + m.Name = convert.PtrStringToFramework(item.GetName()) + m.RoleID = convert.PtrStringToFramework(item.GetRoleId()) + elements = append(elements, m) + } + + out, d2 := types.SetValueFrom(ctx, accountGroupRoleObjectType, elements) + diags.Append(d2...) + + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/test_helper_test.go new file mode 100644 index 00000000..5b807a99 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/test_helper_test.go @@ -0,0 +1,40 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package account_group + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/exists" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" +) + +// AccountGroupTestResource answers whether a thousandeyes_account_group is really in the API. +// +// In a _test.go file rather than an ordinary one, so no test scaffolding reaches a built +// provider. An external _test package still sees the exported identifiers of a package's own +// test files, which is what lets the generated acceptance test refer to this. +type AccountGroupTestResource struct{} + +// Exists reads the object back through the same operation the resource's Read uses. +// +// Deliberately the same call rather than an equivalent one. A check that asked a list endpoint +// instead could report an object present while the read the provider actually depends on was +// broken, which is the failure an acceptance test exists to catch. +func (AccountGroupTestResource) Exists( + ctx context.Context, + state *terraform.InstanceState, +) (*bool, error) { + return exists.Check(ctx, state, func( + ctx context.Context, + client *sdk.ThousandEyesClient, + state *terraform.InstanceState, + ) error { + _, err := client.AccountGroups().ById(state.ID).Get(ctx, nil) + + return err + }) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/maximal.tf new file mode 100644 index 00000000..4eb292ac --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/maximal.tf @@ -0,0 +1,15 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The widest configuration the evidence supports: every writable attribute with a derivable +# value, chosen by the same derivation the rehearsal probe sends to the live API. +# +# Regenerated and policed, deliberately. This file used to be a hand-owned scaffold, and +# hand edits are how its values drifted from the generated assertions -- the checks derive +# from the blueprint, so the blueprint is where a value is corrected: an accFixture hint for +# something only a human knows, recorded behaviour for everything the probe observed. What +# could not be derived is listed inside the block, each with the reason. + +resource "thousandeyes_account_group" "test" { + account_group_name = "tfacc-account-group-name" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/minimal.tf new file mode 100644 index 00000000..09d64644 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/account_groups/v7/account_group/testdata/minimal.tf @@ -0,0 +1,11 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The smallest configuration this resource accepts: its required attributes and nothing else. +# +# Regenerated, so it follows the schema. If the API starts requiring a field, this file gains it +# on the next emit rather than an acceptance run discovering it. + +resource "thousandeyes_account_group" "test" { + account_group_name = "tfacc-account-group-name" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/construct.go new file mode 100644 index 00000000..d4ecd124 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/construct.go @@ -0,0 +1,107 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// constructResource maps Terraform configuration onto the API's request body. +// +// Only writable attributes appear here. A computed attribute has no expand +// conversion, so it cannot be sent by construction rather than by discipline. +func constructResource(ctx context.Context, data *AlertSuppressionWindowResourceModel) (*models.Alerts_API_AlertSuppressionWindowRequest, diag.Diagnostics) { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + tflog.Debug(ctx, "constructing request body", map[string]any{"resource": TypeName}) + + body := models.NewAlerts_API_AlertSuppressionWindowRequest() + + body.SetDuration(convert.FrameworkInt64ToPtrInt32(data.Duration)) + endRepeatRaw, d := expandAlertSuppressionWindowEndRepeat(ctx, data.EndRepeat) + diags.Append(d...) + body.SetEndRepeat(endRepeatRaw) + body.SetIsEnabled(convert.FrameworkToPtrBool(data.IsEnabled)) + body.SetName(convert.FrameworkToPtrString(data.Name)) + repeatRaw, d := expandAlertSuppressionWindowRepeat(ctx, data.Repeat) + diags.Append(d...) + body.SetRepeat(repeatRaw) + startDateRaw, d := convert.FrameworkToPtrTime(data.StartDate) + diags.Append(d...) + body.SetStartDate(startDateRaw) + testIDsRaw, d := convert.FrameworkSetToStringSlice(ctx, data.TestIDs) + diags.Append(d...) + body.SetTestIds(testIDsRaw) + + return body, diags +} + +// expandAlertSuppressionWindowEndRepeat converts the configured AlertSuppressionWindowEndRepeatModel values into SDK structs. +func expandAlertSuppressionWindowEndRepeat(ctx context.Context, v types.Object) (models.Alerts_API_EndRepeatable, diag.Diagnostics) { + var diags diag.Diagnostics + var d diag.Diagnostics + + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + var m AlertSuppressionWindowEndRepeatModel + diags.Append(v.As(ctx, &m, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + + item := models.NewAlerts_API_EndRepeat() + + item.SetCount(convert.FrameworkInt64ToPtrInt32(m.Count)) + dateRaw, d := convert.FrameworkToPtrDateOnly(m.Date) + diags.Append(d...) + item.SetDate(dateRaw) + typeRaw, d := convert.FrameworkToKiotaEnum[models.Alerts_API_EndRepeatType](m.Type, models.ParseAlerts_API_EndRepeatType) + diags.Append(d...) + item.SetTypeEscaped(typeRaw) + + return item, diags +} + +// expandAlertSuppressionWindowRepeat converts the configured AlertSuppressionWindowRepeatModel values into SDK structs. +func expandAlertSuppressionWindowRepeat(ctx context.Context, v types.Object) (models.Alerts_API_Repeatable, diag.Diagnostics) { + var diags diag.Diagnostics + var d diag.Diagnostics + + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + var m AlertSuppressionWindowRepeatModel + diags.Append(v.As(ctx, &m, basetypes.ObjectAsOptions{})...) + if diags.HasError() { + return nil, diags + } + + item := models.NewAlerts_API_Repeat() + + daysOfWeekRaw, d := convert.FrameworkSetToKiotaEnumSlice[models.Alerts_API_DaysOfWeek](ctx, m.DaysOfWeek, models.ParseAlerts_API_DaysOfWeek) + diags.Append(d...) + item.SetDaysOfWeek(daysOfWeekRaw) + item.SetIntervalLength(convert.FrameworkInt64ToPtrInt32(m.IntervalLength)) + intervalTypeRaw, d := convert.FrameworkToKiotaEnum[models.Alerts_API_IntervalType](m.IntervalType, models.ParseAlerts_API_IntervalType) + diags.Append(d...) + item.SetIntervalType(intervalTypeRaw) + typeRaw, d := convert.FrameworkToKiotaEnum[models.Alerts_API_RepeatType](m.Type, models.ParseAlerts_API_RepeatType) + diags.Append(d...) + item.SetTypeEscaped(typeRaw) + + return item, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/crud.go new file mode 100644 index 00000000..ac7b485e --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/crud.go @@ -0,0 +1,266 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/crud" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/errors" +) + +// Create provisions a new thousandeyes_alert_suppression_window. +func (r *AlertSuppressionWindowResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan AlertSuppressionWindowResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "creating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseCreate, CreateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + created, err := r.client.AlertSuppressionWindows().Post(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, err) + return + } + + // An empty success body cannot be recorded: without a response there is no + // identifier, and inventing one would orphan whatever the API just created. + if created == nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, errors.ErrEmptyResponse) + return + } + + // The identifier is assigned first, so that a partially-successful create still + // leaves a resource Terraform can find and delete rather than an orphan it has no + // record of. + plan.ID = convert.PtrStringToFramework(created.GetAlertSuppressionWindowId()) + + // State comes from a read, not from the create response. The two are not + // interchangeable: a field the API only returns when asked to expand it is absent + // from a create response, so mapping that response would write it null and the next + // read would contradict it -- a diff no configuration change can resolve. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpCreate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Read refreshes thousandeyes_alert_suppression_window state from the API. +func (r *AlertSuppressionWindowResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AlertSuppressionWindowResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "reading resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseRead, ReadTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + found, diags := r.readState(ctx, &state, errors.OpRead) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Something deleted outside Terraform must produce a plan that recreates it, not an + // error the practitioner has no way to clear. + if !found { + tflog.Debug(ctx, "resource no longer exists, removing it from state", map[string]any{ + "resource": TypeName, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// readState fetches thousandeyes_alert_suppression_window and maps it onto state. +// +// The single call site of mapRemoteStateToTerraform. Create, Update and Read all arrive +// here, so a change to state mapping is one edit rather than three that have to agree. +// +// The bool distinguishes "not there" from "the request failed", which need different +// handling and used to be conflated: Read turns absence into a recreate, while a read +// after a write treats it as not-yet-consistent and tries again. +func (r *AlertSuppressionWindowResource) readState( + ctx context.Context, + state *AlertSuppressionWindowResourceModel, + op errors.Operation, +) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + remote, err := r.client.AlertSuppressionWindows().ByWindowId(state.ID.ValueString()).Get(ctx, nil) + if err != nil { + if errors.IsNotFound(err) { + return false, diags + } + + errors.Handle(&diags, TypeName, op, err) + + return false, diags + } + + // An empty success body means the object is gone as far as this provider can + // tell; treat it exactly like a not-found rather than mapping nothing. + if remote == nil { + return false, diags + } + diags.Append(mapRemoteStateToTerraform(ctx, state, remote)...) + + return true, diags +} + +// readAfterWrite refreshes state from the API after a create or an update, retrying while +// the object is not yet readable. +// +// Why a read at all, rather than mapping the write's own response: the two are not +// interchangeable. A field the API only returns when asked to expand it is absent from a +// write response, so mapping that response writes it null and the next read contradicts +// it. The pilot's own probe evidence says so. +// +// Why a retry: an API that is not read-your-writes consistent returns 404 for an object it +// has just created, and a create that reports success on state the next plan disagrees with +// is worse than one that waits. +// +// The budget is the built-in default: this API's consistency has not been measured, so +// there is no observation to size it from. An API that is consistent succeeds on the first +// attempt and pays nothing for the loop. +func (r *AlertSuppressionWindowResource) readAfterWrite( + ctx context.Context, + state *AlertSuppressionWindowResourceModel, + op errors.Operation, +) diag.Diagnostics { + var diags diag.Diagnostics + + opts := crud.ReadBackOptions{ + MaxRetries: 10, + Interval: 2000 * time.Millisecond, + ResourceType: TypeName, + Operation: string(op), + } + + err := crud.ReadBack(ctx, opts, func(ctx context.Context) (bool, error) { + found, d := r.readState(ctx, state, op) + diags.Append(d...) + + if d.HasError() { + return false, crud.ErrStopRetrying + } + + return found, nil + }) + + // A stop signal means readState has already produced the diagnostic, so reporting + // here as well would say the same thing twice. + if err != nil && !crud.IsStopRetrying(err) { + errors.Handle(&diags, TypeName, op, err) + } + + return diags +} + +// Update applies configuration changes to an existing thousandeyes_alert_suppression_window. +func (r *AlertSuppressionWindowResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state AlertSuppressionWindowResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "updating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseUpdate, UpdateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.AlertSuppressionWindows().ByWindowId(state.ID.ValueString()).Put(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpUpdate, err) + return + } + + // The plan carries no identifier, since it is computed. Carrying it over from + // prior state keeps it in the model in case the response omits it. + plan.ID = state.ID + + // As with create: state comes from a read rather than from the update response. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpUpdate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete removes a thousandeyes_alert_suppression_window. +func (r *AlertSuppressionWindowResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AlertSuppressionWindowResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseDelete, DeleteTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.client.AlertSuppressionWindows().ByWindowId(state.ID.ValueString()).Delete(ctx, nil) + if err != nil { + // Already gone is the outcome delete was asking for. + if errors.IsNotFound(err) { + return + } + errors.Handle(&resp.Diagnostics, TypeName, errors.OpDelete, err) + return + } + + // State is removed by the framework once Delete returns without error. +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/model.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/model.go new file mode 100644 index 00000000..729c5ec5 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/model.go @@ -0,0 +1,72 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// AlertSuppressionWindowResourceModel is the Terraform state for a thousandeyes_alert_suppression_window. +// +// Field order follows the schema, and the timeouts value is last. +type AlertSuppressionWindowResourceModel struct { + ID types.String `tfsdk:"id"` + Duration types.Int64 `tfsdk:"duration"` + EndRepeat types.Object `tfsdk:"end_repeat"` + IsEnabled types.Bool `tfsdk:"is_enabled"` + Name types.String `tfsdk:"name"` + Repeat types.Object `tfsdk:"repeat"` + StartDate types.String `tfsdk:"start_date"` + Status types.String `tfsdk:"status"` + TestIDs types.Set `tfsdk:"test_ids"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// AlertSuppressionWindowEndRepeatModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type AlertSuppressionWindowEndRepeatModel struct { + Count types.Int64 `tfsdk:"count"` + Date types.String `tfsdk:"date"` + Type types.String `tfsdk:"type"` +} + +// alertSuppressionWindowEndRepeatAttrTypes describes AlertSuppressionWindowEndRepeatModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var alertSuppressionWindowEndRepeatAttrTypes = map[string]attr.Type{ + "count": types.Int64Type, + "date": types.StringType, + "type": types.StringType, +} + +var alertSuppressionWindowEndRepeatObjectType = types.ObjectType{AttrTypes: alertSuppressionWindowEndRepeatAttrTypes} + +// AlertSuppressionWindowRepeatModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type AlertSuppressionWindowRepeatModel struct { + DaysOfWeek types.Set `tfsdk:"days_of_week"` + IntervalLength types.Int64 `tfsdk:"interval_length"` + IntervalType types.String `tfsdk:"interval_type"` + Type types.String `tfsdk:"type"` +} + +// alertSuppressionWindowRepeatAttrTypes describes AlertSuppressionWindowRepeatModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var alertSuppressionWindowRepeatAttrTypes = map[string]attr.Type{ + "days_of_week": types.SetType{ElemType: types.StringType}, + "interval_length": types.Int64Type, + "interval_type": types.StringType, + "type": types.StringType, +} + +var alertSuppressionWindowRepeatObjectType = types.ObjectType{AttrTypes: alertSuppressionWindowRepeatAttrTypes} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource.go new file mode 100644 index 00000000..5f23a1a8 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource.go @@ -0,0 +1,176 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/client" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" + commonschema "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/schema" +) + +const ( + // TypeName is the Terraform type this resource registers as. + TypeName = "thousandeyes_alert_suppression_window" + + // Default per-operation timeouts, in seconds. A practitioner overrides them + // with the resource's timeouts block. + CreateTimeout = 180 + ReadTimeout = 180 + UpdateTimeout = 180 + DeleteTimeout = 180 +) + +// Compile-time assertions that this resource implements what it claims to. They +// turn a missing method into a build failure rather than a runtime surprise. +var ( + _ resource.Resource = (*AlertSuppressionWindowResource)(nil) + _ resource.ResourceWithConfigure = (*AlertSuppressionWindowResource)(nil) + _ resource.ResourceWithImportState = (*AlertSuppressionWindowResource)(nil) +) + +// NewAlertSuppressionWindowResource returns the thousandeyes_alert_suppression_window resource. +func NewAlertSuppressionWindowResource() resource.Resource { + return &AlertSuppressionWindowResource{} +} + +// AlertSuppressionWindowResource implements the thousandeyes_alert_suppression_window resource. +type AlertSuppressionWindowResource struct { + client *sdk.ThousandEyesClient +} + +// Metadata returns the resource type name. +func (r *AlertSuppressionWindowResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TypeName +} + +// Configure receives the configured SDK client from the provider. +func (r *AlertSuppressionWindowResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = client.ForResource(ctx, req, resp, TypeName) +} + +// ImportState maps a terraform import into state. +func (r *AlertSuppressionWindowResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Schema returns the resource schema. +func (r *AlertSuppressionWindowResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve alert suppression window.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique ID of the alert suppression window. Named `id` because terraform-plugin-testing " + + "populates import state only from an attribute literally called id; the wire field stays " + + "alertSuppressionWindowId.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "duration": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Duration in seconds the suppression window is active. The API refuses a window without a " + + "duration (live probe, 2026-08-03). The API enforces " + + "this field's presence, which the specification does not declare. ", + }, + "end_repeat": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "count": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "End repeat after number of occurrences, only valid with count type option.", + }, + "date": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "End repeat after specific date, only valid with date type option (ISO date format).", + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "End repeat options type. Values accepted here: " + + "`count`, `never`, `date`. Observed: the API assigns \"never\" when this is omitted. The API " + + "accepted a value from outside the documented set, so no OneOf validator is generated for " + + "this attribute. ", + }, + }, + Optional: true, + Computed: true, + MarkdownDescription: "End repeat options. The API assigns { type = \"never\" } when omitted (live acceptance, " + + "2026-08-03).", + }, + "is_enabled": schema.BoolAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Set to `false` for `disabled`, `true` for `enabled`.", + }, + "name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Name of the alert suppression window. The API refuses a window without a name (live " + + "acceptance, 2026-08-03); the probe cannot test this itself because name carries the " + + "sweeper's prefix.", + }, + "repeat": schema.SingleNestedAttribute{ + Attributes: map[string]schema.Attribute{ + "days_of_week": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + }, + "interval_length": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: "Number of `intervalTypes` to wait before reactivating the alert suppression window.", + }, + "interval_type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Repeat options interval type Values accepted here: " + + "`day`, `week`, `month`. The API accepted a value from outside the documented set, so no " + + "OneOf validator is generated for this attribute. ", + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Repeat options type. Values accepted here: `day`, " + + "`week`, `month`, `custom`. Observed: the API assigns \"none\" when this is omitted. The API " + + "accepted a value from outside the documented set, so no OneOf validator is generated for " + + "this attribute. ", + }, + }, + Optional: true, + MarkdownDescription: "Repeat options. The API requires repeat conditions on every window; a one-off window " + + "carries `type = \"none\"` (live probe, 2026-08-03). " + + "The API enforces this field's presence, which the specification does not declare. ", + }, + "start_date": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The date/time when the alert suppression window starts (ISO date-time format). The API enforces this field's presence, which the " + + "specification does not declare. ", + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Indicates the current status of the suppression window.", + }, + "test_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "List of test IDs to assign to the alert suppression window. The response never carries " + + "testIds -- it echoes the linked tests as a nested collection instead -- so state carries " + + "the configured value. Neither the update response " + + "nor a read returns this field: it is write-only in practice, and state carries the " + + "configured value. ", + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": commonschema.ResourceTimeouts(ctx), + }, + } +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource_acceptance_test.go new file mode 100644 index 00000000..8a169fe8 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/resource_acceptance_test.go @@ -0,0 +1,101 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window_test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/check" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/destroy" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window" +) + +// The resource under test, and the helper that asks the API whether it is really there. +const address = "thousandeyes_alert_suppression_window.test" + +var testResource = alert_suppression_window.AlertSuppressionWindowTestResource{} + +// TestAccResourceAlertSuppressionWindow_01_Lifecycle exercises the whole lifecycle against a live API. +// +// Guarded by TF_ACC, which terraform-plugin-testing requires, so `go test ./...` never reaches a +// live tenant by accident. The credential comes from the environment; see internal/acceptance. +// +// Every step ends with an implicit plan, and terraform-plugin-testing fails the step if that plan +// is not empty. That is the single strongest signal available that state mapping is right: an +// attribute the provider stores differently from how the API reports it shows up here as a diff on +// a resource nobody touched, and no amount of compiling or schema checking can see it. +func TestAccResourceAlertSuppressionWindow_01_Lifecycle(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + // Destroy is verified against the API rather than against state, because Terraform + // considers a destroy done when the provider's Delete returns no error -- which is a + // statement about the request, not about the object. + // + // The wait is the built-in default; this API's consistency after delete was never measured, + // so this is a budget rather than an observation + CheckDestroy: destroy.CheckDestroyed(testResource, "thousandeyes_alert_suppression_window", 20*time.Second), + Steps: []resource.TestStep{ + // Create, from the smallest configuration the schema permits. + { + Config: config(t, "minimal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("duration").Exists(), + check.That(address).Key("is_enabled").HasValue("true"), + check.That(address).Key("name").HasValue("tfacc-name"), + check.That(address).Key("start_date").HasValue("2027-06-01T00:00:00Z"), + ), + }, + // Import, then compare every attribute against the state the apply produced. + // + // This is what catches a read that populates state differently from a create, which + // is the most common way a generated provider is subtly wrong. + { + ResourceName: address, + ImportState: true, + ImportStateVerify: true, + // Skipped, with the reason for each. An attribute the API never returns is + // absent after an import and present before it, so comparing it fails for a + // reason that has nothing to do with import being broken. + // test_ids: it is write-only, carried from configuration, which an import does not have + ImportStateVerifyIgnore: []string{"test_ids"}, + }, + // Update in place, to a configuration that sets the optional attributes too. + { + Config: config(t, "maximal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("duration").Exists(), + check.That(address).Key("is_enabled").HasValue("true"), + check.That(address).Key("name").HasValue("tfacc-name"), + check.That(address).Key("start_date").HasValue("2027-06-01T00:00:00Z"), + ), + }, + }, + }) +} + +// config reads one of the committed fixtures. +// +// From testdata rather than from a string constant in this file, so the configuration can be +// edited and re-run without recompiling, and so `terraform fmt` can police it. +func config(t *testing.T, name string) string { + t.Helper() + + body, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading fixture %s: %v", name, err) + } + + return string(body) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/state.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/state.go new file mode 100644 index 00000000..15e0b300 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/state.go @@ -0,0 +1,96 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// mapRemoteStateToTerraform maps the API's response onto Terraform state. +// +// This function replaces what a reflection-based provider does at runtime. Being +// generated code, a field whose conversion is wrong is a compile error rather +// than a surprise during apply. +func mapRemoteStateToTerraform(ctx context.Context, data *AlertSuppressionWindowResourceModel, remote models.Alerts_API_AlertSuppressionWindowDetailable) diag.Diagnostics { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + + if remote == nil { + tflog.Debug(ctx, "remote resource is nil, leaving state untouched", map[string]any{ + "resource": TypeName, + }) + return diags + } + + data.ID = convert.PtrStringToFramework(remote.GetAlertSuppressionWindowId()) + data.Duration = convert.PtrInt32ToFrameworkInt64(remote.GetDuration()) + data.EndRepeat, d = flattenAlertSuppressionWindowEndRepeat(ctx, remote.GetEndRepeat()) + diags.Append(d...) + data.IsEnabled = convert.PtrBoolToFramework(remote.GetIsEnabled()) + data.Name = convert.PtrStringToFramework(remote.GetName()) + data.Repeat, d = flattenAlertSuppressionWindowRepeat(ctx, remote.GetRepeat()) + diags.Append(d...) + data.StartDate = convert.PtrTimeToFramework(remote.GetStartDate()) + data.Status = convert.PtrStringerToFramework(remote.GetStatus()) + + tflog.Debug(ctx, "mapped remote state", map[string]any{"resource": TypeName}) + + return diags +} + +// flattenAlertSuppressionWindowEndRepeat converts SDK structs into the framework value state holds. +func flattenAlertSuppressionWindowEndRepeat(ctx context.Context, in models.Alerts_API_EndRepeatable) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + + if in == nil { + return types.ObjectNull(alertSuppressionWindowEndRepeatObjectType.AttrTypes), diags + } + + item := in + + var m AlertSuppressionWindowEndRepeatModel + + m.Count = convert.PtrInt32ToFrameworkInt64(item.GetCount()) + m.Date = convert.PtrDateOnlyToFramework(item.GetDate()) + m.Type = convert.PtrStringerToFramework(item.GetTypeEscaped()) + + out, d2 := types.ObjectValueFrom(ctx, alertSuppressionWindowEndRepeatObjectType.AttrTypes, m) + diags.Append(d2...) + + return out, diags +} + +// flattenAlertSuppressionWindowRepeat converts SDK structs into the framework value state holds. +func flattenAlertSuppressionWindowRepeat(ctx context.Context, in models.Alerts_API_Repeatable) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + var d diag.Diagnostics + + if in == nil { + return types.ObjectNull(alertSuppressionWindowRepeatObjectType.AttrTypes), diags + } + + item := in + + var m AlertSuppressionWindowRepeatModel + + m.DaysOfWeek, d = convert.KiotaEnumSliceToFrameworkSet(ctx, item.GetDaysOfWeek()) + diags.Append(d...) + m.IntervalLength = convert.PtrInt32ToFrameworkInt64(item.GetIntervalLength()) + m.IntervalType = convert.PtrStringerToFramework(item.GetIntervalType()) + m.Type = convert.PtrStringerToFramework(item.GetTypeEscaped()) + + out, d2 := types.ObjectValueFrom(ctx, alertSuppressionWindowRepeatObjectType.AttrTypes, m) + diags.Append(d2...) + + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/test_helper_test.go new file mode 100644 index 00000000..87919e93 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/test_helper_test.go @@ -0,0 +1,40 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package alert_suppression_window + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/exists" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" +) + +// AlertSuppressionWindowTestResource answers whether a thousandeyes_alert_suppression_window is really in the API. +// +// In a _test.go file rather than an ordinary one, so no test scaffolding reaches a built +// provider. An external _test package still sees the exported identifiers of a package's own +// test files, which is what lets the generated acceptance test refer to this. +type AlertSuppressionWindowTestResource struct{} + +// Exists reads the object back through the same operation the resource's Read uses. +// +// Deliberately the same call rather than an equivalent one. A check that asked a list endpoint +// instead could report an object present while the read the provider actually depends on was +// broken, which is the failure an acceptance test exists to catch. +func (AlertSuppressionWindowTestResource) Exists( + ctx context.Context, + state *terraform.InstanceState, +) (*bool, error) { + return exists.Check(ctx, state, func( + ctx context.Context, + client *sdk.ThousandEyesClient, + state *terraform.InstanceState, + ) error { + _, err := client.AlertSuppressionWindows().ByWindowId(state.ID).Get(ctx, nil) + + return err + }) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/maximal.tf new file mode 100644 index 00000000..e2ac5162 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/maximal.tf @@ -0,0 +1,37 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The widest configuration the evidence supports: every writable attribute with a derivable +# value, chosen by the same derivation the rehearsal probe sends to the live API. +# +# Regenerated and policed, deliberately. This file used to be a hand-owned scaffold, and +# hand edits are how its values drifted from the generated assertions -- the checks derive +# from the blueprint, so the blueprint is where a value is corrected: an accFixture hint for +# something only a human knows, recorded behaviour for everything the probe observed. What +# could not be derived is listed inside the block, each with the reason. + +resource "thousandeyes_alert_suppression_window" "test" { + # Where a value came from. + # + # duration: curated in the blueprint's accFixture; the generator cannot derive it + # + # is_enabled: the API's own default, so a value it is known to accept + # + # repeat: curated in the blueprint's accFixture; the generator cannot derive it + # + # start_date: synthesised for the declared date-time format + # + # test_ids: curated in the blueprint's accFixture; the generator cannot derive it + # + # Not filled in, because no correct value could be derived. Each of these is optional, so this + # configuration is valid as it stands -- but it is testing less than it could. + # + # end_repeat: a nested object's members may be identifiers of objects that must already exist, + # and nothing in the specification says whether they are + duration = 3600 + is_enabled = true + name = "tfacc-name" + repeat = { type = "none" } + start_date = "2027-06-01T00:00:00Z" + test_ids = [] +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/minimal.tf new file mode 100644 index 00000000..13f4a17c --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/alert_suppression_windows/v7/alert_suppression_window/testdata/minimal.tf @@ -0,0 +1,25 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The smallest configuration this resource accepts: its required attributes and nothing else. +# +# Regenerated, so it follows the schema. If the API starts requiring a field, this file gains it +# on the next emit rather than an acceptance run discovering it. + +resource "thousandeyes_alert_suppression_window" "test" { + # Where a value came from. A fixture takes what the API was observed to accept, not the first + # value the specification documents -- which is the opposite of how a generated validator is + # built. A validator errs toward permitting, because blocking a configuration the API would + # have taken is a harm nobody can work around; a fixture errs toward working, because it is an + # input rather than a contract. + # + # duration: curated in the blueprint's accFixture; the generator cannot derive it + # + # repeat: curated in the blueprint's accFixture; the generator cannot derive it + # + # start_date: synthesised for the declared date-time format + duration = 3600 + name = "tfacc-name" + repeat = { type = "none" } + start_date = "2027-06-01T00:00:00Z" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/construct.go index dcb2afa3..018a31ea 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/construct.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/construct.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/crud.go index b594a271..cb785ccd 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/crud.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/crud.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/model.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/model.go index c289483a..223461b8 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/model.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/create-credential/ package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource.go index 07475b6b..2066e0ef 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource_acceptance_test.go index 9ea86935..9a5da506 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/resource_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential_test diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/state.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/state.go index ae1169f0..73609cbd 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/state.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/state.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/test_helper_test.go index d996850f..30185449 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/test_helper_test.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/test_helper_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package credential diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/maximal.tf index f9c89c6e..9213a8e5 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/maximal.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/maximal.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The widest configuration the evidence supports: every writable attribute with a derivable # value, chosen by the same derivation the rehearsal probe sends to the live API. diff --git a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/minimal.tf index 51fee1a3..0e09c13b 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/minimal.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/credentials/v7/credential/testdata/minimal.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The smallest configuration this resource accepts: its required attributes and nothing else. # diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/construct.go new file mode 100644 index 00000000..811f0ab7 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/construct.go @@ -0,0 +1,102 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// constructResource maps Terraform configuration onto the API's request body. +// +// Only writable attributes appear here. A computed attribute has no expand +// conversion, so it cannot be sent by construction rather than by discipline. +func constructResource(ctx context.Context, data *EndpointLabelResourceModel) (*models.Endpoint_Agent_Labels_API_LabelRequest, diag.Diagnostics) { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + tflog.Debug(ctx, "constructing request body", map[string]any{"resource": TypeName}) + + body := models.NewEndpoint_Agent_Labels_API_LabelRequest() + + body.SetColor(convert.FrameworkToPtrString(data.Color)) + filtersRaw, d := expandEndpointLabelFilters(ctx, data.Filters) + diags.Append(d...) + body.SetFilters(filtersRaw) + matchTypeRaw, d := convert.FrameworkToKiotaEnum[models.Endpoint_Agent_Labels_API_MatchType](data.MatchType, models.ParseEndpoint_Agent_Labels_API_MatchType) + diags.Append(d...) + body.SetMatchType(matchTypeRaw) + body.SetName(convert.FrameworkToPtrString(data.Name)) + + return body, diags +} + +// constructResourceUpdate builds the update request body. +// +// A separate function because this SDK gives update its own request type. The +// assignment list is create's except where an attribute declares updateExpand -- +// an SDK whose update type spells a field differently from create's -- and the +// bindings check has proven every assigned field exists on its own type. +func constructResourceUpdate(ctx context.Context, data *EndpointLabelResourceModel) (*models.Endpoint_Agent_Labels_API_Label, diag.Diagnostics) { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + tflog.Debug(ctx, "constructing update request body", map[string]any{"resource": TypeName}) + + body := models.NewEndpoint_Agent_Labels_API_Label() + + body.SetColor(convert.FrameworkToPtrString(data.Color)) + filtersRaw, d := expandEndpointLabelFilters(ctx, data.Filters) + diags.Append(d...) + body.SetFilters(filtersRaw) + matchTypeRaw, d := convert.FrameworkToKiotaEnum[models.Endpoint_Agent_Labels_API_MatchType](data.MatchType, models.ParseEndpoint_Agent_Labels_API_MatchType) + diags.Append(d...) + body.SetMatchType(matchTypeRaw) + body.SetName(convert.FrameworkToPtrString(data.Name)) + + return body, diags +} + +// expandEndpointLabelFilters converts the configured EndpointLabelFilterModel values into SDK structs. +func expandEndpointLabelFilters(ctx context.Context, v types.Set) ([]models.Endpoint_Agent_Labels_API_Filterable, diag.Diagnostics) { + var diags diag.Diagnostics + + // Null and unknown both yield nil so the field is omitted from the request, + // which is distinct from an empty collection asking the API to clear it. + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + var elements []EndpointLabelFilterModel + diags.Append(v.ElementsAs(ctx, &elements, false)...) + if diags.HasError() { + return nil, diags + } + + out := make([]models.Endpoint_Agent_Labels_API_Filterable, 0, len(elements)) + for _, m := range elements { + item := models.NewEndpoint_Agent_Labels_API_Filter() + + keyRaw, d := convert.FrameworkToKiotaEnum[models.Endpoint_Agent_Labels_API_FilterType](m.Key, models.ParseEndpoint_Agent_Labels_API_FilterType) + diags.Append(d...) + item.SetKey(keyRaw) + modeRaw, d := convert.FrameworkToKiotaEnum[models.Endpoint_Agent_Labels_API_LabelFilterMode](m.Mode, models.ParseEndpoint_Agent_Labels_API_LabelFilterMode) + diags.Append(d...) + item.SetMode(modeRaw) + valuesRaw, d := convert.FrameworkSetToStringSlice(ctx, m.Values) + diags.Append(d...) + item.SetValues(valuesRaw) + out = append(out, item) + } + + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/crud.go new file mode 100644 index 00000000..2c9a2b38 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/crud.go @@ -0,0 +1,266 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/crud" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/errors" +) + +// Create provisions a new thousandeyes_endpoint_label. +func (r *EndpointLabelResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan EndpointLabelResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "creating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseCreate, CreateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + created, err := r.client.Endpoint().Labels().Post(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, err) + return + } + + // An empty success body cannot be recorded: without a response there is no + // identifier, and inventing one would orphan whatever the API just created. + if created == nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, errors.ErrEmptyResponse) + return + } + + // The identifier is assigned first, so that a partially-successful create still + // leaves a resource Terraform can find and delete rather than an orphan it has no + // record of. + plan.ID = convert.PtrStringToFramework(created.GetId()) + + // State comes from a read, not from the create response. The two are not + // interchangeable: a field the API only returns when asked to expand it is absent + // from a create response, so mapping that response would write it null and the next + // read would contradict it -- a diff no configuration change can resolve. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpCreate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Read refreshes thousandeyes_endpoint_label state from the API. +func (r *EndpointLabelResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state EndpointLabelResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "reading resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseRead, ReadTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + found, diags := r.readState(ctx, &state, errors.OpRead) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Something deleted outside Terraform must produce a plan that recreates it, not an + // error the practitioner has no way to clear. + if !found { + tflog.Debug(ctx, "resource no longer exists, removing it from state", map[string]any{ + "resource": TypeName, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// readState fetches thousandeyes_endpoint_label and maps it onto state. +// +// The single call site of mapRemoteStateToTerraform. Create, Update and Read all arrive +// here, so a change to state mapping is one edit rather than three that have to agree. +// +// The bool distinguishes "not there" from "the request failed", which need different +// handling and used to be conflated: Read turns absence into a recreate, while a read +// after a write treats it as not-yet-consistent and tries again. +func (r *EndpointLabelResource) readState( + ctx context.Context, + state *EndpointLabelResourceModel, + op errors.Operation, +) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + remote, err := r.client.Endpoint().Labels().ById(state.ID.ValueString()).Get(ctx, nil) + if err != nil { + if errors.IsNotFound(err) { + return false, diags + } + + errors.Handle(&diags, TypeName, op, err) + + return false, diags + } + + // An empty success body means the object is gone as far as this provider can + // tell; treat it exactly like a not-found rather than mapping nothing. + if remote == nil { + return false, diags + } + diags.Append(mapRemoteStateToTerraform(ctx, state, remote)...) + + return true, diags +} + +// readAfterWrite refreshes state from the API after a create or an update, retrying while +// the object is not yet readable. +// +// Why a read at all, rather than mapping the write's own response: the two are not +// interchangeable. A field the API only returns when asked to expand it is absent from a +// write response, so mapping that response writes it null and the next read contradicts +// it. The pilot's own probe evidence says so. +// +// Why a retry: an API that is not read-your-writes consistent returns 404 for an object it +// has just created, and a create that reports success on state the next plan disagrees with +// is worse than one that waits. +// +// The budget is the built-in default: this API's consistency has not been measured, so +// there is no observation to size it from. An API that is consistent succeeds on the first +// attempt and pays nothing for the loop. +func (r *EndpointLabelResource) readAfterWrite( + ctx context.Context, + state *EndpointLabelResourceModel, + op errors.Operation, +) diag.Diagnostics { + var diags diag.Diagnostics + + opts := crud.ReadBackOptions{ + MaxRetries: 10, + Interval: 2000 * time.Millisecond, + ResourceType: TypeName, + Operation: string(op), + } + + err := crud.ReadBack(ctx, opts, func(ctx context.Context) (bool, error) { + found, d := r.readState(ctx, state, op) + diags.Append(d...) + + if d.HasError() { + return false, crud.ErrStopRetrying + } + + return found, nil + }) + + // A stop signal means readState has already produced the diagnostic, so reporting + // here as well would say the same thing twice. + if err != nil && !crud.IsStopRetrying(err) { + errors.Handle(&diags, TypeName, op, err) + } + + return diags +} + +// Update applies configuration changes to an existing thousandeyes_endpoint_label. +func (r *EndpointLabelResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state EndpointLabelResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "updating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseUpdate, UpdateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResourceUpdate(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.Endpoint().Labels().ById(state.ID.ValueString()).Patch(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpUpdate, err) + return + } + + // The plan carries no identifier, since it is computed. Carrying it over from + // prior state keeps it in the model in case the response omits it. + plan.ID = state.ID + + // As with create: state comes from a read rather than from the update response. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpUpdate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete removes a thousandeyes_endpoint_label. +func (r *EndpointLabelResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state EndpointLabelResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseDelete, DeleteTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.client.Endpoint().Labels().ById(state.ID.ValueString()).Delete(ctx, nil) + if err != nil { + // Already gone is the outcome delete was asking for. + if errors.IsNotFound(err) { + return + } + errors.Handle(&resp.Diagnostics, TypeName, errors.OpDelete, err) + return + } + + // State is removed by the framework once Delete returns without error. +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/model.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/model.go new file mode 100644 index 00000000..229fd002 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/model.go @@ -0,0 +1,44 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// EndpointLabelResourceModel is the Terraform state for a thousandeyes_endpoint_label. +// +// Field order follows the schema, and the timeouts value is last. +type EndpointLabelResourceModel struct { + Color types.String `tfsdk:"color"` + Filters types.Set `tfsdk:"filters"` + ID types.String `tfsdk:"id"` + MatchType types.String `tfsdk:"match_type"` + Name types.String `tfsdk:"name"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// EndpointLabelFilterModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type EndpointLabelFilterModel struct { + Key types.String `tfsdk:"key"` + Mode types.String `tfsdk:"mode"` + Values types.Set `tfsdk:"values"` +} + +// endpointLabelFilterAttrTypes describes EndpointLabelFilterModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var endpointLabelFilterAttrTypes = map[string]attr.Type{ + "key": types.StringType, + "mode": types.StringType, + "values": types.SetType{ElemType: types.StringType}, +} + +var endpointLabelFilterObjectType = types.ObjectType{AttrTypes: endpointLabelFilterAttrTypes} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource.go new file mode 100644 index 00000000..5267212d --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource.go @@ -0,0 +1,142 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/client" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" + commonschema "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/schema" +) + +const ( + // TypeName is the Terraform type this resource registers as. + TypeName = "thousandeyes_endpoint_label" + + // Default per-operation timeouts, in seconds. A practitioner overrides them + // with the resource's timeouts block. + CreateTimeout = 180 + ReadTimeout = 180 + UpdateTimeout = 180 + DeleteTimeout = 180 +) + +// Compile-time assertions that this resource implements what it claims to. They +// turn a missing method into a build failure rather than a runtime surprise. +var ( + _ resource.Resource = (*EndpointLabelResource)(nil) + _ resource.ResourceWithConfigure = (*EndpointLabelResource)(nil) + _ resource.ResourceWithImportState = (*EndpointLabelResource)(nil) +) + +// NewEndpointLabelResource returns the thousandeyes_endpoint_label resource. +func NewEndpointLabelResource() resource.Resource { + return &EndpointLabelResource{} +} + +// EndpointLabelResource implements the thousandeyes_endpoint_label resource. +type EndpointLabelResource struct { + client *sdk.ThousandEyesClient +} + +// Metadata returns the resource type name. +func (r *EndpointLabelResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TypeName +} + +// Configure receives the configured SDK client from the provider. +func (r *EndpointLabelResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = client.ForResource(ctx, req, resp, TypeName) +} + +// ImportState maps a terraform import into state. +func (r *EndpointLabelResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Schema returns the resource schema. +func (r *EndpointLabelResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve label.", + Attributes: map[string]schema.Attribute{ + "color": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "UI color Observed: the API assigns \"#d9d9d9\" when " + + "this is omitted. ", + }, + "filters": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "key": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Type of filter - the data that will be used to filter. Values accepted here: `agent-id`, `public-network`, `local-network`, `connection`, " + + "`gateway`, `platform`, `agent-type`, `vpn-vendor`, `vpn-gateway-address`, " + + "`vpn-client-network`, `vpn-client-address`, `ip-address-family`, `ssid`, `bssid`, " + + "`hostname`, `username`, `asn`. Observed: the API assigns \"platform\" when this is omitted. " + + "The API accepted a value from outside the documented set, so no OneOf validator is " + + "generated for this attribute. ", + }, + "mode": schema.StringAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: "Type of matching to be applied for the values: * `in`: The value on the agent must match " + + "one of the list of values provided. * `not-in`: The value on the agent must not match any " + + "of the list of values provided. Values accepted " + + "here: `in`, `not-in`. Observed: the API assigns \"in\" when this is omitted. The API " + + "accepted a value from outside the documented set, so no OneOf validator is generated for " + + "this attribute. ", + }, + "values": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + }, + }, + }, + Optional: true, + MarkdownDescription: "The filters combined using the matchType to determine the label's match. The API refuses a " + + "label without at least one filter (live probe, 2026-08-04). Only returned when the read " + + "asks for expand=filters; the generated read always does (live probe, 2026-08-04). The API enforces this field's presence, which the " + + "specification does not declare. ", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Label identifier.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "match_type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Specifies how the filters are combined for matching this label. * `and`: All filters must " + + "be matched. * `or`: Any of the filters should match. " + + "Values accepted here: `and`, `or`. The API enforces this field's presence, which the " + + "specification does not declare. ", + Validators: []validator.String{ + stringvalidator.OneOf("and", "or"), + }, + }, + "name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The label name. Required-by-API by curation: the sweeper's field, unprobeable by omission.", + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": commonschema.ResourceTimeouts(ctx), + }, + } +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource_acceptance_test.go new file mode 100644 index 00000000..631067d3 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/resource_acceptance_test.go @@ -0,0 +1,94 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label_test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/check" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/destroy" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label" +) + +// The resource under test, and the helper that asks the API whether it is really there. +const address = "thousandeyes_endpoint_label.test" + +var testResource = endpoint_label.EndpointLabelTestResource{} + +// TestAccResourceEndpointLabel_01_Lifecycle exercises the whole lifecycle against a live API. +// +// Guarded by TF_ACC, which terraform-plugin-testing requires, so `go test ./...` never reaches a +// live tenant by accident. The credential comes from the environment; see internal/acceptance. +// +// Every step ends with an implicit plan, and terraform-plugin-testing fails the step if that plan +// is not empty. That is the single strongest signal available that state mapping is right: an +// attribute the provider stores differently from how the API reports it shows up here as a diff on +// a resource nobody touched, and no amount of compiling or schema checking can see it. +func TestAccResourceEndpointLabel_01_Lifecycle(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + // Destroy is verified against the API rather than against state, because Terraform + // considers a destroy done when the provider's Delete returns no error -- which is a + // statement about the request, not about the object. + // + // The wait is the built-in default; this API's consistency after delete was never measured, + // so this is a budget rather than an observation + CheckDestroy: destroy.CheckDestroyed(testResource, "thousandeyes_endpoint_label", 20*time.Second), + Steps: []resource.TestStep{ + // Create, from the smallest configuration the schema permits. + { + Config: config(t, "minimal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("color").HasValue("#d9d9d9"), + check.That(address).Key("match_type").HasValue("and"), + check.That(address).Key("name").HasValue("tfacc-name"), + ), + }, + // Import, then compare every attribute against the state the apply produced. + // + // This is what catches a read that populates state differently from a create, which + // is the most common way a generated provider is subtly wrong. + { + ResourceName: address, + ImportState: true, + ImportStateVerify: true, + }, + // Update in place, to a configuration that sets the optional attributes too. + { + Config: config(t, "maximal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("color").HasValue("#d9d9d9"), + check.That(address).Key("match_type").HasValue("and"), + check.That(address).Key("name").HasValue("tfacc-name"), + ), + }, + }, + }) +} + +// config reads one of the committed fixtures. +// +// From testdata rather than from a string constant in this file, so the configuration can be +// edited and re-run without recompiling, and so `terraform fmt` can police it. +func config(t *testing.T, name string) string { + t.Helper() + + body, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading fixture %s: %v", name, err) + } + + return string(body) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/state.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/state.go new file mode 100644 index 00000000..385e98f6 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/state.go @@ -0,0 +1,73 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// mapRemoteStateToTerraform maps the API's response onto Terraform state. +// +// This function replaces what a reflection-based provider does at runtime. Being +// generated code, a field whose conversion is wrong is a compile error rather +// than a surprise during apply. +func mapRemoteStateToTerraform(ctx context.Context, data *EndpointLabelResourceModel, remote models.Endpoint_Agent_Labels_API_LabelResponseable) diag.Diagnostics { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + + if remote == nil { + tflog.Debug(ctx, "remote resource is nil, leaving state untouched", map[string]any{ + "resource": TypeName, + }) + return diags + } + + data.Color = convert.PtrStringToFramework(remote.GetColor()) + data.Filters, d = flattenEndpointLabelFilters(ctx, remote.GetFilters()) + diags.Append(d...) + data.ID = convert.PtrStringToFramework(remote.GetId()) + data.MatchType = convert.PtrStringerToFramework(remote.GetMatchType()) + data.Name = convert.PtrStringToFramework(remote.GetName()) + + tflog.Debug(ctx, "mapped remote state", map[string]any{"resource": TypeName}) + + return diags +} + +// flattenEndpointLabelFilters converts SDK structs into the framework value state holds. +func flattenEndpointLabelFilters(ctx context.Context, in []models.Endpoint_Agent_Labels_API_Filterable) (types.Set, diag.Diagnostics) { + var diags diag.Diagnostics + var d diag.Diagnostics + + // A nil slice is null and an empty slice is empty. Conflating them produces a + // diff no configuration change can resolve. + if in == nil { + return types.SetNull(endpointLabelFilterObjectType), diags + } + + elements := make([]EndpointLabelFilterModel, 0, len(in)) + for _, item := range in { + var m EndpointLabelFilterModel + + m.Key = convert.PtrStringerToFramework(item.GetKey()) + m.Mode = convert.PtrStringerToFramework(item.GetMode()) + m.Values, d = convert.StringSliceToFrameworkSet(ctx, item.GetValues()) + diags.Append(d...) + elements = append(elements, m) + } + + out, d2 := types.SetValueFrom(ctx, endpointLabelFilterObjectType, elements) + diags.Append(d2...) + + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/test_helper_test.go new file mode 100644 index 00000000..cf9bf4dc --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/test_helper_test.go @@ -0,0 +1,40 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package endpoint_label + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/exists" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" +) + +// EndpointLabelTestResource answers whether a thousandeyes_endpoint_label is really in the API. +// +// In a _test.go file rather than an ordinary one, so no test scaffolding reaches a built +// provider. An external _test package still sees the exported identifiers of a package's own +// test files, which is what lets the generated acceptance test refer to this. +type EndpointLabelTestResource struct{} + +// Exists reads the object back through the same operation the resource's Read uses. +// +// Deliberately the same call rather than an equivalent one. A check that asked a list endpoint +// instead could report an object present while the read the provider actually depends on was +// broken, which is the failure an acceptance test exists to catch. +func (EndpointLabelTestResource) Exists( + ctx context.Context, + state *terraform.InstanceState, +) (*bool, error) { + return exists.Check(ctx, state, func( + ctx context.Context, + client *sdk.ThousandEyesClient, + state *terraform.InstanceState, + ) error { + _, err := client.Endpoint().Labels().ById(state.ID).Get(ctx, nil) + + return err + }) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/maximal.tf new file mode 100644 index 00000000..839f1116 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/maximal.tf @@ -0,0 +1,25 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The widest configuration the evidence supports: every writable attribute with a derivable +# value, chosen by the same derivation the rehearsal probe sends to the live API. +# +# Regenerated and policed, deliberately. This file used to be a hand-owned scaffold, and +# hand edits are how its values drifted from the generated assertions -- the checks derive +# from the blueprint, so the blueprint is where a value is corrected: an accFixture hint for +# something only a human knows, recorded behaviour for everything the probe observed. What +# could not be derived is listed inside the block, each with the reason. + +resource "thousandeyes_endpoint_label" "test" { + # Where a value came from. + # + # color: the API's own default, so a value it is known to accept + # + # filters: curated in the blueprint's accFixture; the generator cannot derive it + # + # match_type: observed accepted + color = "#d9d9d9" + filters = [{ key = "platform", mode = "in", values = ["windows"] }] + match_type = "and" + name = "tfacc-name" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/minimal.tf new file mode 100644 index 00000000..0257b9c1 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/endpoint_agent_labels/v7/endpoint_label/testdata/minimal.tf @@ -0,0 +1,22 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The smallest configuration this resource accepts: its required attributes and nothing else. +# +# Regenerated, so it follows the schema. If the API starts requiring a field, this file gains it +# on the next emit rather than an acceptance run discovering it. + +resource "thousandeyes_endpoint_label" "test" { + # Where a value came from. A fixture takes what the API was observed to accept, not the first + # value the specification documents -- which is the opposite of how a generated validator is + # built. A validator errs toward permitting, because blocking a configuration the API would + # have taken is a harm nobody can work around; a fixture errs toward working, because it is an + # input rather than a contract. + # + # filters: curated in the blueprint's accFixture; the generator cannot derive it + # + # match_type: observed accepted + filters = [{ key = "platform", mode = "in", values = ["windows"] }] + match_type = "and" + name = "tfacc-name" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/construct.go new file mode 100644 index 00000000..afc8e133 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/construct.go @@ -0,0 +1,35 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// constructResource maps Terraform configuration onto the API's request body. +// +// Only writable attributes appear here. A computed attribute has no expand +// conversion, so it cannot be sent by construction rather than by discipline. +func constructResource(ctx context.Context, data *RoleResourceModel) (*models.Administrative_API_RoleRequestBody, diag.Diagnostics) { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + tflog.Debug(ctx, "constructing request body", map[string]any{"resource": TypeName}) + + body := models.NewAdministrative_API_RoleRequestBody() + + body.SetName(convert.FrameworkToPtrString(data.Name)) + permissionsRaw, d := convert.FrameworkSetToStringSlice(ctx, data.Permissions) + diags.Append(d...) + body.SetPermissions(permissionsRaw) + + return body, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/crud.go new file mode 100644 index 00000000..ce728b81 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/crud.go @@ -0,0 +1,266 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/crud" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/errors" +) + +// Create provisions a new thousandeyes_role. +func (r *RoleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan RoleResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "creating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseCreate, CreateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + created, err := r.client.Roles().Post(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, err) + return + } + + // An empty success body cannot be recorded: without a response there is no + // identifier, and inventing one would orphan whatever the API just created. + if created == nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, errors.ErrEmptyResponse) + return + } + + // The identifier is assigned first, so that a partially-successful create still + // leaves a resource Terraform can find and delete rather than an orphan it has no + // record of. + plan.ID = convert.PtrStringToFramework(created.GetRoleId()) + + // State comes from a read, not from the create response. The two are not + // interchangeable: a field the API only returns when asked to expand it is absent + // from a create response, so mapping that response would write it null and the next + // read would contradict it -- a diff no configuration change can resolve. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpCreate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Read refreshes thousandeyes_role state from the API. +func (r *RoleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state RoleResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "reading resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseRead, ReadTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + found, diags := r.readState(ctx, &state, errors.OpRead) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Something deleted outside Terraform must produce a plan that recreates it, not an + // error the practitioner has no way to clear. + if !found { + tflog.Debug(ctx, "resource no longer exists, removing it from state", map[string]any{ + "resource": TypeName, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// readState fetches thousandeyes_role and maps it onto state. +// +// The single call site of mapRemoteStateToTerraform. Create, Update and Read all arrive +// here, so a change to state mapping is one edit rather than three that have to agree. +// +// The bool distinguishes "not there" from "the request failed", which need different +// handling and used to be conflated: Read turns absence into a recreate, while a read +// after a write treats it as not-yet-consistent and tries again. +func (r *RoleResource) readState( + ctx context.Context, + state *RoleResourceModel, + op errors.Operation, +) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + remote, err := r.client.Roles().ById(state.ID.ValueString()).Get(ctx, nil) + if err != nil { + if errors.IsNotFound(err) { + return false, diags + } + + errors.Handle(&diags, TypeName, op, err) + + return false, diags + } + + // An empty success body means the object is gone as far as this provider can + // tell; treat it exactly like a not-found rather than mapping nothing. + if remote == nil { + return false, diags + } + mapRemoteStateToTerraform(ctx, state, remote) + + return true, diags +} + +// readAfterWrite refreshes state from the API after a create or an update, retrying while +// the object is not yet readable. +// +// Why a read at all, rather than mapping the write's own response: the two are not +// interchangeable. A field the API only returns when asked to expand it is absent from a +// write response, so mapping that response writes it null and the next read contradicts +// it. The pilot's own probe evidence says so. +// +// Why a retry: an API that is not read-your-writes consistent returns 404 for an object it +// has just created, and a create that reports success on state the next plan disagrees with +// is worse than one that waits. +// +// The budget is the built-in default: this API's consistency has not been measured, so +// there is no observation to size it from. An API that is consistent succeeds on the first +// attempt and pays nothing for the loop. +func (r *RoleResource) readAfterWrite( + ctx context.Context, + state *RoleResourceModel, + op errors.Operation, +) diag.Diagnostics { + var diags diag.Diagnostics + + opts := crud.ReadBackOptions{ + MaxRetries: 10, + Interval: 2000 * time.Millisecond, + ResourceType: TypeName, + Operation: string(op), + } + + err := crud.ReadBack(ctx, opts, func(ctx context.Context) (bool, error) { + found, d := r.readState(ctx, state, op) + diags.Append(d...) + + if d.HasError() { + return false, crud.ErrStopRetrying + } + + return found, nil + }) + + // A stop signal means readState has already produced the diagnostic, so reporting + // here as well would say the same thing twice. + if err != nil && !crud.IsStopRetrying(err) { + errors.Handle(&diags, TypeName, op, err) + } + + return diags +} + +// Update applies configuration changes to an existing thousandeyes_role. +func (r *RoleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state RoleResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "updating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseUpdate, UpdateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.Roles().ById(state.ID.ValueString()).Put(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpUpdate, err) + return + } + + // The plan carries no identifier, since it is computed. Carrying it over from + // prior state keeps it in the model in case the response omits it. + plan.ID = state.ID + + // As with create: state comes from a read rather than from the update response. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpUpdate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete removes a thousandeyes_role. +func (r *RoleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state RoleResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseDelete, DeleteTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.client.Roles().ById(state.ID.ValueString()).Delete(ctx, nil) + if err != nil { + // Already gone is the outcome delete was asking for. + if errors.IsNotFound(err) { + return + } + errors.Handle(&resp.Diagnostics, TypeName, errors.OpDelete, err) + return + } + + // State is removed by the framework once Delete returns without error. +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/model.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/model.go new file mode 100644 index 00000000..b98efbcb --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/model.go @@ -0,0 +1,20 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// RoleResourceModel is the Terraform state for a thousandeyes_role. +// +// Field order follows the schema, and the timeouts value is last. +type RoleResourceModel struct { + IsBuiltin types.Bool `tfsdk:"is_builtin"` + Name types.String `tfsdk:"name"` + Permissions types.Set `tfsdk:"permissions"` + ID types.String `tfsdk:"id"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource.go new file mode 100644 index 00000000..16c502c8 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource.go @@ -0,0 +1,102 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/client" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" + commonschema "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/schema" +) + +const ( + // TypeName is the Terraform type this resource registers as. + TypeName = "thousandeyes_role" + + // Default per-operation timeouts, in seconds. A practitioner overrides them + // with the resource's timeouts block. + CreateTimeout = 180 + ReadTimeout = 180 + UpdateTimeout = 180 + DeleteTimeout = 180 +) + +// Compile-time assertions that this resource implements what it claims to. They +// turn a missing method into a build failure rather than a runtime surprise. +var ( + _ resource.Resource = (*RoleResource)(nil) + _ resource.ResourceWithConfigure = (*RoleResource)(nil) + _ resource.ResourceWithImportState = (*RoleResource)(nil) +) + +// NewRoleResource returns the thousandeyes_role resource. +func NewRoleResource() resource.Resource { + return &RoleResource{} +} + +// RoleResource implements the thousandeyes_role resource. +type RoleResource struct { + client *sdk.ThousandEyesClient +} + +// Metadata returns the resource type name. +func (r *RoleResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TypeName +} + +// Configure receives the configured SDK client from the provider. +func (r *RoleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = client.ForResource(ctx, req, resp, TypeName) +} + +// ImportState maps a terraform import into state. +func (r *RoleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Schema returns the resource schema. +func (r *RoleResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve role.", + Attributes: map[string]schema.Attribute{ + "is_builtin": schema.BoolAttribute{ + Computed: true, + MarkdownDescription: "Flag indicating if the role is built-in (Account Admin, Organization Admin, Regular User).", + }, + "name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Name of the role. Required-by-API by curation: the sweeper's field, unprobeable by " + + "omission.", + }, + "permissions": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "List of permission ids to grant (permissionId values from the permissions API). The " + + "response echoes permissions as expanded objects, never this id list, so state carries the " + + "configured value. Effectively required: a permission-less role create answers 500 (live " + + "probe, 2026-08-03). The API enforces this field's " + + "presence, which the specification does not declare. ", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique ID representing the role. Named `id` per the import convention; the wire field " + + "stays roleId.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": commonschema.ResourceTimeouts(ctx), + }, + } +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource_acceptance_test.go new file mode 100644 index 00000000..30c8acc0 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/resource_acceptance_test.go @@ -0,0 +1,92 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role_test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/check" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/destroy" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role" +) + +// The resource under test, and the helper that asks the API whether it is really there. +const address = "thousandeyes_role.test" + +var testResource = role.RoleTestResource{} + +// TestAccResourceRole_01_Lifecycle exercises the whole lifecycle against a live API. +// +// Guarded by TF_ACC, which terraform-plugin-testing requires, so `go test ./...` never reaches a +// live tenant by accident. The credential comes from the environment; see internal/acceptance. +// +// Every step ends with an implicit plan, and terraform-plugin-testing fails the step if that plan +// is not empty. That is the single strongest signal available that state mapping is right: an +// attribute the provider stores differently from how the API reports it shows up here as a diff on +// a resource nobody touched, and no amount of compiling or schema checking can see it. +func TestAccResourceRole_01_Lifecycle(t *testing.T) { + // This resource needs more privilege than the ordinary credential carries, so the + // gate is its own variable: a run against a lesser token skips it by name here, + // stated, instead of failing red for a reason that is about the token. + if os.Getenv("TFPFGEN_ACC_ADMIN") == "" { + t.Skipf("set TFPFGEN_ACC_ADMIN to run this test; it needs a more privileged credential") + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + // Destroy is verified against the API rather than against state, because Terraform + // considers a destroy done when the provider's Delete returns no error -- which is a + // statement about the request, not about the object. + // + // The wait is the built-in default; this API's consistency after delete was never measured, + // so this is a budget rather than an observation + CheckDestroy: destroy.CheckDestroyed(testResource, "thousandeyes_role", 20*time.Second), + Steps: []resource.TestStep{ + // Create, from the smallest configuration the schema permits. + { + Config: config(t, "minimal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("name").HasValue("tfacc-name"), + ), + }, + // Import, then compare every attribute against the state the apply produced. + // + // This is what catches a read that populates state differently from a create, which + // is the most common way a generated provider is subtly wrong. + { + ResourceName: address, + ImportState: true, + ImportStateVerify: true, + // Skipped, with the reason for each. An attribute the API never returns is + // absent after an import and present before it, so comparing it fails for a + // reason that has nothing to do with import being broken. + // permissions: it is write-only, carried from configuration, which an import does not have + ImportStateVerifyIgnore: []string{"permissions"}, + }, + }, + }) +} + +// config reads one of the committed fixtures. +// +// From testdata rather than from a string constant in this file, so the configuration can be +// edited and re-run without recompiling, and so `terraform fmt` can police it. +func config(t *testing.T, name string) string { + t.Helper() + + body, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading fixture %s: %v", name, err) + } + + return string(body) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/state.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/state.go new file mode 100644 index 00000000..4d8abb40 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/state.go @@ -0,0 +1,33 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// mapRemoteStateToTerraform maps the API's response onto Terraform state. +// +// This function replaces what a reflection-based provider does at runtime. Being +// generated code, a field whose conversion is wrong is a compile error rather +// than a surprise during apply. +func mapRemoteStateToTerraform(ctx context.Context, data *RoleResourceModel, remote models.Administrative_API_RoleDetailable) { + if remote == nil { + tflog.Debug(ctx, "remote resource is nil, leaving state untouched", map[string]any{ + "resource": TypeName, + }) + return + } + + data.IsBuiltin = convert.PtrBoolToFramework(remote.GetIsBuiltin()) + data.Name = convert.PtrStringToFramework(remote.GetName()) + data.ID = convert.PtrStringToFramework(remote.GetRoleId()) + + tflog.Debug(ctx, "mapped remote state", map[string]any{"resource": TypeName}) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/test_helper_test.go new file mode 100644 index 00000000..e06210b0 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/test_helper_test.go @@ -0,0 +1,40 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package role + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/exists" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" +) + +// RoleTestResource answers whether a thousandeyes_role is really in the API. +// +// In a _test.go file rather than an ordinary one, so no test scaffolding reaches a built +// provider. An external _test package still sees the exported identifiers of a package's own +// test files, which is what lets the generated acceptance test refer to this. +type RoleTestResource struct{} + +// Exists reads the object back through the same operation the resource's Read uses. +// +// Deliberately the same call rather than an equivalent one. A check that asked a list endpoint +// instead could report an object present while the read the provider actually depends on was +// broken, which is the failure an acceptance test exists to catch. +func (RoleTestResource) Exists( + ctx context.Context, + state *terraform.InstanceState, +) (*bool, error) { + return exists.Check(ctx, state, func( + ctx context.Context, + client *sdk.ThousandEyesClient, + state *terraform.InstanceState, + ) error { + _, err := client.Roles().ById(state.ID).Get(ctx, nil) + + return err + }) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/maximal.tf new file mode 100644 index 00000000..b0038d17 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/maximal.tf @@ -0,0 +1,19 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The widest configuration the evidence supports: every writable attribute with a derivable +# value, chosen by the same derivation the rehearsal probe sends to the live API. +# +# Regenerated and policed, deliberately. This file used to be a hand-owned scaffold, and +# hand edits are how its values drifted from the generated assertions -- the checks derive +# from the blueprint, so the blueprint is where a value is corrected: an accFixture hint for +# something only a human knows, recorded behaviour for everything the probe observed. What +# could not be derived is listed inside the block, each with the reason. + +resource "thousandeyes_role" "test" { + # Where a value came from. + # + # permissions: curated in the blueprint's accFixture; the generator cannot derive it + name = "tfacc-name" + permissions = ["1", "2"] +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/minimal.tf new file mode 100644 index 00000000..71f13a4e --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/roles/v7/role/testdata/minimal.tf @@ -0,0 +1,19 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The smallest configuration this resource accepts: its required attributes and nothing else. +# +# Regenerated, so it follows the schema. If the API starts requiring a field, this file gains it +# on the next emit rather than an acceptance run discovering it. + +resource "thousandeyes_role" "test" { + # Where a value came from. A fixture takes what the API was observed to accept, not the first + # value the specification documents -- which is the opposite of how a generated validator is + # built. A validator errs toward permitting, because blocking a configuration the API would + # have taken is a harm nobody can work around; a fixture errs toward working, because it is an + # input rather than a contract. + # + # permissions: curated in the blueprint's accFixture; the generator cannot derive it + name = "tfacc-name" + permissions = ["1", "2"] +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/conditional_validators.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/conditional_validators.go index bd85c799..336a0c52 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/conditional_validators.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/conditional_validators.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/construct.go index 1f02156b..bdc35c29 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/construct.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/construct.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/crud.go index 8cf1fcc7..52f5b981 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/crud.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/crud.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_acceptance_test.go index 6d9adef3..82ced49d 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag_test diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_resource.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_resource.go index 998c2204..6402b302 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_resource.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/list_resource.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/model.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/model.go index c77ee2f7..581c0ec8 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/model.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/model.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. // REF: https://developer.cisco.com/docs/thousandeyes/list-tags/ package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource.go index 8b762ad3..2d7d959c 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource_acceptance_test.go index f149f038..cb7d241f 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource_acceptance_test.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/resource_acceptance_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag_test diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/state.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/state.go index 248f35f1..95d2c403 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/state.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/state.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/test_helper_test.go index 8d7ea434..3282cda6 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/test_helper_test.go +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/test_helper_test.go @@ -1,5 +1,5 @@ // Code generated by tfpfgen from blueprints/thousandeyes-kiota -// (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. package tag diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/maximal.tf index 17c8b2bc..c6fd053d 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/maximal.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/maximal.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The widest configuration the evidence supports: every writable attribute with a derivable # value, chosen by the same derivation the rehearsal probe sends to the live API. diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/minimal.tf index 184453cb..28b3913f 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/minimal.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/minimal.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The smallest configuration this resource accepts: its required attributes and nothing else. # diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/query.tf b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/query.tf index a6b78181..ffa36454 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/query.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/query.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The query configuration: one list block over the whole collection. The provider block # is explicit because the list block references it by symbol, unlike a resource step diff --git a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/replace.tf b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/replace.tf index e3adde29..33d8258e 100644 --- a/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/replace.tf +++ b/pilot/thousandeyes-kiota/internal/services/resources/tags/v7/tag/testdata/replace.tf @@ -1,5 +1,5 @@ # Code generated by tfpfgen from blueprints/thousandeyes-kiota -# (sha256:e2e4011910f8a9637cfa5f8068ccb463ea4f18a477cf56bdcfcd868b342b4781). DO NOT EDIT. +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. # The smallest configuration this resource accepts: its required attributes and nothing else. # diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/construct.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/construct.go new file mode 100644 index 00000000..4c572db4 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/construct.go @@ -0,0 +1,71 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// constructResource maps Terraform configuration onto the API's request body. +// +// Only writable attributes appear here. A computed attribute has no expand +// conversion, so it cannot be sent by construction rather than by discipline. +func constructResource(ctx context.Context, data *UserResourceModel) (*models.Administrative_API_UserRequest, diag.Diagnostics) { + var ( + diags diag.Diagnostics + d diag.Diagnostics + ) + tflog.Debug(ctx, "constructing request body", map[string]any{"resource": TypeName}) + + body := models.NewAdministrative_API_UserRequest() + + accountGroupRolesRaw, d := expandUserAccountGroupRoles(ctx, data.AccountGroupRoles) + diags.Append(d...) + body.SetAccountGroupRoles(accountGroupRolesRaw) + allAccountGroupRoleIDsRaw, d := convert.FrameworkSetToStringSlice(ctx, data.AllAccountGroupRoleIDs) + diags.Append(d...) + body.SetAllAccountGroupRoleIds(allAccountGroupRoleIDsRaw) + body.SetEmail(convert.FrameworkToPtrString(data.Email)) + body.SetLoginAccountGroupId(convert.FrameworkToPtrString(data.LoginAccountGroupID)) + body.SetName(convert.FrameworkToPtrString(data.Name)) + + return body, diags +} + +// expandUserAccountGroupRoles converts the configured UserAccountGroupRoleModel values into SDK structs. +func expandUserAccountGroupRoles(ctx context.Context, v types.Set) ([]models.Administrative_API_UserAccountGroupRoleable, diag.Diagnostics) { + var diags diag.Diagnostics + + // Null and unknown both yield nil so the field is omitted from the request, + // which is distinct from an empty collection asking the API to clear it. + if v.IsNull() || v.IsUnknown() { + return nil, diags + } + + var elements []UserAccountGroupRoleModel + diags.Append(v.ElementsAs(ctx, &elements, false)...) + if diags.HasError() { + return nil, diags + } + + out := make([]models.Administrative_API_UserAccountGroupRoleable, 0, len(elements)) + for _, m := range elements { + item := models.NewAdministrative_API_UserAccountGroupRole() + + item.SetAccountGroupId(convert.FrameworkToPtrString(m.AccountGroupID)) + roleIdsRaw, d := convert.FrameworkSetToStringSlice(ctx, m.RoleIds) + diags.Append(d...) + item.SetRoleIds(roleIdsRaw) + out = append(out, item) + } + + return out, diags +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/crud.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/crud.go new file mode 100644 index 00000000..32e8746d --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/crud.go @@ -0,0 +1,266 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/crud" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/errors" +) + +// Create provisions a new thousandeyes_user. +func (r *UserResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan UserResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "creating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseCreate, CreateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + created, err := r.client.Users().Post(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, err) + return + } + + // An empty success body cannot be recorded: without a response there is no + // identifier, and inventing one would orphan whatever the API just created. + if created == nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpCreate, errors.ErrEmptyResponse) + return + } + + // The identifier is assigned first, so that a partially-successful create still + // leaves a resource Terraform can find and delete rather than an orphan it has no + // record of. + plan.ID = convert.PtrStringToFramework(created.GetUid()) + + // State comes from a read, not from the create response. The two are not + // interchangeable: a field the API only returns when asked to expand it is absent + // from a create response, so mapping that response would write it null and the next + // read would contradict it -- a diff no configuration change can resolve. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpCreate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Read refreshes thousandeyes_user state from the API. +func (r *UserResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state UserResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "reading resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseRead, ReadTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + found, diags := r.readState(ctx, &state, errors.OpRead) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Something deleted outside Terraform must produce a plan that recreates it, not an + // error the practitioner has no way to clear. + if !found { + tflog.Debug(ctx, "resource no longer exists, removing it from state", map[string]any{ + "resource": TypeName, + }) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +// readState fetches thousandeyes_user and maps it onto state. +// +// The single call site of mapRemoteStateToTerraform. Create, Update and Read all arrive +// here, so a change to state mapping is one edit rather than three that have to agree. +// +// The bool distinguishes "not there" from "the request failed", which need different +// handling and used to be conflated: Read turns absence into a recreate, while a read +// after a write treats it as not-yet-consistent and tries again. +func (r *UserResource) readState( + ctx context.Context, + state *UserResourceModel, + op errors.Operation, +) (bool, diag.Diagnostics) { + var diags diag.Diagnostics + + remote, err := r.client.Users().ById(state.ID.ValueString()).Get(ctx, nil) + if err != nil { + if errors.IsNotFound(err) { + return false, diags + } + + errors.Handle(&diags, TypeName, op, err) + + return false, diags + } + + // An empty success body means the object is gone as far as this provider can + // tell; treat it exactly like a not-found rather than mapping nothing. + if remote == nil { + return false, diags + } + mapRemoteStateToTerraform(ctx, state, remote) + + return true, diags +} + +// readAfterWrite refreshes state from the API after a create or an update, retrying while +// the object is not yet readable. +// +// Why a read at all, rather than mapping the write's own response: the two are not +// interchangeable. A field the API only returns when asked to expand it is absent from a +// write response, so mapping that response writes it null and the next read contradicts +// it. The pilot's own probe evidence says so. +// +// Why a retry: an API that is not read-your-writes consistent returns 404 for an object it +// has just created, and a create that reports success on state the next plan disagrees with +// is worse than one that waits. +// +// The budget is the built-in default: this API's consistency has not been measured, so +// there is no observation to size it from. An API that is consistent succeeds on the first +// attempt and pays nothing for the loop. +func (r *UserResource) readAfterWrite( + ctx context.Context, + state *UserResourceModel, + op errors.Operation, +) diag.Diagnostics { + var diags diag.Diagnostics + + opts := crud.ReadBackOptions{ + MaxRetries: 10, + Interval: 2000 * time.Millisecond, + ResourceType: TypeName, + Operation: string(op), + } + + err := crud.ReadBack(ctx, opts, func(ctx context.Context) (bool, error) { + found, d := r.readState(ctx, state, op) + diags.Append(d...) + + if d.HasError() { + return false, crud.ErrStopRetrying + } + + return found, nil + }) + + // A stop signal means readState has already produced the diagnostic, so reporting + // here as well would say the same thing twice. + if err != nil && !crud.IsStopRetrying(err) { + errors.Handle(&diags, TypeName, op, err) + } + + return diags +} + +// Update applies configuration changes to an existing thousandeyes_user. +func (r *UserResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state UserResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "updating resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, plan.Timeouts, crud.PhaseUpdate, UpdateTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + body, constructDiags := constructResource(ctx, &plan) + resp.Diagnostics.Append(constructDiags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.client.Users().ById(state.ID.ValueString()).Put(ctx, body, nil) + if err != nil { + errors.Handle(&resp.Diagnostics, TypeName, errors.OpUpdate, err) + return + } + + // The plan carries no identifier, since it is computed. Carrying it over from + // prior state keeps it in the model in case the response omits it. + plan.ID = state.ID + + // As with create: state comes from a read rather than from the update response. + resp.Diagnostics.Append(r.readAfterWrite(ctx, &plan, errors.OpUpdate)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +// Delete removes a thousandeyes_user. +func (r *UserResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state UserResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "deleting resource", map[string]any{"resource": TypeName}) + + timeout := crud.Timeout(ctx, state.Timeouts, crud.PhaseDelete, DeleteTimeout*time.Second, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + err := r.client.Users().ById(state.ID.ValueString()).Delete(ctx, nil) + if err != nil { + // Already gone is the outcome delete was asking for. + if errors.IsNotFound(err) { + return + } + errors.Handle(&resp.Diagnostics, TypeName, errors.OpDelete, err) + return + } + + // State is removed by the framework once Delete returns without error. +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/model.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/model.go new file mode 100644 index 00000000..19751f6b --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/model.go @@ -0,0 +1,45 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// UserResourceModel is the Terraform state for a thousandeyes_user. +// +// Field order follows the schema, and the timeouts value is last. +type UserResourceModel struct { + AccountGroupRoles types.Set `tfsdk:"account_group_roles"` + AllAccountGroupRoleIDs types.Set `tfsdk:"all_account_group_role_ids"` + DateRegistered types.String `tfsdk:"date_registered"` + Email types.String `tfsdk:"email"` + LastLogin types.String `tfsdk:"last_login"` + LoginAccountGroupID types.String `tfsdk:"login_account_group_id"` + Name types.String `tfsdk:"name"` + ID types.String `tfsdk:"id"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// UserAccountGroupRoleModel is one element of a nested attribute. +// +// It is a sibling of the resource model rather than an inner type, because the +// framework decodes collection elements into a named type. +type UserAccountGroupRoleModel struct { + AccountGroupID types.String `tfsdk:"account_group_id"` + RoleIds types.Set `tfsdk:"role_ids"` +} + +// userAccountGroupRoleAttrTypes describes UserAccountGroupRoleModel to the framework. +// +// It is declared once and referenced by both conversion helpers, so the shape +// cannot drift between the direction that reads it and the direction that writes it. +var userAccountGroupRoleAttrTypes = map[string]attr.Type{ + "account_group_id": types.StringType, + "role_ids": types.SetType{ElemType: types.StringType}, +} + +var userAccountGroupRoleObjectType = types.ObjectType{AttrTypes: userAccountGroupRoleAttrTypes} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource.go new file mode 100644 index 00000000..14583210 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource.go @@ -0,0 +1,138 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/client" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" + commonschema "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/schema" +) + +const ( + // TypeName is the Terraform type this resource registers as. + TypeName = "thousandeyes_user" + + // Default per-operation timeouts, in seconds. A practitioner overrides them + // with the resource's timeouts block. + CreateTimeout = 180 + ReadTimeout = 180 + UpdateTimeout = 180 + DeleteTimeout = 180 +) + +// Compile-time assertions that this resource implements what it claims to. They +// turn a missing method into a build failure rather than a runtime surprise. +var ( + _ resource.Resource = (*UserResource)(nil) + _ resource.ResourceWithConfigure = (*UserResource)(nil) + _ resource.ResourceWithImportState = (*UserResource)(nil) +) + +// NewUserResource returns the thousandeyes_user resource. +func NewUserResource() resource.Resource { + return &UserResource{} +} + +// UserResource implements the thousandeyes_user resource. +type UserResource struct { + client *sdk.ThousandEyesClient +} + +// Metadata returns the resource type name. +func (r *UserResource) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = TypeName +} + +// Configure receives the configured SDK client from the provider. +func (r *UserResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client = client.ForResource(ctx, req, resp, TypeName) +} + +// ImportState maps a terraform import into state. +func (r *UserResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// Schema returns the resource schema. +func (r *UserResource) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Retrieve user.", + Attributes: map[string]schema.Attribute{ + "account_group_roles": schema.SetNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "account_group_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Unique ID of the account group.", + }, + "role_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Unique role IDs to grant in the account group.", + }, + }, + }, + Optional: true, + MarkdownDescription: "Role assignments to write: account group id plus role ids. The response echoes assignments " + + "as expanded objects under allAccountGroupRoles, never in this shape, so state carries the " + + "configured value. The API refuses a user without at least one role (live probe, " + + "2026-08-03). The API enforces this field's presence, " + + "which the specification does not declare. ", + }, + "all_account_group_role_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: "Unique IDs representing the roles. The response echoes roles as nested objects, never as " + + "this flat id list; state carries the configured value. Omitted from generated fixtures: " + + "its members are live role ids, and account_group_roles already carries the assignment the " + + "fixtures exercise.", + }, + "date_registered": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "UTC date the user registered their account (ISO date-time format).", + }, + "email": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "User's email address. The API enforces this field's " + + "presence, which the specification does not declare. ", + }, + "last_login": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "UTC last login of the user (ISO date-time format).", + }, + "login_account_group_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "Unique ID of the login account group. The response echoes loginAccountGroup as a nested " + + "object, never this id; state carries the configured value. The API refuses a user without " + + "a login account group (live probe, 2026-08-03). The " + + "API enforces this field's presence, which the specification does not declare. ", + }, + "name": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "User's display name. Required-by-API by curation: the sweeper's field, unprobeable by " + + "omission.", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Unique ID of the user.\n\nNamed `id` per the import convention; the wire field stays uid.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": commonschema.ResourceTimeouts(ctx), + }, + } +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource_acceptance_test.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource_acceptance_test.go new file mode 100644 index 00000000..9dd3863e --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/resource_acceptance_test.go @@ -0,0 +1,98 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user_test + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/check" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/destroy" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user" +) + +// The resource under test, and the helper that asks the API whether it is really there. +const address = "thousandeyes_user.test" + +var testResource = user.UserTestResource{} + +// TestAccResourceUser_01_Lifecycle exercises the whole lifecycle against a live API. +// +// Guarded by TF_ACC, which terraform-plugin-testing requires, so `go test ./...` never reaches a +// live tenant by accident. The credential comes from the environment; see internal/acceptance. +// +// Every step ends with an implicit plan, and terraform-plugin-testing fails the step if that plan +// is not empty. That is the single strongest signal available that state mapping is right: an +// attribute the provider stores differently from how the API reports it shows up here as a diff on +// a resource nobody touched, and no amount of compiling or schema checking can see it. +func TestAccResourceUser_01_Lifecycle(t *testing.T) { + // This resource needs more privilege than the ordinary credential carries, so the + // gate is its own variable: a run against a lesser token skips it by name here, + // stated, instead of failing red for a reason that is about the token. + if os.Getenv("TFPFGEN_ACC_ADMIN") == "" { + t.Skipf("set TFPFGEN_ACC_ADMIN to run this test; it needs a more privileged credential") + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories, + // Destroy is verified against the API rather than against state, because Terraform + // considers a destroy done when the provider's Delete returns no error -- which is a + // statement about the request, not about the object. + // + // The wait is the built-in default; this API's consistency after delete was never measured, + // so this is a budget rather than an observation + CheckDestroy: destroy.CheckDestroyed(testResource, "thousandeyes_user", 20*time.Second), + Steps: []resource.TestStep{ + // Create, from the smallest configuration the schema permits. + { + Config: config(t, "minimal.tf"), + Check: resource.ComposeAggregateTestCheckFunc( + check.That(address).ExistsInAPI(testResource), + check.That(address).Key("id").Exists(), + check.That(address).Key("email").Exists(), + check.That(address).Key("name").HasValue("tfacc-name"), + ), + }, + // Import, then compare every attribute against the state the apply produced. + // + // This is what catches a read that populates state differently from a create, which + // is the most common way a generated provider is subtly wrong. + { + ResourceName: address, + ImportState: true, + ImportStateVerify: true, + // Skipped, with the reason for each. An attribute the API never returns is + // absent after an import and present before it, so comparing it fails for a + // reason that has nothing to do with import being broken. + // account_group_roles: it is write-only, carried from configuration, which an import does not + // have + // all_account_group_role_ids: it is write-only, carried from configuration, which an import + // does not have + // login_account_group_id: it is write-only, carried from configuration, which an import does + // not have + ImportStateVerifyIgnore: []string{"account_group_roles", "all_account_group_role_ids", "login_account_group_id"}, + }, + }, + }) +} + +// config reads one of the committed fixtures. +// +// From testdata rather than from a string constant in this file, so the configuration can be +// edited and re-run without recompiling, and so `terraform fmt` can police it. +func config(t *testing.T, name string) string { + t.Helper() + + body, err := os.ReadFile(filepath.Join("testdata", name)) + if err != nil { + t.Fatalf("reading fixture %s: %v", name, err) + } + + return string(body) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/state.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/state.go new file mode 100644 index 00000000..206f7627 --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/state.go @@ -0,0 +1,35 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-log/tflog" + + models "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk/models" + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/services/common/convert" +) + +// mapRemoteStateToTerraform maps the API's response onto Terraform state. +// +// This function replaces what a reflection-based provider does at runtime. Being +// generated code, a field whose conversion is wrong is a compile error rather +// than a surprise during apply. +func mapRemoteStateToTerraform(ctx context.Context, data *UserResourceModel, remote models.Administrative_API_UserDetailable) { + if remote == nil { + tflog.Debug(ctx, "remote resource is nil, leaving state untouched", map[string]any{ + "resource": TypeName, + }) + return + } + + data.DateRegistered = convert.PtrTimeToFramework(remote.GetDateRegistered()) + data.Email = convert.PtrStringToFramework(remote.GetEmail()) + data.LastLogin = convert.PtrTimeToFramework(remote.GetLastLogin()) + data.Name = convert.PtrStringToFramework(remote.GetName()) + data.ID = convert.PtrStringToFramework(remote.GetUid()) + + tflog.Debug(ctx, "mapped remote state", map[string]any{"resource": TypeName}) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/test_helper_test.go b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/test_helper_test.go new file mode 100644 index 00000000..b7681dfb --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/test_helper_test.go @@ -0,0 +1,40 @@ +// Code generated by tfpfgen from blueprints/thousandeyes-kiota +// (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +package user + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/acceptance/exists" + sdk "github.com/deploymenttheory/terraform-plugin-framework-codegen/pilot/thousandeyes-kiota/internal/sdk" +) + +// UserTestResource answers whether a thousandeyes_user is really in the API. +// +// In a _test.go file rather than an ordinary one, so no test scaffolding reaches a built +// provider. An external _test package still sees the exported identifiers of a package's own +// test files, which is what lets the generated acceptance test refer to this. +type UserTestResource struct{} + +// Exists reads the object back through the same operation the resource's Read uses. +// +// Deliberately the same call rather than an equivalent one. A check that asked a list endpoint +// instead could report an object present while the read the provider actually depends on was +// broken, which is the failure an acceptance test exists to catch. +func (UserTestResource) Exists( + ctx context.Context, + state *terraform.InstanceState, +) (*bool, error) { + return exists.Check(ctx, state, func( + ctx context.Context, + client *sdk.ThousandEyesClient, + state *terraform.InstanceState, + ) error { + _, err := client.Users().ById(state.ID).Get(ctx, nil) + + return err + }) +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/maximal.tf b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/maximal.tf new file mode 100644 index 00000000..1f7623ca --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/maximal.tf @@ -0,0 +1,32 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The widest configuration the evidence supports: every writable attribute with a derivable +# value, chosen by the same derivation the rehearsal probe sends to the live API. +# +# Regenerated and policed, deliberately. This file used to be a hand-owned scaffold, and +# hand edits are how its values drifted from the generated assertions -- the checks derive +# from the blueprint, so the blueprint is where a value is corrected: an accFixture hint for +# something only a human knows, recorded behaviour for everything the probe observed. What +# could not be derived is listed inside the block, each with the reason. + +resource "thousandeyes_user" "test" { + # Where a value came from. + # + # account_group_roles: curated in the blueprint's accFixture; the generator cannot derive it + # + # email: curated in the blueprint's accFixture; the generator cannot derive it + # + # login_account_group_id: curated in the blueprint's accFixture; the generator cannot derive + # it + # + # Not filled in, because no correct value could be derived. Each of these is optional, so this + # configuration is valid as it stands -- but it is testing less than it could. + # + # all_account_group_role_ids: curated omission; individually valid, refused in combination -- + # see the attribute's documentation + account_group_roles = [{ account_group_id = "281474976742769", role_ids = ["281474976759598"] }] + email = "dafydd.watkins+tfacc@deploymenttheory.com" + login_account_group_id = "281474976742769" + name = "tfacc-name" +} diff --git a/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/minimal.tf b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/minimal.tf new file mode 100644 index 00000000..7c9cfdfd --- /dev/null +++ b/pilot/thousandeyes-kiota/internal/services/resources/users/v7/user/testdata/minimal.tf @@ -0,0 +1,26 @@ +# Code generated by tfpfgen from blueprints/thousandeyes-kiota +# (sha256:c2cf76ac3103c3952bf7293d203903e0fe737bf6f5557c9886edf186808f4c22). DO NOT EDIT. + +# The smallest configuration this resource accepts: its required attributes and nothing else. +# +# Regenerated, so it follows the schema. If the API starts requiring a field, this file gains it +# on the next emit rather than an acceptance run discovering it. + +resource "thousandeyes_user" "test" { + # Where a value came from. A fixture takes what the API was observed to accept, not the first + # value the specification documents -- which is the opposite of how a generated validator is + # built. A validator errs toward permitting, because blocking a configuration the API would + # have taken is a harm nobody can work around; a fixture errs toward working, because it is an + # input rather than a contract. + # + # account_group_roles: curated in the blueprint's accFixture; the generator cannot derive it + # + # email: curated in the blueprint's accFixture; the generator cannot derive it + # + # login_account_group_id: curated in the blueprint's accFixture; the generator cannot derive + # it + account_group_roles = [{ account_group_id = "281474976742769", role_ids = ["281474976759598"] }] + email = "dafydd.watkins+tfacc@deploymenttheory.com" + login_account_group_id = "281474976742769" + name = "tfacc-name" +}